/* ==========================================================================
   KRISTJAN RUUS
   Single stylesheet. No build step, no framework.

   To swap typefaces later (e.g. Neue Haas Grotesk, Suisse Int'l, Gotham):
   change --font-display / --font-text in the :root block below and replace
   the @font-face rules. Nothing else in this file references a font name.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. FONTS
   Two families, each self-hosted and subset to Latin + Estonian diacritics.

   Libre Franklin — a Franklin Gothic revival. Carries the interface: the
   navigation, every label, and all small print. SIL Open Font License.
   Variable, weight axis trimmed to 400–700. 29 KB.

   TeX Gyre Pagella — a Palatino design. Carries the display name, and is
   held in reserve for editorial body copy. GUST Font License. 25 KB.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "Libre Franklin";
  src: url("/assets/fonts/librefranklin-var.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Pagella";
  src: url("/assets/fonts/pagella-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Pagella";
  src: url("/assets/fonts/pagella-bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Pagella";
  src: url("/assets/fonts/pagella-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

/* --------------------------------------------------------------------------
   2. TOKENS
   -------------------------------------------------------------------------- */

:root {
  /* -- Typefaces. Three roles, three variables. To move to a commercial
        family later (Neue Haas Grotesk, Suisse Int'l, Gotham), add its
        @font-face above and change --font-sans here. Nothing else in this
        file names a typeface. The fallbacks in each stack are metrically
        close, so the swap-in shift stays small. ------------------------- */

  /* Interface: navigation, labels, all small print. */
  --font-sans: "Libre Franklin", "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* Display: the name, and large titles. */
  --font-display: "Pagella", Palatino, "Palatino Linotype", "Book Antiqua",
    Georgia, serif;
  /* Editorial: long-form body copy. */
  --font-text: "Pagella", Palatino, "Palatino Linotype", "Book Antiqua",
    Georgia, serif;

  /* -- Weight of the very large display type. 400 = art book, 700 = masthead. */
  --display-weight: 400;

  /* -- Colour. Warm, never pure #000 / #fff.
        Background alternatives, all tested against --fg and --muted.
        Swap --bg only; the rules derive from --fg and follow automatically.

          #0d0d0c  warm black       fg 15.3  muted 6.6
          #1e1512  chocolate        fg 14.1  muted 6.1
          #241713  dark chocolate   fg 13.7  muted 5.9
          #2e1d14  cocoa            fg 12.7  muted 5.5   <- current
          #38241a  bitter chocolate fg 11.5  muted 5.0
          #2a211b  mocha            fg 12.4  muted 5.3
          #332821  light mocha      fg 11.3  muted 4.9

        Going lighter than #38241a drops --muted below the 4.5 AA floor;
        lighten --muted to #b0aa9f if you ever want to. ------------------ */
  --bg: #2e1d14;
  --fg: #e8e4db;
  --muted: #99968e;
  --rule: rgba(232, 228, 219, 0.16);
  --rule-strong: rgba(232, 228, 219, 0.55);
  /* Client logos sit one step back from body text so the section stays
     quieter than the name. 8.4:1 on the current background. */
  --logo-ink: rgba(232, 228, 219, 0.78);

  /* Waveform. Same cream, three weights — no second hue, no gradient. */
  --wave-base: rgba(232, 228, 219, 0.26);
  --wave-hover: rgba(232, 228, 219, 0.38);
  --wave-fill: rgba(232, 228, 219, 0.92);

  /* -- Space. One scale, used everywhere. -------------------------------- */
  --gutter: clamp(1.25rem, 4.5vw, 5rem);
  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.75rem;
  --space-4: 3rem;
  --space-5: 5rem;
  --space-6: 8rem;
  --measure: 62ch;

  /* -- Clients section metrics.
        These live here, rather than inline in the rules, because the hero
        height is derived from them: the first screen is meant to end just
        below the second logo row, and that only stays true if both places
        read the same numbers. Change spacing here and the hero follows. */
  --clients-pad-block: clamp(2.75rem, 7vh, 5rem);
  --clients-title-pad: clamp(1.5rem, 3vh, 2.25rem);
  --clients-title-h: 1.25rem; /* .meta line box, rounded up */
  --clients-title-mb: clamp(2rem, 5vh, 3.25rem);
  --logo-row-h: clamp(2.75rem, 5vw, 4rem);
  --logo-row-gap: clamp(1.5rem, 4vh, 2.75rem);

  /* Section top edge -> bottom of row 2. */
  --clients-two-rows: calc(
    var(--clients-pad-block) + var(--clients-title-pad) +
      var(--clients-title-h) + var(--clients-title-mb) +
      (var(--logo-row-h) * 2) + var(--logo-row-gap)
  );

  /* -- The fixed player. --player-clear is the bottom clearance the page
        gets once the bar is up; --page-end-bg is the tone that clearance is
        painted in, overridden per page so it continues the final section
        rather than falling back to mocha. ------------------------------- */
  /* Sized from the bar itself (57px desktop, 76px mobile) plus a little air,
     rather than generously rounded up — the slack was enough to tip a page
     that otherwise fits the screen into scrolling for nothing. */
  --player-clear: clamp(3.75rem, 7vh, 4.5rem);
  --page-end-bg: var(--bg);

  /* -- Scrollbar. Track stays transparent so the page's own colour shows
        through on the gradient pages; the thumb is the same deep espresso
        the gradients already end on. ------------------------------------ */
  /* Warm and quiet, but readable against both the mocha at the top of a page
     and the near-black at the bottom of HOME and LISTEN. It was the same near
     black as the page end, which made it invisible exactly where a long page
     most needs a position indicator. */
  --scroll-thumb: #5a4739;

  /* -- Motion. Hover only. Nothing animates on load or on scroll. -------- */
  --hover: 140ms cubic-bezier(0.4, 0, 0.2, 1);

  color-scheme: dark;
}

/* --------------------------------------------------------------------------
   3. RESET / BASE
   -------------------------------------------------------------------------- */

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

html {
  /* Native scrolling. No smoothing, no hijacking, no inertia. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  line-height: 1.6;
  font-kerning: normal;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Guarantee: nothing may ever scroll sideways. */
  overflow-x: hidden;
}

h1,
h2,
h3,
h4,
p,
figure,
blockquote,
ul,
ol,
dl,
dd {
  margin: 0;
}

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

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

a {
  color: inherit;
  text-decoration: none;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

::selection {
  background: var(--fg);
  color: var(--bg);
}

/* Visible but quiet focus ring. Keyboard only. */
:focus-visible {
  outline: 1px solid var(--fg);
  outline-offset: 6px;
}

/* nav.js moves focus to <main> after an internal navigation so the page
   change is announced and the keyboard lands in the new content. <main> is a
   structural container, not a control, so it takes the focus without drawing
   the ring — otherwise every navigation paints a full-width cream outline
   around the whole page. Scoped to that one target: every real control,
   including links, buttons, the play toggles and the waveform, keeps its
   indicator. */
main[tabindex="-1"]:focus,
main[tabindex="-1"]:focus-visible {
  outline: none;
}

/* Progressive enhancement: where the browser allows it, the track is left
   transparent so the page colour behind it stays visible (it changes down the
   page on HOME and LISTEN), and the thumb is dark and quiet rather than the
   default light grey. Where the platform overrides this — macOS overlay
   scrollbars, for instance — the native appearance stands. */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--scroll-thumb) transparent;
}

/* THE BROWSER CANVAS on the two pages that end on an image.

   The canvas is what Safari paints outside the document: the rubber-band area
   past the bottom, the region behind the fixed player once the page has run
   out, and any safe-area gutter. Left alone it takes body's mocha, so dragging
   past the still exposed a band of the original brown under it.

   This is a static terminal colour, deliberately. It was previously driven
   from scroll position so the canvas could follow the ramp; that was fragile
   in iOS Safari and bought little, since the canvas is only ever exposed at
   the edges — and at the bottom, which is where the page has already arrived
   at this exact tone. #0c0806 is the final stop of both ramps (see the two
   gradient blocks at the end of this file): the colour the page is already
   showing where it meets the image. Keep the three in step if any changes.

   The page's own mocha-to-dark progression is untouched by this. It belongs to
   body and to the section ramps, which paint over the canvas everywhere the
   document actually is.

   Giving the root a background stops body's from propagating to the canvas, so
   body then paints its own box as an ordinary block background. That is only
   safe because the ramps sit at z-index: 0 rather than -1 — at negative
   z-index they are painted before in-flow block backgrounds and body's mocha
   buries them completely. If those z-indexes are ever changed back, this must
   go too.

   Keyed off a class set by media.js, not :has(). See flagPageEnding().

   Both html AND body take it. Which of the two Safari stretches into the
   rubber-band area is not worth depending on — body's background box ends with
   the document, but WebKit does not always expose the root's colour there, and
   while body kept the page's mocha that was the brown appearing under the
   still. With the mocha moved onto the content (see below) neither layer can
   show it, whichever one gets exposed.

   Native bounce is deliberately left alone: no overscroll-behavior here, so
   HOME and LISTEN scroll exactly like every other page.

   Site-wide, not just the two image pages. The dark root is the deliberate
   boundary between the browser and the website: on a phone the status bar and
   its indicators sit against it, and on a desktop it is what appears past the
   top of the page instead of the site's own brown bleeding into the toolbars.
   The same colour still does the job it was introduced for at the bottom of
   HOME and LISTEN — one mechanism, both ends, every page. */
html,
body {
  background-color: #0c0806;
}

/* The editorial pages take the mocha back onto body, and guarantee body is at
   least as tall as the screen.

   The dark belongs to the browser edge, not to the page. It was reaching the
   bottom of these pages because main only covers its own content: anything
   shorter than the viewport left the rest of the screen showing the root
   colour, which read as a black footer. Making body mocha and filling the
   viewport means the page's own background continues to the bottom edge, with
   nothing to show through.

   The top is unaffected: that strip is the canvas, which comes from html, and
   html stays dark on every page.

   dvh over svh so the fill follows Safari's collapsing address bar — at 100svh
   the band would reappear the moment the bar hid and the viewport grew. svh is
   kept first as the fallback. Neither adds scrolling: box-sizing is border-box,
   so the player's clearance is already inside the height.

   HOME and LISTEN are excluded. They end on the still and their bottom is
   meant to be dark. */
body:not(.ends-in-image) {
  /* No mocha here. body stays #0c0806 on every page, because Safari derives
     its chrome tint from the root/body background — with mocha on body the
     editorial pages tinted the browser brown while HOME and LISTEN went dark,
     and identical theme-color metadata could not override it. The mocha is a
     property of the page surface instead, which is what the visitor sees.

     Structurally this is what CONTACT was already doing: body becomes a column
     and main takes the slack, so the content surface reaches the bottom of the
     screen on a short page and no dark band can appear beneath it. The rows
     are header / content / end band; the first and last size to their content,
     main absorbs everything left over.

     Nothing here adds scrolling: box-sizing is border-box, so the tracks sum
     to the min-height rather than overflowing it, and a page taller than the
     screen simply grows past it as before.

     The explicit column stays. .shell no longer centres itself with auto
     inline margins — it centres its content with padding, so it stretches on
     its own now — but the track is what guarantees a single full-width column
     for the bands to stretch into, and the width below is what makes them
     take it. Both are cheap and both are load-bearing if .shell ever changes
     again. */
  display: grid;
  grid-template-rows: auto 1fr auto;
  grid-template-columns: minmax(0, 1fr);
  min-height: 100svh;
  min-height: 100dvh;
}

body:not(.ends-in-image) > .shell {
  width: 100%;
}

/* ...and the page's own mocha moves onto the content that should be showing
   it. main covers the whole document on both pages, so this is the same mocha
   in the same places — it is simply owned by the page now instead of by body.
   The ramps and the still paint over it as before: .clients::before and
   .tracks::before are positioned descendants, and .film-still carries its own
   background, so all three land above this.

   The end band is included because it is real page area on the editorial
   pages — without it that strip would read as part of the browser edge rather
   than the foot of the page. On HOME and LISTEN it is display: none, so it
   costs nothing there. */
main,
.site-header,
.site-foot--bare {
  background-color: var(--bg);
}

/* (There was a fixed band here that repainted the top safe area in mocha. It
   is gone: the dark edge is now the intended treatment on every page, so
   covering it up would work against the design.) */

/* The bar itself is transparent as well as the track, so on HOME and LISTEN
   the page's own gradient shows through the full width of the gutter instead
   of a fixed brown strip running down beside a page that is getting darker. */
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
  background: transparent;
}

::-webkit-scrollbar-track {
  background: transparent;
  border: 0;
  box-shadow: none;
}

::-webkit-scrollbar-thumb {
  background-color: var(--scroll-thumb);
  border-radius: 0;
}

/* --------------------------------------------------------------------------
   4. TYPOGRAPHIC PRIMITIVES
   -------------------------------------------------------------------------- */

/* Every label, role line and caption. Authored in sentence case and set
   in caps here, so the markup stays readable and translatable. */
.meta {
  font-family: var(--font-sans);
  font-weight: 500;
  text-transform: uppercase;
  font-variant-numeric: lining-nums tabular-nums;
  letter-spacing: 0.1em;
  font-size: clamp(0.6875rem, 0.64rem + 0.22vw, 0.8125rem);
  line-height: 1.5;
  color: var(--muted);
}

/* Editorial body copy — the one place the serif returns at reading size. */
.prose {
  font-family: var(--font-text);
  font-size: clamp(1.0625rem, 1rem + 0.35vw, 1.3125rem);
  line-height: 1.62;
  max-width: var(--measure);
}

.meta--fg {
  color: var(--fg);
}

/* Timecodes and durations: digits must not jitter while playing. */
.tnum {
  font-variant-numeric: lining-nums tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}

.rule {
  border: 0;
  border-top: 1px solid var(--rule);
}

/* The measure, centred — without the element itself being that narrow.

   This used to be `max-width: 120rem; margin-inline: auto`, which capped the
   box rather than the text inside it. That is fine for a section that carries
   no background, but .shell is also on <main> (CREDITS, ABOUT, CONTACT) and on
   .site-header everywhere, and those are the elements painting the mocha. Past
   120rem the paint stopped with the box: an ultrawide monitor showed a centred
   mocha slab with the dark root canvas down both sides, and on LISTEN and
   MUSIC FILMS a header strip narrower than the page under it.

   Centring the content with padding instead separates the two jobs. The box
   now spans its container, so the background reaches both edges at any width;
   the padding grows symmetrically past the cap and holds the content on the
   same measure it has always had.

   The arithmetic is deliberately the identity: below the cap the gutter wins
   and nothing changes at all, and above it the content settles at exactly
   `120rem - 2 * gutter` — the same width the old max-width produced, since
   that cap was on the border box and the gutter sat inside it. So every
   layout at or under 1920px is untouched, to the pixel. */
.shell {
  --shell-measure: calc(120rem - 2 * var(--gutter));
  padding-inline: max(var(--gutter), (100% - var(--shell-measure)) / 2);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  padding: var(--space-2) var(--space-3);
  background: var(--fg);
  color: var(--bg);
  z-index: 10;
}

.skip-link:focus {
  left: 0;
}

/* Available to assistive tech, absent visually. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   5. HOMEPAGE
   A single screen. Name, who/where, and an index of the site.
   -------------------------------------------------------------------------- */

/* Below the desktop breakpoint the hero is sized by its own content, not by
   the viewport. It used to be min-height: 100svh here, which on a phone left
   ~236px of empty brown between the navigation and the clients heading — the
   section was a screen tall because the screen was, not because anything was
   in it. The padding below is the intentional gap; the clients section then
   follows as the next thing on the page rather than a screen later.

   (grid-template-rows is 1fr because this was `auto 1fr` when a colophon
   occupied the first row; with that gone, a lone child would land in the auto
   row and the desktop centring would silently stop working.) */
.home {
  display: grid;
  grid-template-rows: 1fr;
  padding-block: clamp(1.5rem, 4vh, 3rem) clamp(2.75rem, 8vw, 4.5rem);
}

.home__composition {
  align-self: center;
  width: 100%;
  max-width: 76rem;
  margin-inline: auto;
}

/* Reads as a colophon: stacked on narrow screens, opposed corners on wide. */
.home__top {
  display: flex;
  flex-direction: column;
  gap: 0.1em;
}

@media (min-width: 40rem) {
  .home__top {
    flex-direction: row;
    justify-content: space-between;
    gap: var(--space-3);
  }
}

/* -- The name. One line, and the largest thing on the site by a wide margin.
      Sized in cqi against its own column, so it fills the measure exactly
      rather than being guessed at with vw and hoped for. "KRISTJAN RUUS" is
      7.49em wide at this tracking, so 12.6cqi lands at ~94% of the column
      and leaves margin for the fallback face during font swap. ----------- */
.home__name {
  container-type: inline-size;
}

.wordmark {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: 12.6cqi;
  line-height: 1;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  white-space: nowrap;
  margin: 0;
}

/* Phones only. On one line the name can reach just ~40px at 375px, which
   puts it level with the nav labels instead of above them. Wrapping to two
   lines buys back the scale (~70px, the same 3x dominance desktop has).
   The cap keeps the size continuous across the breakpoint: 76px here vs
   69px single-line at 600px, so nothing jumps. "KRISTJAN" is 4.62em wide,
   so 20.99cqi fills ~97% of the column. */
@media (max-width: 37.5rem) {
  .wordmark {
    white-space: normal;
    font-size: min(20.99cqi, 4.75rem);
    line-height: 0.88;
  }

}

.home__identity {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(2.5rem, 6vw, 4rem);
  align-items: center;
}

/* One column, two paired blocks. The gap between the pairs is what separates
   them; the gap inside each pair stays tight so a role and its explanation
   read as one unit. */
.home__role {
  margin-top: clamp(1.25rem, 2.5vw, 2rem);
  display: grid;
  justify-content: start;
  row-gap: 0.9em;
}

.role-item {
  display: grid;
  row-gap: 0.15em;
}

/* Unchanged from the single line it replaces. */
.role-head {
  color: var(--fg);
  font-weight: 600;
  font-size: clamp(0.8125rem, 0.74rem + 0.42vw, 1rem);
  letter-spacing: 0.09em;
}



/* -- Index navigation. Large, unmissable, one row per destination. ------- */
.index-nav {
  width: 100%;
}

.index-nav li {
  border-top: 1px solid var(--rule);
}

.index-nav li:last-child {
  border-bottom: 1px solid var(--rule);
}

/* The whole row is the target, so the hit area is generous on touch. */
.index-nav a {
  display: block;
  padding-block: clamp(0.6rem, 1.4vh, 0.9rem);
}

.index-nav .label {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: clamp(1.375rem, 3.7vw, 2.0625rem);
  line-height: 1.1;
  text-transform: uppercase;
  letter-spacing: -0.005em;
  transition: opacity var(--hover);
}

@media (hover: hover) {
  .index-nav a:hover .label {
    opacity: 0.62;
  }
}

/* --------------------------------------------------------------------------
   6. SELECTED CLIENTS & COLLABORATORS
   A credits page, not a logo wall. Left-aligned, unboxed, generously spaced,
   and deliberately quieter than the name above it.
   -------------------------------------------------------------------------- */

.clients {
  padding-block: var(--clients-pad-block);
}

.clients__title {
  padding-top: var(--clients-title-pad);
  border-top: 1px solid var(--rule);
  margin-bottom: var(--clients-title-mb);
}

/* Equal columns, equal rows. Every cell centres its own content, so column 1
   and column 4 sit the same distance from the content edges and the block
   reads as centred rather than left-weighted. */
.logo-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Fixed, not minmax: optical sizing pushes a few logos past the row height,
     and rows that grew would change the section height — which the hero
     height is derived from, moving the whole page. The overspill is the
     logo's own transparent padding and is far smaller than the row gap. */
  grid-auto-rows: var(--logo-row-h);
  gap: var(--logo-row-gap) clamp(1rem, 3vw, 2.5rem);
}

@media (min-width: 40rem) {
  .logo-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* Desktop is fixed at 4 x 3 — the 12 entries are chosen for that shape. */
@media (min-width: 60rem) {
  .logo-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* One identical invisible box per logo. What varies inside it is the logo's
   own size, tuned per entry by --optical: optical equality, not mathematical
   equality. A long wordmark and a compact badge should read as equally
   important, which they will not do at identical widths. */
.logo {
  /* Base height of the shared bounding box, before --optical. Capped so that
     even the tallest trim (NFL at 1.69) stays inside --logo-row-h — if a logo
     outgrew the row, the grid would stretch and the derived hero height would
     no longer land row 2 at the fold. */
  --logo-box-h: clamp(1.375rem, 2.4vw, 2.125rem);
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--logo-row-h);
  min-width: 0;
  text-align: center;
}

/* The artwork as supplied: original colours, no filter, no tint, no box.
   Each logo is fitted inside the shared bounding box — cell width by
   --logo-box-h scaled by its own --optical — and centred there. object-fit
   keeps every aspect ratio intact; nothing is cropped or stretched. */
.logo__img {
  width: 100%;
  height: calc(var(--logo-box-h) * var(--optical, 1));
  object-fit: contain;
  object-position: center;
}

/* --------------------------------------------------------------------------
   7. CINEMATIC STILL
   Full bleed: the page opens into a film frame. This section deliberately
   carries no .shell — it is a direct child of <main>, which is unconstrained,
   so it fills the viewport width on its own. That is preferred over the
   100vw + negative-margin breakout, which overshoots by the scrollbar width
   and forces a horizontal overflow the body then has to clip.
   No frame, no radius, no shadow, no overlay.
   -------------------------------------------------------------------------- */

/* No padding at all. Any space here is a band of flat colour between the end
   of the section ramp above and the image, and a flat band reads as a solid
   block no matter how dark it is — which is exactly what a gradient running
   into a photograph must not do. With this at zero the ramp finishes on the
   image's own top edge and the two meet directly; the section above already
   carries its own bottom padding, so the darkening still has room to happen.
   The bottom is zero for the same reason it was before: the image is the last
   thing on the page. */
.film-still {
  padding-block: 0;
}

.film-still__link {
  display: block;
  transition: opacity var(--hover);
}

/* No aspect-ratio box and no object-fit: the image takes its height from its
   own intrinsic 2.389:1, so the full graded frame is shown uncropped. */
.film-still__link img {
  width: 100%;
  height: auto;
}

@media (hover: hover) {
  .film-still__link:hover {
    opacity: 0.88;
  }
}

/* The phone crop that used to live here is gone: full bleed gives the frame
   the whole viewport width, so it no longer reads as a strip, and the
   complete uncropped frame is now shown at every size. */

/* --------------------------------------------------------------------------
   8. SITE FOOTER
   -------------------------------------------------------------------------- */

.site-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
  padding-top: var(--space-3);
  padding-bottom: clamp(1.75rem, 5vh, 3rem);
}

.site-foot ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.link-underline {
  transition: color var(--hover), text-decoration-color var(--hover);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.28em;
  text-decoration-color: var(--rule-strong);
}

@media (hover: hover) {
  .link-underline:hover {
    color: var(--fg);
    text-decoration-color: currentColor;
  }
}

/* -- Desktop: name on the left, index on the right. --------------------- */
@media (min-width: 60rem) {
  /* Short of a full screen, so the divider and the section heading below sit
     inside the first viewport. That reveal is the only affordance that there
     is more page — no indicator, no arrow, nothing that moves. */
  /* The hero is content-driven on desktop, not viewport-driven. Centring the
     composition in a fixed svh box meant any height taken off the hero came
     off the top as well, dragging the identity block upward with it. Anchored
     to the top instead: the air above the composition is stated explicitly
     (and matches where centring used to put it), so every pixel saved comes
     off the empty space *below* it — which is what pulls the clients rows up
     without moving the name. */
  /* Derived, not guessed. The hero takes whatever height is left once the
     clients heading and its first two rows have been accounted for, so the
     first screen ends just under row 2 and row 3 falls below the fold — at
     every viewport height, not just the one it was tuned on. A fixed svh
     value cannot do this: the section's own spacing hits its clamp ceilings
     and stays ~270-360px regardless of viewport height, so the hero share
     it implies ranges from 58svh to 73svh across common displays.
     max() guards the calc from going negative on very short windows; the
     content height wins there anyway, since this only sets a floor. */
  .home {
    min-height: max(0px, calc(100svh - var(--clients-two-rows) - 1.5rem));
    padding-bottom: clamp(1.75rem, 4.5vh, 3.5rem);
  }

  .home__composition {
    align-self: start;
    padding-top: clamp(3rem, 9vh, 7rem);
    padding-bottom: 0;
  }

  /* Name and index sit close enough to read as one composition; the
     breathing room is pushed out to the margins instead. */
  .home__identity {
    grid-template-columns: minmax(0, 1fr) clamp(13rem, 19vw, 17rem);
    gap: clamp(2rem, 3.5vw, 3.5rem);
    align-items: center;
  }

  .index-nav .label {
    font-size: clamp(1.5rem, 2.35vw, 2.1875rem);
  }
}

/* The end band with its links removed. It is kept as an element because it is
   also the page's last tonal step — on HOME and LISTEN it carries the
   gradient running from the image down to the page end, and without it the
   base mocha would reappear under the still. Height only, no content.

   Solid pages need just enough to stop the last line sitting on the page
   edge; the two gradient pages keep close to the old footer's height so the
   approved ramp still has the room it was tuned in. */
.site-foot--bare {
  display: block;
  padding-block: 0;
  min-height: clamp(2.5rem, 6vh, 3.5rem);
}

/* On the two pages that end on an image there is no band at all. The still is
   the final element and the page stops at its lower edge — anything here would
   be an empty dark strip pretending to be a footer. The editorial pages keep
   theirs, since their last line of type does need somewhere to sit.

   Class, not :has(): this one decides whether the document extends past the
   image, so it must not depend on selector support. */
body.ends-in-image .site-foot--bare {
  display: none;
}

/* --------------------------------------------------------------------------
   9. SITE HEADER — inner pages
   The homepage carries its navigation inside the hero; every other page needs
   it up top. Same language: uppercase sans, hairline, no hamburger at any
   width. Deliberately not sticky — the player is the only fixed element.
   -------------------------------------------------------------------------- */

.site-header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1) var(--space-3);
  padding-block: clamp(1.1rem, 2.8vh, 1.75rem);
  border-bottom: 1px solid var(--rule);
}

.site-header__mark {
  font-family: var(--font-display);
  font-size: clamp(1rem, 1.5vw, 1.25rem);
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.site-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(0.8rem, 1.9vw, 1.75rem);
}

.site-nav a {
  font-family: var(--font-sans);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: clamp(0.6875rem, 0.64rem + 0.22vw, 0.8125rem);
  line-height: 1.5;
  color: var(--muted);
  transition: color var(--hover);
}

/* Current section reads at full strength; everything else sits back. */
.site-nav a[aria-current="page"] {
  color: var(--fg);
}

/* On a phone the six labels want 396-400px and the shell offers 335-390, so
   CONTACT dropped to a second line on its own. Measured at 375/390/393/402/
   430, the shortfall runs 61.3px down to 9.9px.

   Most of it is bought back from the gap rather than the type: the row is
   the only thing on its line, so the words can sit closer without crowding
   anything, and 8px against a 10.4px label still reads as separate items.
   The rest is a 6% trim of the size and a tightening of the tracking, which
   is where uppercase sans of this size has room to give — the labels stay
   the same words at nearly the same weight, not a shrunken version.

   Scoped to 30rem so it never reaches a tablet or a desktop window, and the
   sizes are fixed here rather than left to the vw clamp, so the fit does not
   drift with the viewport it is being fitted to. */
@media (max-width: 30rem) {
  .site-nav ul {
    gap: 0.5rem;
  }

  .site-nav a {
    font-size: 0.65rem;
    letter-spacing: 0.06em;
  }
}

/* The 375px class needs a little more than the rest: at the settings above it
   still lands about half a pixel over. Rather than tighten every phone for
   the narrowest one, this last step applies only there. */
@media (max-width: 23.5rem) {
  .site-nav ul {
    gap: 0.4375rem;
  }

  .site-nav a {
    font-size: 0.625rem;
    letter-spacing: 0.05em;
  }
}

@media (hover: hover) {
  .site-nav a:hover,
  .site-header__mark:hover {
    color: var(--fg);
  }
}

/* --------------------------------------------------------------------------
   10. LISTEN
   A tracklist as a credits sheet: catalogue number left, title through the
   middle, descriptor and duration to the right, on one baseline.
   -------------------------------------------------------------------------- */

.film-still__img {
  width: 100%;
  height: auto;
}

.page-intro {
  padding-block: clamp(2rem, 6vh, 4rem) clamp(1.25rem, 3.5vh, 2.25rem);
}

.page-title {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: clamp(2.25rem, 6.5vw, 5rem);
  line-height: 1;
  letter-spacing: -0.01em;
  text-transform: uppercase;
}

.page-intro__note {
  margin-top: clamp(0.6rem, 1.4vh, 1rem);
}

/* With the page heading gone, this padding is the whole transition from the
   full-bleed frame into the tracklist — the hero's own padding alone left it
   reading as cramped. */
.tracks {
  padding-block: clamp(2.125rem, 4.4vh, 2.75rem) clamp(2.5rem, 7vh, 5rem);
}

/* Two bodies of work, weighted equally. The column gap is the point of the
   layout, not leftover space — it is what keeps the page reading as an
   editorial spread rather than a two-pane library. */
.collections {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  row-gap: clamp(2.75rem, 8vh, 4.5rem);
}

/* Two columns only from 1200px. Below that a pair of columns leaves the
   waveform around 60px — technically side-by-side, functionally useless —
   so tablets get one full-width column and a waveform worth having. */
@media (min-width: 80rem) {
  .collections {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: clamp(2.5rem, 6vw, 6rem);
  }
}

/* Stacked, the two collections need a vertical rhythm of their own. Side by
   side those gaps never meet, so nothing related them: the drop from the
   header rule into COMPOSER measured 36px against 65px from the last COMPOSER
   row into PRODUCER, which read as an accident. They are now the same family
   of value, the step between sections staying the larger of the two so the
   grouping still reads correctly. */
@media (max-width: 79.99rem) {
  .tracks {
    padding-top: clamp(2.75rem, 7vw, 3.5rem);
  }

  .collections {
    row-gap: clamp(3.25rem, 9vw, 4rem);
  }
}

/* Strong, but plainly subordinate to LISTEN above it. The first track's
   top rule doubles as this heading's underline — no second line needed. */
.collection__title {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: clamp(1.25rem, 2vw, 1.75rem);
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin-bottom: clamp(0.9rem, 2.2vh, 1.4rem);
}

/* Five cells on one axis: number, title, waveform, duration, control. The
   tracks are declared once on the list and inherited by every row through
   subgrid, so the columns line up down the whole collection. The waveform
   takes the flexible middle; everything else is content-width. */
.tracklist {
  display: grid;
  grid-template-columns: auto auto minmax(3rem, 1fr) auto auto;
  column-gap: clamp(0.7rem, 1.4vw, 1.25rem);
}

.track {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  border-top: 1px solid var(--rule);
  padding-block: clamp(0.7rem, 1.7vh, 1rem);
}

.track:last-child {
  border-bottom: 1px solid var(--rule);
}

/* Catalogue number: an editorial index mark, never louder than the title.
   Tabular figures so the column of 001-005 aligns exactly. */
.track__num {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: clamp(0.6875rem, 0.64rem + 0.22vw, 0.8125rem);
  letter-spacing: 0.18em;
  color: var(--muted);
  transition: color var(--hover);
}

/* Sized to hold the longest title on one line at desktop widths while still
   leaving the waveform room to be useful. */
.track__title {
  font-family: var(--font-display);
  font-size: clamp(0.9375rem, 1.25vw, 1.1875rem);
  line-height: 1.15;
  text-transform: uppercase;
  letter-spacing: 0.005em;
  white-space: nowrap;
}

.track__dur {
  white-space: nowrap;
  text-align: right;
}

/* -- Waveform ---------------------------------------------------------- */

/* Height is reserved whether or not the peak data has arrived, so the rows
   never shift when it does. touch-action keeps a horizontal scrub from being
   stolen by the page scroller on touch devices. */
.track__wave {
  height: clamp(0.9rem, 1.9vh, 1.25rem);
  min-width: 0;
  cursor: pointer;
  touch-action: none;
}

.wave {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* Unplayed: present enough to read the shape of the piece. */
.wave__base {
  fill: var(--wave-base);
}

/* Played: the same bars at full strength — the progress indicator is the
   waveform itself, not a line or a playhead laid over it. */
.wave__fill {
  fill: var(--wave-fill);
}

@media (hover: hover) {
  .track__wave:hover .wave__base {
    fill: var(--wave-hover);
  }
}

/* -- Play / pause ------------------------------------------------------ */

/* A bare glyph — no circle, no pill, no filled background. */
.track__toggle {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  width: 1.5rem;
  height: 1.5rem;
  color: var(--muted);
  transition: color var(--hover);
}

.track__toggle:focus-visible {
  outline-offset: 2px;
}

.track__icon {
  width: 0.6875rem;
  height: auto;
  fill: currentColor;
}

.track__icon--pause {
  display: none;
}

.track[data-state="playing"] .track__icon--play {
  display: none;
}

.track[data-state="playing"] .track__icon--pause {
  display: block;
}

/* Active row: colour only, the row is not repainted. */
.track[data-state="playing"] .track__toggle,
.track[data-state="paused"] .track__toggle,
.track[data-state="playing"] .track__num,
.track[data-state="paused"] .track__num {
  color: var(--fg);
}

.track[data-state="error"] {
  opacity: 0.5;
}

@media (hover: hover) {
  .track__toggle:hover,
  .track:hover .track__num {
    color: var(--fg);
  }
}

/* --------------------------------------------------------------------------
   11. MUSIC FILMS
   A triptych of three identical 16:9 frames. No cards, no panels, no radius —
   the stills provide the structure.
   -------------------------------------------------------------------------- */

/* Now sits below the films. Same small scale as the 001 / ORCHESTRAL TOOLS
   metadata on each entry, so it reads as context rather than a statement. */
.films-intro {
  padding-block: clamp(1rem, 2.4vh, 1.75rem) clamp(2rem, 5vh, 3.5rem);
}

/* The block is centred on the page; the text inside is not. */
/* One statement under the three films, centred on the page and centre-aligned
   within itself. The measure is deliberately short of the film row above: the
   films are the work and this is a caption to them, so the space either side
   is part of the composition rather than something to fill. */
.films-intro__body {
  max-width: 40rem;
  margin-inline: auto;
  text-align: center;
  font-family: var(--font-sans);
  font-weight: 500;
  /* Sentence case, so the wide tracking that suited caps is dialled back to
     near-normal — 0.1em on lowercase reads as loose rather than deliberate. */
  letter-spacing: 0.01em;
  /* The same body size the ABOUT biography is set in. */
  font-size: clamp(0.9375rem, 0.9rem + 0.18vw, 1.0625rem);
  line-height: 1.7;
  color: var(--fg);
}

/* Top padding matches .tracks on LISTEN, so the first line of film metadata
   sits the same distance below the header rule as COMPOSER / PRODUCER do. */
.films {
  padding-block: clamp(2.125rem, 4.4vh, 2.75rem) clamp(2.5rem, 7vh, 5rem);
}

.film-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(2.25rem, 5vh, 3.5rem) clamp(1.25rem, 2.5vw, 2.25rem);
}

/* Three equal columns. Equal by construction: 1fr each, one shared gutter. */
@media (min-width: 56rem) {
  .film-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.film__meta {
  display: grid;
  gap: 0.15em;
  margin-bottom: clamp(0.6rem, 1.4vh, 0.9rem);
}

.film__title {
  font-family: var(--font-display);
  font-size: clamp(1.0625rem, 1.5vw, 1.375rem);
  line-height: 1.15;
  text-transform: uppercase;
  letter-spacing: 0.005em;
}

.film__num {
  letter-spacing: 0.18em;
}

/* The fixed frame. Every film gets exactly this box whatever its native
   ratio, and whether it is showing a still or a player. */
.film__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: rgba(232, 228, 219, 0.05);
}

.film__open {
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
}

/* Cropping happens here, in CSS. The source files are untouched. */
.film__still {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transition: opacity var(--hover);
}

/* Inside the frame the player fills the box; YouTube letterboxes or
   pillarboxes the film itself so its composition is never cropped. */
.film__player {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* The only thing laid over the still: a small glyph, no circle, no gradient. */
.film__glyph {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

/* The stills range from a dark studio to a bright sky, and a cream glyph
   vanishes against the light ones. This is the smallest fix that works on
   both: a shade on the mark itself — not a gradient or scrim over the image. */
.film__glyph svg {
  width: clamp(0.75rem, 1.1vw, 1rem);
  height: auto;
  fill: var(--fg);
  opacity: 0.82;
  filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.55));
  transition: opacity var(--hover);
}

@media (hover: hover) {
  .film__open:hover .film__glyph svg {
    opacity: 1;
  }
  .film__open:hover .film__still {
    opacity: 0.86;
  }
}

.film__open:focus-visible {
  outline-offset: -3px;
}

/* --------------------------------------------------------------------------
   12. CREDITS
   A credit index, not a CV: year, work and role distributed across the page
   on one baseline, separated by hairlines. No cards, no logos, no thumbnails.
   -------------------------------------------------------------------------- */

/* The bottom padding was sized for a page that ended in a footer full of
   links. With the last Producer credit as the final element it only needs
   enough to keep that credit off the page edge — 126px of nothing followed it
   otherwise, which read as a large empty scroll area. */
.credits {
  padding-block: clamp(2.25rem, 5.5vh, 4rem) clamp(1.25rem, 3vh, 1.75rem);
}

/* Same reasoning as ABOUT: the padding above is the breathing room, so the
   band would only be more height after the final credit. */
body:has(.credits) .site-foot--bare {
  display: none;
}

/* The credits sit in a controlled column rather than running the full width
   of a large monitor: past about this width the eye has to travel too far
   from a title on the left to its role on the right for the two to read as
   one entry. */
.credits-section {
  max-width: 84rem;
  margin-inline: auto;
}

/* Section identifiers. Stronger than the metadata — full cream, heavier,
   wider tracked — but still sans and still small, so they organise the page
   without competing with the credits themselves.

   This rule is now the only one on the page: the entries below it used to
   carry a hairline each, which made the section read as a table. The heading
   and its rule group them instead, and whitespace separates them. */
.credits-section__title {
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: clamp(0.8125rem, 0.76rem + 0.25vw, 0.9375rem);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg);
  padding-bottom: clamp(0.7rem, 1.8vh, 1.1rem);
  border-bottom: 1px solid var(--rule);
  margin-bottom: clamp(1.1rem, 2.6vh, 1.6rem);
}

/* A clear step between the two identities, without an empty field. */
.credits-section + .credits-section {
  margin-top: clamp(2.25rem, 5.5vh, 3.5rem);
}

/* Education sits well clear of the credits, and reads smaller than them. */
.education {
  margin-top: clamp(2.25rem, 5.5vh, 3.5rem);
}

.credit-list--minor .credit__title {
  font-size: clamp(0.9375rem, 1.15vw, 1.0625rem);
}

/* Deliberately quiet: the section number and label are wayfinding, the work
   itself is the loud part. */
.credit-group__title {
  display: flex;
  align-items: baseline;
  gap: clamp(0.7rem, 1.6vw, 1.1rem);
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: clamp(0.6875rem, 0.64rem + 0.22vw, 0.8125rem);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: clamp(0.75rem, 2vh, 1.15rem);
}

.credit-group__num {
  letter-spacing: 0.18em;
}

/* Columns declared once and inherited, so year / work / role hold the same
   axis down every group. */
/* Two columns: the work, and what he did on it. No year column — the list
   is ordered by importance, not chronology, and a date rail made it read as
   a timeline. Years live in the supporting line where they matter.

   row-gap is what separates one entry from the next now that the hairlines
   are gone — enough to group each title with its own role, not so much that
   the section loosens into a list of unrelated blocks. */
.credit-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: clamp(1.5rem, 4vw, 4rem);
  row-gap: clamp(1.125rem, 2.2vh, 1.5rem);
}

/* Dense on purpose: the list is meant to be scanned, so as many entries as
   possible should sit in one screen without shrinking the type.

   align-items: center, not baseline. On a baseline the role locks to the
   title's first line, which leaves it sitting high on every row that carries
   a supporting line. Centring aligns it to the middle of the whole row —
   one rule, no per-row offsets, and single-line rows are unaffected. */
.credit {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
}

/* Entries in an index, not headings: small enough that COMPOSER and
   PRODUCER carry the hierarchy and no single credit shouts. */
.credit__title {
  font-family: var(--font-display);
  font-size: clamp(0.9375rem, 1.2vw, 1.125rem);
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.005em;
  font-weight: var(--display-weight);
}

.credit__note {
  margin-top: 0.25em;
  max-width: 52ch;
  text-transform: none;
  letter-spacing: 0.02em;
  line-height: 1.5;
}

/* A slightly wider measure on CREDITS only. At 52ch the Venice line broke
   mid-phrase ("Jury Prize at / Marrakech"); 62ch lands the break after
   Marrakech, so the award and its festival stay on one line. Measured, not
   guessed — and no hard <br>, so it still reflows on narrow screens.
   Scoped to this page: ABOUT's Education notes keep the 52ch measure. */
.credits .credit__note {
  max-width: 62ch;
}

/* A run of names, not prose, so it takes a wider measure than the 52ch used
   for sentences — but a bounded one. Unbounded it ran the full 997px of the
   column while every other supporting line sat at 470px, which read as a
   different typographic system rather than a longer entry. 74ch wraps it to
   about three lines and keeps it recognisably part of the same family.
   Compound selector so it beats the rule above rather than relying on
   source order. */
.credit__note.credit__note--wide {
  max-width: 74ch;
}

/* One phrase held together, on the desktop layout only.

   The Music Meets Film line names two things separated by a slash, and the
   second one begins with "Tallinn". Left to itself the measure turned the
   line after that word, so "Tallinn" sat at the end of the first line and its
   own festival continued on the next. Keeping the name unbreakable moves the
   whole of it down instead, and the line turns after the slash where the
   sense already divides.

   Not a hard break: the phrase still wraps normally below 48rem, where the
   column is far too narrow to hold it and the stacked layout wants to reflow
   freely. Scoped to this one span, so no other entry is affected. */
@media (min-width: 48rem) {
  .credit__keep {
    white-space: nowrap;
  }
}

.credit__role {
  text-align: right;
  white-space: nowrap;
}

@media (max-width: 47.99rem) {
  /* Stacked, but the role stays tied to its title rather than drifting. */
  .credit-list {
    display: block;
  }

  .credit {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    row-gap: 0.3rem;
  }

  /* The list's row-gap does nothing once it is display: block, so every entry
     sat flush against the next one and the three stacked lines of one credit
     ran into the title of the following one. This is what separates them now,
     and it has to be larger than the 0.3rem inside an entry or the grouping
     reads backwards. */
  .credit + .credit {
    margin-top: clamp(1.5rem, 4.5vw, 2rem);
  }

  /* A little more air inside an entry too: at this width the three parts are
     stacked, so title, supporting line and role need to read as one unit
     without touching. */
  .credit__note {
    margin-top: 0.45em;
    max-width: none;
  }

  .credit__role {
    /* Stacked, the role reads better under its title than pushed to the edge. */
    text-align: left;
    white-space: normal;
    margin-top: 0.15em;
  }

  /* And the section headings need to sit clear of the group above them. */
  .credits-section + .credits-section {
    margin-top: clamp(2.5rem, 8vw, 3.5rem);
  }

  .education {
    margin-top: clamp(2rem, 6vw, 2.75rem);
  }
}

/* --------------------------------------------------------------------------
   13. ABOUT
   Asymmetric: a portrait against a narrower column of reading text.
   -------------------------------------------------------------------------- */

/* The bottom padding is a floor under the logo strip, not a band after it.

   body is a grid whose middle row is 1fr, so main stretches to fill a short
   page: where the content fits there is already slack below the strip — 105px
   at 1512x900 — and it is mocha, because main carries --bg. This padding is
   smaller than that slack, so on a desktop that fits it changes nothing at
   all: main still stretches to the same place and the strip does not move.

   It only does work where there is no slack to inherit. Once the page
   overflows, the strip is the last element in the document and the gap below
   it was exactly 0 — the row sat on the page edge on every phone. Padding
   rather than a margin, so the space is part of main and stays mocha; a
   margin would end the mocha early and let the dark canvas through as a false
   footer.

   Narrow layouts always scroll, so there the gap is real space, sized against
   the page's own outer spacing. */
.about {
  padding-block: clamp(2.25rem, 5.5vh, 4rem)
    max(clamp(2rem, 4.5vh, 2.75rem), env(safe-area-inset-bottom, 0px) + 1rem);
}

/* The home indicator sits over the last of the page on iOS, and with
   viewport-fit=cover it is inside the layout viewport. Folded into the same
   max() above rather than added to it: the gap is whichever is larger, so the
   spacing clears the indicator without stacking into a deep empty tail. */

/* Desktop takes no bottom padding at all: the 1fr row already leaves slack
   below the strip there — 105px at 1512x900 — and it is already mocha. Adding
   any floor on top of it is inert at most heights but not all: a measured
   sweep found 1440x740 and 1512x780 fitting with less than one floor to
   spare, so a floor would have tipped exactly those into scrolling by 6px and
   1px. That is the phantom scrollbar again, in a new disguise.

   The floor is therefore given only to windows short enough to be scrolling
   already. ABOUT's content runs 739-830px tall across desktop widths, so at
   45rem (720px) and under the page overflows at every one of them, whatever
   this padding does — the same sweep measured 1440x720 already over by 6px
   and 1512x720 by 31px. Inside that band the padding can only add to a
   scrollbar that exists, never create one, and it stops a vertically resized
   window from ending flush on the logo row. */
@media (min-width: 60rem) {
  .about {
    padding-block-end: 0;
  }
}

@media (min-width: 60rem) and (max-height: 45rem) {
  .about {
    padding-block-end: clamp(0.875rem, 2.2vh, 1.5rem);
  }
}

/* The page ends on the logo strip, so the band adds nothing but height — and
   on a shorter desktop screen that height was the whole reason the page
   scrolled while its content still fitted. The padding above is the breathing
   room; this is not needed as well. */
body:has(.about) .site-foot--bare {
  display: none;
}

.about__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.75rem, 4.5vh, 3rem);
}

@media (min-width: 56rem) {
  .about__grid {
    grid-template-columns: minmax(0, 0.78fr) minmax(0, 1.22fr);
    gap: clamp(2rem, 5vw, 4.5rem);
    align-items: start;
  }
}

/* The frame carries the supplied file's own shape — 1200x1200, so 1:1 — and
   the picture therefore shows whole: nothing cut from the cap, face or
   shoulders, no headroom removed, the grade untouched. The file itself is
   never modified; this is display only.

   It still occupies the same slot as before: same column, same left edge,
   same top line as the biography, filling the column's full width. Only the
   height differs, and the right-hand column is taller than it either way, so
   the page does not grow.

   If the portrait is replaced again, set this to the new file's own ratio —
   any other value crops it. object-fit stays as a guard: a mismatched file
   crops rather than distorts. */
.about__portrait {
  aspect-ratio: 1 / 1;
  overflow: hidden;
}

.about__portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

.about__bio {
  display: grid;
  gap: clamp(1.5rem, 4vh, 2.5rem);
}

/* Long-form reading size, scoped to the biography only: .prose is shared
   with the MUSIC FILMS intro, which is two short paragraphs and stays at the
   larger statement size. ~17px on desktop against the previous ~21px. */
.about__bio .prose {
  font-size: clamp(0.9375rem, 0.9rem + 0.18vw, 1.0625rem);
  line-height: 1.6;
  /* No measure of its own. The Education list below fills this column edge to
     edge, so a narrower cap on the biography left the two with different right
     boundaries and tipped the whole right-hand side leftward. The column is
     what sets the measure now, and both blocks share one outer edge. */
  max-width: none;
}

.about__bio .prose p + p {
  margin-top: 0.9em;
}

/* EDUCATION now sits inside .about__bio, so the column's own grid gap is the
   only thing setting the distance from the location line — the standalone
   margin it carried as a full-width section would stack on top of that and
   open a hole between the two halves of the column. */
.about__bio .education {
  margin-top: 0;
}

/* Narrower column than CREDITS, so the rail between an entry and its
   right-hand metadata closes up. At 4vw it read as two detached columns
   rather than one compact row. Rows, rules and centring are untouched. */
.about__bio .credit-list {
  column-gap: clamp(1rem, 2vw, 1.75rem);
}

/* A quiet close to the page: one hairline sets the section off, and the label
   is the same small metadata used everywhere else, so the strip reads as a
   footnote to the biography rather than a second homepage. */
.about-clients {
  margin-top: clamp(2.25rem, 5.5vh, 3.5rem);
}

.about-clients__title {
  padding-top: clamp(0.9rem, 2.2vh, 1.35rem);
  border-top: 1px solid var(--rule);
  margin-bottom: clamp(1.25rem, 3vh, 2rem);
}

/* Reuses .logo-grid and .logo untouched and overrides only the two size
   variables, locally. --logo-row-h in particular is a homepage token that the
   derived hero height is calculated from, so it must never be changed
   globally — scoping it here leaves HOME exactly where it is.

   Three across on phone and tablet (3 x 2), six across from the width where
   they stop crowding each other. */
.logo-strip {
  --logo-row-h: clamp(1.75rem, 3.2vw, 2.5rem);
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.25rem, 3.5vh, 2rem) clamp(1.25rem, 3vw, 2.5rem);
}

.logo-strip .logo {
  --logo-box-h: clamp(0.875rem, 1.5vw, 1.3125rem);

  /* Optical sizing scales each logo to its own weight, so several are taller
     than the row: ESPN+ is five times the base box, and its frame measures
     105px inside a 40px cell — but only 17.3px of that is ink. The rest is
     transparent PNG margin, 44px of nothing hanging off each side. On HOME it
     simply falls between rows and costs nothing.

     Here the strip is the last thing on the page, so that overhang was real
     height: the strip carried 34px of padding purely to contain it, and ABOUT
     stood 34px taller than the content it actually shows. Those were the
     pixels tipping a page that fits into a scrollbar.

     Clipping at the cell reclaims them. Every logo's ink is centred and well
     inside its box — the tightest is NFL, whose 39.8px shield leaves 1.86px
     clear, which the clip margin covers. Cell heights, positions and optical
     sizing are untouched; only the empty margin stops occupying space. */
  overflow: clip;
  overflow-clip-margin: 2px;
}

@media (min-width: 60rem) {
  .logo-strip {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
}

/* --------------------------------------------------------------------------
   14. CONTACT
   One block, set on the same left margin as every other page, with the space
   around it doing the work. No page title, no form, no centring: the only
   thing here is the address, so it is left to carry the page on its own.
   -------------------------------------------------------------------------- */

/* This page holds three short lines, so it is shorter than the screen and the
   footer would otherwise sit stranded in the middle of it. Scoped with
   :has(.contact) — the same technique the gradients use — the body becomes a
   column just here, and CONTACT takes the slack. The footer lands at the
   bottom of the viewport, the negative space collects below the address
   rather than around it, and no other page is touched. */
body:has(.contact) {
  display: grid;
  /* header, content, footer. The skip link, the player bar and the live region
     are all positioned, so none of them is a grid item and the three rows
     always match. The same three-row shape the generic rule above uses, so
     CONTACT and the other editorial pages resolve their height the same way;
     the middle row is what CONTACT centres its group inside. */
  grid-template-rows: auto 1fr auto;
  /* One explicit full-width track, so every band spans the viewport and the
     mocha surface reaches both edges. .shell no longer constrains its own box
     — it is full width and centres its content with padding — so the track
     and the width rule above are what the bands stretch into. The minmax(0, …)
     floor keeps a wide child from pushing the track past the viewport instead
     of wrapping inside it. */
  grid-template-columns: minmax(0, 1fr);
  /* dvh, matching the generic rule above: at 100svh the surface falls short
     the moment Safari's address bar collapses and the viewport grows. */
  min-height: 100dvh;
}

.contact {
  /* The group sits in the middle of what is left of the screen rather than at
     the top of it. align-content does the centring inside the grid's free row,
     and the heavier bottom padding lifts it a little above true centre — dead
     centre reads as low once the navigation is above it.

     This also handles the player: its clearance is body padding, so the free
     row shrinks by exactly that much when the bar appears and the group rises
     with it. The shift is around half the bar's height, not a jump. */
  /* Mathematically centred, not optically nudged: the padding is equal top and
     bottom, so align-content puts the group's centre exactly on the centre of
     this row. The row is everything between the header and the foot of the
     viewport, so the group centres in the usable area below the header at any
     screen size — including a phone, where the navigation wraps to two lines
     and the row shrinks accordingly. No breakpoint needed. */
  display: grid;
  align-content: center;
  padding-block: clamp(2rem, 5vh, 3.5rem);
}

/* Nothing may sit below the group, or the row it centres in stops being the
   area below the header and the centring is off by half the band. */
body:has(.contact) .site-foot--bare {
  display: none;
}

/* One symmetrical composition: the block is centred on the page and every
   line is centred within it, so the four tiers share a single axis. The
   measure is wide enough for the address at its largest and no wider. */
.contact__block {
  max-width: 34rem;
  margin-inline: auto;
  text-align: center;
}

/* Stays small and muted — it introduces the address rather than competing
   with it. The gap holds the two together as one pair. */
.contact__label {
  margin-bottom: clamp(0.9rem, 2.4vh, 1.4rem);
}

/* The focal point of the page. Scaled to carry the same weight as the large
   HOME navigation labels — those are bold uppercase sans at 22-33px, and a
   lowercase serif has to run larger to hold the same presence, so this lands
   at 27px on a phone and 38px on a desktop. Still Pagella, still lowercase:
   the address is read, not shouted, and it belongs to the editorial serif
   identity rather than to the navigation. */
.contact__mail {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: clamp(1.6875rem, 1.35rem + 1.7vw, 2.375rem);
  line-height: 1.2;
  /* Long enough to need a sensible break point on a narrow phone, and this is
     the only place it should ever take one. */
  overflow-wrap: anywhere;
}

/* Secondary and muted, on one line where there is room. No rule above it —
   there are only three lines on this page and none of them needs dividing. */
/* Second tier: the profiles, on one line, in the same uppercase link
   treatment the site already uses. Text, never icons. They wrap only if the
   screen is too narrow to hold them side by side. */
.contact__social {
  display: flex;
  flex-wrap: wrap;
  /* A flex row ignores text-align, so it is centred on its own axis. */
  justify-content: center;
  gap: 0.5em clamp(1.25rem, 3vw, 2.25rem);
  margin-top: clamp(2rem, 5.5vh, 3.25rem);
}

/* Third tier, and the quietest: where he is, and that it does not limit him.
   Two lines rather than one run-on, both centred on the same axis.

   Deliberately NOT dimmed further: --muted is 5.5:1 on this background, and
   any opacity below about 0.9 drops 13px text under the 4.5:1 floor. The
   second line is already secondary by position, and making a statement of
   availability hard to read would defeat the point of making it. */
.contact__place {
  margin-top: clamp(1.5rem, 4vh, 2.5rem);
}

.contact__place span {
  display: block;
}

.contact__place span + span {
  margin-top: 0.45em;
}

/* --------------------------------------------------------------------------
   15. STICKY PLAYER
   Hidden until the first play. Part of the page furniture, not a widget:
   same background, same hairline, same type. No artwork, no waveform.
   -------------------------------------------------------------------------- */

.player {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 20;
  background-color: var(--player-bg);
  border-top: 1px solid var(--rule);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  /* Slow enough that the tone change is never a visible event, quick enough
     that the bar has caught up by the time a scroll settles. Nothing else
     about the bar animates. */
  transition: background-color 320ms linear;
}

.player[hidden] {
  display: none;
}

.player__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr) auto auto;
  grid-template-areas: "id controls time close";
  align-items: center;
  gap: clamp(0.9rem, 2.5vw, 2.5rem);
  padding-block: clamp(0.65rem, 1.5vh, 0.95rem);
}

.player__id {
  grid-area: id;
  display: flex;
  /* Centred, not baseline-aligned. A shared baseline reads well in running
     text, but here the three items sit in a control strip whose every other
     part — toggle, waveform, time, close — is centred on one axis, and the
     serif title is half again the size of the sans beside it. On a baseline
     the title's cap block rode above that axis. Centred, the smaller sans
     lands within 0.1px of its own optical centre, because its ink sits
     symmetrically inside its line box. */
  align-items: center;
  gap: clamp(0.6rem, 1.4vw, 1.1rem);
  min-width: 0;
}

.player__collection,
.player__num {
  letter-spacing: 0.18em;
  flex: none;
}

/* Kept down to ~352px, since both collections number 001-005 and the label
   is what disambiguates them. Dropped only below that, where the title
   would otherwise be truncated to a few characters. */
@media (max-width: 22rem) {
  .player__collection {
    display: none;
  }
}

.player__title {
  font-family: var(--font-display);
  font-size: clamp(0.95rem, 1.3vw, 1.125rem);
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;

  /* The last step of centring the title, and only that.

     `align-items: center` on the row centres each line box. For the sans that
     is enough — its ink sits symmetrically inside its box, 0.10px out. Pagella
     does not: its uppercase ink sits 1.73px ABOVE the middle of its own line
     box, because the box is built from the font's ascent and descent and the
     caps do not sit centred between them. Centring the box therefore still
     leaves the visible letters high, and no line-height can fix it — leading
     is added equally top and bottom, so it moves the box, never the ink
     within it.

     This puts the ink where the box already is. Relative positioning, so it
     shifts painted glyphs after layout: row height, every sibling's position
     and the bar's geometry are untouched. In em, so the same correction holds
     at any size the bar uses.

     Measured, not guessed: with this applied the title's cap-block midpoint
     and the sans midpoint land on the same axis, which is what puts the
     serif's top slightly above the sans and its bottom slightly below. */
  position: relative;
  top: 0.114em;
}

.player__controls {
  grid-area: controls;
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 2vw, 1.5rem);
}

/* A glyph, not the word "Pause": the same two icons and the same weight the
   track rows use, so the bar reads as part of the list. No background, no box,
   no label — the padding is the touch target. */
.player__toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  color: var(--fg);
  padding: 0.5rem;
  margin-left: -0.5rem;
  transition: opacity var(--hover);
}

.player__icon {
  width: 0.6875rem;
  height: auto;
  fill: currentColor;
}

.player__icon--pause {
  display: none;
}

.player__toggle[data-playing] .player__icon--play {
  display: none;
}

.player__toggle[data-playing] .player__icon--pause {
  display: block;
}

@media (hover: hover) {
  .player__toggle:hover {
    opacity: 0.65;
  }
}

/* The active track's waveform, with a transparent native range on top. The
   input keeps every interaction the platform already implements — pointer,
   touch, drag and arrow keys — while the SVG underneath does the drawing. */
.player__wave {
  position: relative;
  flex: 1;
  min-width: 0;
  height: clamp(1.125rem, 2.2vh, 1.5rem);
  /* The strip owns the gesture once a finger lands on it, so a horizontal
     scrub is not stolen by the page trying to scroll underneath. */
  touch-action: none;
}

.player__wave-art {
  position: absolute;
  inset: 0;
}

/* Empty until a track loads: a hairline standing in for the waveform. */
.player__wave-art:empty {
  background: linear-gradient(var(--rule-strong), var(--rule-strong)) center /
    100% 1px no-repeat;
}

.player__seek {
  -webkit-appearance: none;
  appearance: none;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  background: none;
  opacity: 0; /* invisible, but still focusable and hit-testable */
  cursor: pointer;
}

.player__seek:disabled {
  cursor: default;
}

/* The input is transparent, so the focus ring is drawn on the wrapper. */
/* On a phone the row waveforms are display-only. They stay exactly as they
   look, but a finger landing on one while scrolling the list must not seek or
   start a track — the strips are thin, the rows are dense, and the gesture is
   almost always a scroll. pointer-events: none lets the touch pass straight
   through to the page, so scrolling is unaffected and no listener can fire.

   Deliberately narrow: only the row strips, and only where the primary input
   is touch with no hover. The play button is a sibling and keeps working, the
   waveform in the fixed player stays fully seekable — which is where seeking
   is done on a phone — and every pointer-capable device is untouched. */
@media (hover: none) and (pointer: coarse) {
  .track__wave {
    pointer-events: none;
  }
}

.player__wave:focus-within {
  outline: 1px solid var(--fg);
  outline-offset: 4px;
}

.player__seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 8px;
  height: 100%;
  border: 0;
}

.player__seek::-moz-range-thumb {
  width: 8px;
  height: 100%;
  border: 0;
  border-radius: 0;
}

.player__time {
  grid-area: time;
  display: flex;
  gap: 0.4em;
  white-space: nowrap;
}

/* Ends the session. Deliberately the quietest thing in the bar: a hairline
   glyph in the same muted cream as the metadata, no background, no border. The
   padding is the touch target — the mark itself stays 10px. */
.player__close {
  grid-area: close;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.75rem;
  margin-right: -0.75rem;
  color: var(--muted);
  transition: color var(--hover);
}

.player__close svg {
  width: 10px;
  height: 10px;
  stroke: currentColor;
  stroke-width: 1.25;
  fill: none;
}

@media (hover: hover) {
  .player__close:hover {
    color: var(--fg);
  }
}

.player__slash {
  opacity: 0.5;
}

/* Keep the footer and last track clear of the fixed bar. Applied only once
   the player exists, so the page is not padded for nothing. It stays on for
   the rest of the visit, across pages, because the bar does.

   The clearance is body padding, and the body's own ground is mocha — which
   on HOME and LISTEN put a strip of base mocha between the dark footer and
   the bar. So the strip is painted separately, in whatever tone that page
   actually ends on: --page-end-bg is mocha on the editorial pages and the
   footer's final espresso on the two gradient pages, so the clearance always
   belongs to the last section rather than exposing a global background.
   The band is sized from the same variable as the padding, so the two cannot
   drift apart. The player itself stays position: fixed and adds no height. */
/* :not(.ends-in-image), not a :has() override afterwards. The clearance is the
   one thing that can add document height below the still, so the rule that
   withholds it must be part of the same selector rather than a correction that
   depends on selector support — if :has() failed to resolve, these two pages
   gained ~88px of mocha that could be scrolled into below the image.

   The bar is allowed to sit over the still's lower edge instead. Clearance
   exists to keep type and controls out from under it; the image carries
   neither, so reserving space would add an empty strip and extra scroll to buy
   nothing. HOME and LISTEN are therefore exactly as tall with the player up as
   without it. */
body.has-player:not(.ends-in-image) {
  padding-bottom: var(--player-clear);
  background-image: linear-gradient(var(--page-end-bg), var(--page-end-bg));
  background-repeat: no-repeat;
  background-position: left bottom;
  background-size: 100% var(--player-clear);
}

/* -- Player tones --------------------------------------------------------
   The bar sits over whatever the page has at the bottom of the viewport, so
   its ground follows that region instead of staying one fixed brown. media.js
   picks a name; these turn the name into a colour.

   Every value below is already in this file — they are the stops of the two
   page gradients further down. No new palette, and no page gains a gradient
   it did not have: on CREDITS, ABOUT and MUSIC FILMS all six names resolve to
   --bg, so the bar is plain mocha there at every scroll position.

   Only the ground moves. Track text, numbering, controls, waveform, time and
   metadata stay in the locked cream system. */
:root {
  --player-mocha: var(--bg);
  --player-dark: var(--bg);
  --player-deep: var(--bg);
  --player-deeper: var(--bg);
  --player-image: var(--bg);
  --player-foot: var(--bg);
  --player-bg: var(--player-mocha);
}

@media (max-width: 47.99rem) {
  /* Stacked, the close button keeps the top row with the identity and time so
     the waveform below still gets the full width. */
  .player__inner {
    grid-template-columns: minmax(0, 1fr) auto auto;
    grid-template-areas:
      "id time close"
      "controls controls controls";
    gap: 0.5rem 1rem;
  }

  body.has-player {
    --player-clear: 5.5rem;
  }

  /* Recomposed, not squeezed: number and title take the first line, the
     waveform and its controls the second. All five elements survive. */
  .tracklist,
  .track {
    display: block;
  }

  .track {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas:
      "num   title title  title"
      "wave  wave  dur    toggle";
    column-gap: 0.75rem;
    row-gap: 0.5rem;
    align-items: center;
  }

  /* Desktop aligns this column through the list's subgrid, so every number
     shares one width and every title starts on one line. Here each row is its
     own grid — it has to be, because the two-line areas above are per row —
     so the column is sized by that row's own number.

     That should still match: the numbers are always three digits and carry
     tabular figures. The subset font does not actually ship the tnum feature,
     though, so "1" stays narrow and the column measures 26.72px on 001 against
     28.75px on 004. Ten titles then start on five different left edges,
     2.03px apart, which is visible on the strongest vertical in the layout.

     A floor of three zero-widths plus the tracking that follows each of the
     three digits is exactly what "000" occupies, so the column lands on the
     width it already has for every number except the narrow ones, which come
     up to meet it. Stated in the number's own em/ch, so it follows the
     clamped font size instead of pinning a pixel value. */
  .track__num {
    grid-area: num;
    min-width: calc(3ch + 0.54em);
  }
  .track__title {
    grid-area: title;
    white-space: normal;
  }
  .track__wave {
    grid-area: wave;
  }
  .track__dur {
    grid-area: dur;
  }
  .track__toggle {
    grid-area: toggle;
  }
}

/* ==========================================================================
   HOMEPAGE BACKGROUND GRADIENT TEST

   EXPERIMENT — self-contained. Delete this whole block to revert; nothing
   outside it was touched, and no markup was changed.

   Scoped with :has(.home) — only the homepage has that element — so no body
   class is needed and every other page is untouched. Browsers without :has()
   get the page exactly as it was.

   Measured anchors, sampled from the still itself:
     top edge     #090A0A   L 9.8
     bottom edge  #0E0C0B   L 12.4
   The deep tones below are set just warm of those lumas, so the handoff
   reads as tonal rather than as the page turning cold at its darkest point.

   The still is never touched: no overlay, filter, opacity or blend. The
   background adapts to the image, not the reverse.
   ========================================================================== */

/* The bar's tones on this page: the stops of the .clients ramp below, then
   the still's own tone, then the foot of the page. */
body:has(.home) {
  --player-dark: #2b1b13;
  --player-deep: #271811;
  --player-deeper: #17100c;
  --player-image: var(--bg-deep);
  --player-foot: var(--bg-foot);

  /* The bar's clearance continues the footer's final tone instead of
     dropping back to mocha underneath it. */
  --page-end-bg: var(--bg-foot);

  /* Meets the still's top edge. */
  --bg-deep: #0f0b09;
  /* Meets the still's bottom edge — warmer and a touch lighter than the top,
     which is why it is a separate value rather than reusing --bg-deep. */
  --bg-under: #110c09;
  /* Foot of the page. Dark, but still brown — never black. */
  --bg-foot: #0a0705;
}

/* The ramp is confined to the clients section: it starts at that section's
   top edge and ends exactly at its bottom, which is the top of the still.
   Keeping it inside this one element is what leaves the hero a single solid
   field — the earlier version reached up into the hero and darkened it. */
body:has(.home) .clients {
  position: relative;
}

/* The ramp is a positioned descendant now, so it would otherwise paint over
   the heading and the logos. Lifting the section's own children above it puts
   the order back. */
body:has(.home) .clients > *,
body:has(.tracks) .tracks > * {
  position: relative;
  z-index: 1;
}

/* z-index 0, not -1. At -1 the ramp is painted before in-flow block
   backgrounds, which puts body's opaque mocha on top of it the moment the root
   element is given a background of its own. At 0 it paints as a positioned
   descendant, above body — and the section's children are lifted to z-index 1
   below so the logos still sit over it. */
body:has(.home) .clients::before {
  content: "";
  position: absolute;
  inset: 0;
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  z-index: 0;
  pointer-events: none;
  /* Paced against the three logo rows, which begin at 32%, 53% and 74% of
     this section. Ten stops rather than six: the eye can pick out the moment
     a gradient "starts" if the steps are far apart, and the point here is that
     it should never be identifiable as an effect at all.

       0-20%    heading            imperceptible
       32%      row 1              only slightly darker than mocha
       53%      row 2              gradually darker
       74%      row 3              noticeably dark espresso
       100%     section bottom     meets the image's own top edge

     Every value keeps the mocha's warm ratio (R > G > B) as it descends, so
     the page darkens without ever cooling toward grey, and the darkest stop
     is still brown rather than black. */
  background: linear-gradient(
    to bottom,
    var(--bg) 0%,
    #2b1b13 20%,
    #291911 32%,
    #251710 42%,
    #20130d 53%,
    #1b100b 63%,
    #160d09 74%,
    #110a07 85%,
    #0e0906 94%,
    #0c0806 100%
  );
}

/* The still's own padding bands. Top meets the image's upper edge, bottom
   meets its lower edge; the middle of this gradient sits behind the image
   and is never seen. This is what stops the page snapping back to mocha
   underneath the still. */
body:has(.home) .film-still {
  background-image: linear-gradient(
    to bottom,
    var(--bg-deep) 0%,
    var(--bg-deep) 30%,
    var(--bg-under) 100%
  );
}

/* Carries on from the still's lower edge and settles deeper toward the page
   edge. Foreground here is untouched — only the ground behind it moves. */
body:has(.home) .site-foot {
  background-image: linear-gradient(
    to bottom,
    var(--bg-under) 0%,
    #0d0907 55%,
    var(--bg-foot) 100%
  );
}

/* ==========================================================================
   LISTEN BACKGROUND GRADIENT

   Same tonal philosophy as the homepage block above, anchored to this page's
   own image. Self-contained: delete to revert.

   Scoped with :has(.tracks) — only LISTEN has that element.

   Measured from screen2 itself:
     top edge     #0B0C0A   L 11.6
     bottom edge  #070909   L  8.6
   Its lower edge is darker than the homepage still's, which is why the
   under/foot values here are their own rather than shared.

   The image is never touched: no overlay, filter, opacity or blend.
   ========================================================================== */

body:has(.tracks) {
  --bg-deep: #0f0b09;
  --bg-under: #0c0806;
  --bg-foot: #080503;

  /* As on the homepage: the stops of this page's own ramp. */
  --player-dark: #2b1b13;
  --player-deep: #261811;
  --player-deeper: #17100c;
  --player-image: var(--bg-deep);
  --player-foot: var(--bg-foot);
  --page-end-bg: var(--bg-foot);
}

/* The ramp lives inside the track section and ends exactly at its bottom
   edge, which is the top of the still — so it lands on the image at any
   viewport without depending on page height. */
body:has(.tracks) .tracks {
  position: relative;
}

/* z-index 0 for the same reason as the homepage ramp above. */
body:has(.tracks) .tracks::before {
  content: "";
  position: absolute;
  inset: 0;
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  z-index: 0;
  pointer-events: none;
  /* Same palette and the same principle as the homepage ramp, but held mocha
     for longer: COMPOSER / PRODUCER and the upper rows must still read on the
     familiar background, so nothing perceptible happens until about a third of
     the way down and the darkening belongs to the lower half of the list.
     Ends on the image's own top edge. */
  background: linear-gradient(
    to bottom,
    var(--bg) 0%,
    #2c1c13 18%,
    #291911 30%,
    #241610 42%,
    #1f130d 54%,
    #1a100b 66%,
    #150c08 78%,
    #110a07 88%,
    #0e0906 95%,
    #0c0806 100%
  );
}

/* Padding bands either side of the frame: deep above to continue the ramp,
   darker below to match this image's lower edge. */
body:has(.tracks) .film-still {
  background-image: linear-gradient(
    to bottom,
    var(--bg-deep) 0%,
    var(--bg-deep) 30%,
    var(--bg-under) 100%
  );
}

body:has(.tracks) .site-foot {
  background-image: linear-gradient(
    to bottom,
    var(--bg-under) 0%,
    #090604 55%,
    var(--bg-foot) 100%
  );
}

/* --------------------------------------------------------------------------
   16. REDUCED MOTION
   There are no entrance, scroll or page-transition animations to disable.
   This only removes the hover fades.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}
