/* =========================================================
   SYNC. by FreshTracks — Landing Page
   Design source: Figma "FreshTracks Sync. (Landing Page Design)"
   Frame: Landing Page (Hero) — 1920 × 10288
   ========================================================= */

/* ---------- Fonts ----------
   CHANEY, licensed webfont kit (Font Squirrel build). Each width is its own
   family with a single normal weight, so they are declared separately rather
   than as weights of one family. woff2 covers every current browser; the woff
   is there for older ones. Licence PDF sits alongside the files.
---------------------------------------------------------------- */
@font-face {
  font-family: 'Chaney';
  src: url('assets/fonts/CHANEY-Regular.woff2') format('woff2'),
       url('assets/fonts/CHANEY-Regular.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Chaney Wide';
  src: url('assets/fonts/CHANEY-Wide.woff2') format('woff2'),
       url('assets/fonts/CHANEY-Wide.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Chaney Extended';
  src: url('assets/fonts/CHANEY-Extended.woff2') format('woff2'),
       url('assets/fonts/CHANEY-Extended.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Chaney Ultra';
  src: url('assets/fonts/CHANEY-UltraExtended.woff2') format('woff2'),
       url('assets/fonts/CHANEY-UltraExtended.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ---------- Tokens ---------- */
:root {
  --blue-note: #0037AB;
  --record-red: #FF3733;
  --white: #FFFFFF;
  --steel: #4E7FB5;
  --deep-red: #C8341F;

  --font-display: 'Chaney Ultra', 'Chaney Extended', 'Chaney', 'Arial Black', 'Helvetica Neue', sans-serif;
  --font-head: 'Chaney', 'Arial Black', 'Helvetica Neue', sans-serif;
  --font-ext:  'Chaney Extended', 'Chaney', 'Arial Black', 'Helvetica Neue', sans-serif;
  --font-wide: 'Chaney Wide', 'Chaney', 'Arial Black', 'Helvetica Neue', sans-serif;
  --font-mono: 'Inconsolata', ui-monospace, 'SF Mono', Menlo, monospace;

  /* 1920-based fluid scale: everything scales between 360px and 1920px */
  /* Up to 1920 this is the design's own gutter. Past it the second term takes
     over and grows with the extra width, so the content stays in a 1920 column
     centred on the screen instead of stretching across an ultrawide. At
     exactly 1920 both terms are 92.8px, so nothing below that moves. */
  --gutter: max(clamp(1.25rem, 4.84vw, 5.8rem), calc((100vw - 1920px) / 2 + 5.8rem));
  /* Display titles sit 24px from the page edge in the design, while everything
     else lines up with the gutter. min() so this only ever pulls left — once
     the gutter is tighter than 24px, on a phone, it does nothing. */
  /* Floored at the pull it has at 1920: past that the titles should bleed to
     the edge of the content column, not chase the edge of a 34-inch screen. */
  --edge-pull: max(calc(20px - 5.8rem), min(0px, calc(20px - var(--gutter))));
  --section-y: clamp(3rem, 5.5vw, 6.5rem);

  --fs-hero:   clamp(2.85rem, 8.33vw, 10rem);  /* 160px @1920 */
  /* no --fs-h2: the display headings are not one shared step, they each
     carry their own size from their own frame. See .section-title. */
  --fs-h3:     clamp(1.15rem, 1.67vw, 2rem);
  --fs-lede:   clamp(1rem, 1.46vw, 1.75rem);   /* 28px @1920 */
  --fs-body:   clamp(0.8rem, 0.83vw, 1rem);
  --fs-label:  clamp(.7rem, .9375vw, 1.125rem);  /* 18px @1920 */

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --nav-h: clamp(56px, 4.4vw, 84px);
  /* Height of the nav's tallest item, the CTA: its own font-size plus .58em of
     padding top and bottom on a line-height of 1. The menu's top row has no
     CTA, so without this the two rows differ and the centred logo jumps as the
     menu opens. Keep in step with .btn. */
  --nav-row-h: calc(clamp(.85rem, 1.25vw, 1.5rem) * 2.16);
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}
body {
  margin: 0;
  background: var(--blue-note);
  color: var(--white);
  font-family: var(--font-mono);
  font-size: var(--fs-body);
  line-height: 1.45;
  overflow-x: hidden;
}
img, svg, video { display: block; max-width: 100%; }
ul, ol { margin: 0; padding: 0; list-style: none; }
h1, h2, h3, p { margin: 0; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

::selection { background: var(--record-red); color: var(--white); }

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--record-red); color: #fff; padding: .75rem 1.25rem;
  font-family: var(--font-mono); text-transform: uppercase;
}
.skip-link:focus { left: 0; }

:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }
/* set by main.js when focus is handed back after a modal closes, and dropped
   again on the next key or click — see restoreFocusQuietly */
.is-focus-quiet:focus-visible { outline: none; }

/* ---------- Shared type ---------- */
/* The (0X_...) tags. 18px at 100% in the frames — measured on (01_SERV); the
   clients one is 20 and takes its own size below. Tracking is left as authored:
   Figma reports "2000%" for these, which is the same unusable readout as
   elsewhere, and there is no hug width on any of them to derive it from. */
.label {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: .12em;
  opacity: .85;
}
.clients__head .label { font-size: clamp(.75rem, 1.042vw, 1.25rem); }  /* 20 @1920 */
.label--right { text-align: right; }

/* Display headings are CHANEY Extended, weight 400, no tracking — but the size
   is per section in the frame, not one shared step. Measured, section by
   section: Selected Work and Services are 160/128; Why SYNC. and How we work
   are 120/92; Meet the team is 120/79%. The base here is the 120 that most of
   them take, and the two 160s override it below. */
.section-title {
  font-family: var(--font-ext);                 /* CHANEY Extended */
  font-size: clamp(1.9rem, 6.25vw, 7.5rem);     /* 120 @1920 */
  line-height: .79;
  text-transform: uppercase;
  letter-spacing: 0;
  font-weight: 400;
}
.work__title,
.services__title {
  font-size: clamp(2.2rem, 8.333vw, 10rem);     /* 160 @1920 */
  line-height: .8;                              /* 128 on 160 */
}
.why__title,
.process__title { line-height: .767; }          /* 92 on 120 */

.btn {
  display: inline-block;
  font-family: var(--font-ext);
  font-size: clamp(.85rem, 1.25vw, 1.5rem);   /* 24px @1920 */
  line-height: 1;
  text-transform: uppercase;
  /* The frame sets -1.5px at 24px, which the designer has since called too
     tight on the CTAs. Opened to a slight positive instead — a deliberate
     departure from 2280-44, not an oversight. Covers all three: the nav, the
     services card and the brief's submit. */
  letter-spacing: .02em;
  padding: .58em 1em;
  border-radius: 14px;
  transition: transform .25s var(--ease), background-color .25s var(--ease), color .25s var(--ease);
  white-space: nowrap;
}
.btn--red { background: var(--record-red); color: var(--white); }
/* colour only — the old translateY made it "pop" under the cursor */
.btn--red:hover { background: var(--white); color: var(--record-red); }
.btn--ghost { border: 1px solid currentColor; }
.btn--ghost:hover { background: var(--white); color: var(--record-red); }

/* =========================================================
   00 · NAVIGATION STRIP
   ========================================================= */
.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 90;
  padding: clamp(.75rem, 2.2vw, 2.6rem) var(--gutter) 0;
  transition: transform .35s var(--ease);
  /* The bar is a strip of mostly empty space sitting over the hero. Letting it
     fall through keeps the play cursor right up to the top edge; the three
     controls take their events back below. */
  pointer-events: none;
}
.nav.is-hidden { transform: translateY(-140%); }

/* The bar is type on nothing. Over the hero that reads, but coming back on a
   scroll-up it lands on whatever section is under it. A scrim from the top
   edge, fading out before the bar's own baseline, so it never reads as a
   solid header — and only once you have left the top. */
.nav::before {
  content: "";
  position: absolute; inset: 0 0 -2.5rem 0;
  background: linear-gradient(180deg, rgba(0,0,0,.5) 0%, rgba(0,0,0,.26) 52%, rgba(0,0,0,0) 100%);
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s var(--ease);
  /* .nav is already a stacking context, so this drops under its own content
     without having to position the three controls to outrank it */
  z-index: -1;
}
.nav.is-stuck::before { opacity: 1; }

/* .nav is pointer-events: none so the strip does not sit over the hero and eat
   clicks meant for the film; each thing you can actually press opts back in. The
   plane was missing from this list, so pressing it played the showreel through
   it instead. */
.nav__burger, .nav__logo, .nav__cta, .nav__plane { pointer-events: auto; }
.nav__inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1rem;
  min-height: var(--nav-row-h);
}

/* The bars stay 34x26; the padding only grows the hitbox, and the matching
   negative margin keeps the glyph where it was — otherwise it drifts inward
   and, over the hero, the play plate steals the click. */
.nav__burger, .menu__close {
  display: flex; flex-direction: column; justify-content: center; gap: 5px;
  box-sizing: content-box;
  width: 34px; height: 26px;
  padding: 14px 16px; margin: -14px -16px;
  justify-self: start;
}
.nav__burger span, .menu__close span {
  display: block; height: 3px; width: 100%;
  background: var(--white);
  transition: transform .3s var(--ease), opacity .3s var(--ease);
}
.nav__burger:hover span,
.menu__close:hover span { background: var(--record-red); }

.menu__close span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
.menu__close span:nth-child(2) { opacity: 0; }
.menu__close span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }

.nav__logo img {
  display: block;
  width: clamp(104px, 8.2vw, 157px);
  height: auto;
}
.nav__logo { text-align: center; justify-self: center; }
.nav__logo-main {
  display: block;
  font-family: var(--font-head);
  font-size: clamp(1.1rem, 1.56vw, 1.9rem);
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: 1;
}
.nav__logo-sub {
  display: block;
  font-family: var(--font-mono);
  font-size: clamp(.55rem, .58vw, .7rem);
  letter-spacing: .18em;
  opacity: .9;
}
.nav__cta { justify-self: end; }

/* Hidden until the mobile breakpoint turns it on. Padding rather than a bigger
   drawing for the touch target: the glyph is 42px wide and the box around it is
   44 square, so the plane stays the size it is drawn at and the finger still has
   somewhere to land. */
.nav__plane {
  display: none;
  justify-self: end;
  position: relative;
  align-items: center; justify-content: center;
  box-sizing: content-box;
  min-width: 44px; min-height: 44px;
  padding: 0;
  background: none; border: 0;
  cursor: pointer;
}
.nav__plane-fin {
  fill: var(--record-red);
  transition: fill .25s var(--ease);
}
.nav__plane:hover .nav__plane-fin,
.nav__plane:focus-visible .nav__plane-fin { fill: var(--blue-note); }

/* While it is in the air the plane in the strip is empty: what you are watching
   is a copy of it flying, positioned over the page. */
.nav__plane.is-flying { pointer-events: none; }
.nav__plane.is-flying svg { opacity: 0; }

/* The copy that actually flies. Fixed, so the path can be worked out in plain
   viewport coordinates, and above everything including the form it opens. */
.plane-flight {
  position: fixed; left: 0; top: 0;
  z-index: 200;
  pointer-events: none;
  transform-origin: 50% 50%;
  will-change: transform, opacity;
}
.plane-flight svg { display: block; width: 100%; height: auto; }
/* In flight it wears its pressed colour: the fin turns Blue Note the moment it
   launches, which is the hover state the design gives it. */
.plane-flight .nav__plane-fin { fill: var(--blue-note); }

/* The form arrives as the plane leaves the screen. A short rise rather than a
   plain fade: the plane carried the brief off and this is what it turns into, so
   it should land rather than dissolve in. Small enough not to be a flourish. */
@keyframes brief-pop {
  from { opacity: 0; scale: .965; }
  to   { opacity: 1; scale: 1; }
}
.brief.is-popping { animation: brief-pop .34s cubic-bezier(.2,.7,.3,1) both; }
@media (prefers-reduced-motion: reduce) {
  .brief.is-popping { animation: none; }
}


@media (prefers-reduced-motion: reduce) {
  .nav__plane svg, .nav__plane-fin { transition: none; }
  .nav__plane.is-flying svg { translate: 0 0; rotate: 0deg; opacity: 0; }
}

@media (max-width: 640px) {
  /* The paper plane replaces the text CTA. Two things the designer wanted: the
     button smaller, and the logo actually in the middle. Both are the same fix —
     the strip is 1fr auto 1fr, so the wider of the two outer tracks decides where
     the centre column sits, and the text button was far wider than the burger.
     The plane is sized to sit inside the burger's width so the sides balance. */
  .nav__cta { display: none; }
  .nav__plane { display: flex; }
  .nav__plane svg { width: 38px; height: auto; }
}

/* =========================================================
   NAVIGATION OVERLAY
   ========================================================= */
.menu {
  position: fixed; inset: 0; z-index: 100;
  background: var(--blue-note);
  display: flex; flex-direction: column;
  padding: clamp(.75rem, 2.2vw, 2.6rem) var(--gutter) clamp(1.5rem, 3vw, 3rem);
  opacity: 0; visibility: hidden;
  transition: opacity .35s var(--ease), visibility .35s var(--ease);
}
.menu.is-open { opacity: 1; visibility: visible; }

.menu__top { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; min-height: var(--nav-row-h); }
.menu__spacer { display: block; }

.menu__list {
  flex: 1;
  display: flex; flex-direction: column; justify-content: center;
  gap: clamp(.1rem, .4vw, .35rem);
}
.menu__list a {
  display: inline-block;
  white-space: nowrap;   /* one line per item, as the frame sets them */
  font-family: var(--font-ext);
  /* floor drops to 24: at 32 the longest item measures 412 against the 350 a
     390 screen has, and nowrap sent the overflow off the side. Only binds
     below 536px, so the frame's size is untouched everywhere it fits. */
  font-size: clamp(1.5rem, 4.48vw, 5.375rem);
  line-height: 1.02;
  text-transform: uppercase;
  transition: color .2s var(--ease), background-color .2s var(--ease);
  padding: 0 .12em;
  line-height: .88;
}
.menu__list a:hover, .menu__list a:focus-visible { background: var(--record-red); }

.menu__bottom { display: flex; justify-content: space-between; align-items: flex-end; gap: 2rem; flex-wrap: wrap; }
.menu__socials a { display: block; margin-top: .35rem; font-family: var(--font-wide); font-size: clamp(.9rem, 1.46vw, 1.75rem); line-height: .88; text-transform: uppercase; }
.menu__socials a:hover { color: var(--record-red); }

/* Frame 3503 — 368 x 48: a Record Red block and the eq square, 6px apart.
   The text sits in a frame fixed at 48px for all three lines, so the pitch is
   48/3 = 16px. The 26px line-height on the text node itself is overflow the
   designer let ride — matching it blows the block to 78px. */
.miniplayer {
  display: flex;
  gap: .31vw;
  width: clamp(268px, 19.17vw, 368px);
  max-width: 100%;
  height: clamp(35px, 2.5vw, 48px);
  margin-top: .5rem;
}
.miniplayer__main {
  flex: 1;
  min-width: 0;
  display: flex; align-items: center; gap: .885vw;   /* 17px @1920 */
  background: var(--record-red);
  padding: 0 .625vw;
  text-align: left;
  transition: filter .2s var(--ease);
}
.miniplayer__main:hover { filter: brightness(1.08); }

.miniplayer__icon {
  flex: 0 0 auto;
  width: 15px; height: 17px;
  background: var(--white);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
/* swaps to a pause glyph while the playlist is running */
.miniplayer.is-playing .miniplayer__icon {
  clip-path: none;
  width: 13px;
  background: linear-gradient(90deg, var(--white) 0 38%, transparent 38% 62%, var(--white) 62% 100%);
}

.miniplayer__text {
  display: flex; flex-direction: column;
  font-family: var(--font-ext);
  font-size: clamp(11px, .83vw, 16px);   /* 16px @1920 */
  line-height: 1;                        /* 16px pitch, three lines to 48px */
  text-transform: uppercase;
  color: var(--white);
  overflow: hidden;
}
.miniplayer__text span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.miniplayer__eq {
  flex: 0 0 clamp(35px, 2.5vw, 48px);   /* square, matches the player height */
  display: grid; place-items: center;
  background: var(--record-red);
  transition: filter .2s var(--ease);
}
.miniplayer__eq:hover { filter: brightness(1.08); }
.miniplayer__eq > span { display: flex; align-items: flex-end; gap: 3px; height: 22px; }
.miniplayer__eq i { width: 4px; background: var(--white); }
.miniplayer__eq i:nth-child(1) { height: 45%; }
.miniplayer__eq i:nth-child(2) { height: 100%; }
.miniplayer__eq i:nth-child(3) { height: 65%; }
.miniplayer__eq i:nth-child(4) { height: 85%; }
/* the bars only dance while something is actually playing */
.miniplayer.is-playing .miniplayer__eq i {
  animation: eq .9s ease-in-out infinite alternate;
  transform-origin: bottom;
}
.miniplayer.is-playing .miniplayer__eq i:nth-child(1) { animation-delay: -.2s; }
.miniplayer.is-playing .miniplayer__eq i:nth-child(2) { animation-delay: -.6s; }
.miniplayer.is-playing .miniplayer__eq i:nth-child(3) { animation-delay: -.4s; }
@keyframes eq { from { transform: scaleY(.4); } to { transform: scaleY(1); } }

/* =========================================================
   01 · HERO
   ========================================================= */
.hero {
  position: relative;
  min-height: clamp(560px, 56vw, 1076px);
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: calc(var(--nav-h) + 3rem) var(--gutter) clamp(2rem, 4vw, 4.5rem);
  overflow: hidden;
  background: var(--blue-note);
}
/* Showreel plate. The <video> falls back to its poster until the loop exists,
   so this looks identical either way. */
.hero__media {
  /* the new frame runs the film the full height of the hero */
  position: absolute; inset: 0;
  overflow: hidden;
  background: #2b2320;
  /* Quiet while the headline is up — four lines of 160px type is the thing to
     read first, and the footage only has to suggest motion behind it. Once the
     hero swaps to REEL. the headline is gone and the film becomes the subject,
     so it comes up to full. */
  opacity: .58;
}
.hero__video { width: 100%; height: 100%; object-fit: cover; }
.hero__media::after {
  content: "";
  position: absolute; inset: 0;
  /* heavier at the top, where the headline crosses it. Gradients can't be
     transitioned, so the wash lifts by fading the whole layer instead. */
  background: linear-gradient(180deg, rgba(0, 55, 171, .42) 0%, rgba(255, 55, 51, .22) 55%);
  pointer-events: none;
}
.hero__wrap { position: relative; z-index: 1; }

.hero__title {
  /* Full page width, not gutter to gutter: the frame sets this in four lines
     with "work sound" together, and at the narrower width it broke to five —
     an extra 123px band of type, which is what pushed the block up under the
     nav in the first place. */
  margin-left: var(--edge-pull);
  margin-right: var(--edge-pull);
  font-family: var(--font-display);
  /* Capped to what the column can hold. A line is free to wrap, so what has to
     fit is not the longest line but the longest TOKEN — and --fs-hero has a
     2.85rem floor, so on a phone the type stops shrinking while the column
     carries on, until "UNFORGETT-" wanted 499px of a 350px line and was cut
     mid-word by the line-reveal's mask.

     6.4 is the longest token in multiples of the font size, measured off the
     rendered face, taking the narrow line variants into account — remeasure it
     if the headline ever changes. The column is 100vw - 40px at every width
     below about 1900, because the gutters and --edge-pull cancel out.

     min() so this can only ever make it smaller, and with the word broken it now
     binds on nothing bigger than a small phone: at 390 the cap is 54.7px against
     the 45.6 already set, at 768 it is 114 against 64, at 1800 it is 275 against
     150. It earns its place below about 350px wide, where the column finally
     gets narrower than the word. */
  font-size: min(var(--fs-hero), calc((100vw - 40px) / 6.4));
  line-height: .77;
  text-transform: uppercase;
  color: var(--white);
  font-weight: 400;
  margin-bottom: clamp(1.5rem, 3.6vw, 4.3rem);
}

/* ---------------------------------------------------------------
   Shared line reveal — every display title enters from its left edge.
   Each line masks its own travel; the vertical padding/margin pair keeps
   the mask from clipping glyphs at these tight line-heights.
   ---------------------------------------------------------------- */
.rl {
  display: block;
  overflow: hidden;
  padding-block: .08em;
  margin-block: -.08em;
}
.rl > span {
  display: block;
  will-change: transform;
  transform: translateX(-105%);
  transition: transform .95s cubic-bezier(0.16, 0.84, 0.28, 1);
}
/* right-aligned headings enter from their right edge instead */
.rl--right > span { transform: translateX(105%); }

/* The headline's long word: one line on a wide screen, two on a narrow one —
   see the note in index.html. These have to sit AFTER .rl rather than with the
   other mobile rules: a media query adds no specificity, so against .rl's own
   display: block it is source order that decides, and up there they lost. */
.rl--narrow { display: none; }
@media (max-width: 640px) {
  .rl--wide { display: none; }
  .rl--narrow { display: block; }
}
[data-lines].is-in .rl > span { transform: translateX(0); }

[data-lines].is-in .rl:nth-child(1) > span { transition-delay: .05s; }
[data-lines].is-in .rl:nth-child(2) > span { transition-delay: .15s; }
[data-lines].is-in .rl:nth-child(3) > span { transition-delay: .25s; }
[data-lines].is-in .rl:nth-child(4) > span { transition-delay: .35s; }

/* --- [data-chars]: the glyphs rise out of the line, left to right ---
   An opt-in on top of [data-lines] — the line still supplies the mask and the
   .is-in trigger, but the whole row no longer slides in as one piece; main.js
   splits it and each character comes up out of its own box instead. */
[data-chars] .rl > span {
  transform: none;
  transition: none;
  /* Glyph masks are inline-blocks, so the line box has to leave room for the
     strut's descender under their baseline — which added about an eighth of an
     em to every line. Zeroing the container's leading and hanging the masks
     from the top gives back the frame's tight setting. */
  line-height: 0;
}
/* Keeps a line breaking at spaces only. Glyph masks are inline-blocks, so
   without a word wrapper the browser may break between any two letters; and
   nowrap because an inline-block is shrink-to-fit — "unforgett-" is a hair
   wider than its line and is meant to bleed past the edge, not fold at the
   hyphen the way a capped inline-block would. */
/* The .rl overshoot guard is padding .08em against margin -.08em. Between two
   lines those negative margins collapse into one, so every gap kept half the
   padding — the rest of the leading the designer flagged. Doubling the top
   margin on each line after the first cancels both halves. */
[data-chars] .rl + .rl { margin-top: -.16em; }

.word { display: inline-block; white-space: nowrap; }
.ch {
  display: inline-block;
  vertical-align: top;
  line-height: inherit;
  overflow: hidden;
  /* same trick as .rl: round glyphs overshoot the cap line and would clip */
  padding-block: .08em;
  margin-block: -.08em;
}
.ch > span {
  display: inline-block;
  line-height: var(--lh, .77);   /* the masks carry the leading, not the line */
  will-change: transform;
  translate: 0 115%;
  transition: translate .8s cubic-bezier(0.16, 0.84, 0.28, 1);
  transition-delay: calc(var(--n) * 26ms);
}
[data-chars].is-in .ch > span { translate: 0 0; }

@media (prefers-reduced-motion: reduce) {
  .rl > span { transform: none; transition: none; }
  .ch > span { translate: 0 0; transition: none; }
}



/* the PLAY tag belongs to the reel state, so hold it visible there */

@media (prefers-reduced-motion: reduce) {
  .hero__title, }
/* 790px of the 1920 frame — at 49ch it stacked into six lines and read as a
   paragraph rather than a standfirst. */
.hero__intro { max-width: min(100%, 41vw); }
.hero__sub {
  font-family: var(--font-head);                /* CHANEY Regular */
  font-size: clamp(.85rem, 1.458vw, 1.75rem);   /* 28 @1920 */
  line-height: .88;
  text-transform: uppercase;
  margin-top: .5rem;
}

/* =========================================================
   02 · CLIENT LOGOS
   ========================================================= */
.clients {
  background: var(--record-red);
  padding: clamp(2rem, 3.6vw, 4.3rem) 0;
}
/* label sits above the heading, both hard right, as in the frame */
.clients__head {
  padding: 0 var(--gutter);
  margin-bottom: clamp(1.25rem, 2.4vw, 2.8rem);
  text-align: right;
}
.clients__title {
  font-family: var(--font-wide);                /* CHANEY Wide */
  font-size: clamp(1.35rem, 3.125vw, 3.75rem);  /* 60 @1920 */
  text-transform: uppercase;
  line-height: .88;
  letter-spacing: 0;
  font-weight: 400;
  margin-left: auto;
  text-align: right;
}
.clients__head .label { margin-bottom: .35rem; }

.marquee { display: flex; overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent); mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent); }
/* Each copy of the strip is its own element and travels exactly its own width,
   which is what makes the loop seamless: at the end of a cycle every copy sits
   precisely where the copy before it started, so the frame is identical to the
   frame at the start and there is nothing to see. -100% of the element itself
   needs no measuring, no custom property and no division, so it cannot be
   thrown off by a clamp resolving differently, by sub-pixel widths, or by a
   browser that resolves calc() in keyframes differently from this one.

   No gap: the spacing rides on each item as margin-right, so the trailing edge
   of a copy carries its own spacing and the joint between copies matches the
   spacing inside them. */
.marquee__track {
  display: flex; align-items: center;
  flex: none;
  animation: marquee 38s linear infinite;
}
.marquee__track > * { margin-right: clamp(2rem, 4vw, 5rem); }
.marquee:hover .marquee__track { animation-play-state: paused; }
.marquee__track li {
  font-family: var(--font-head);
  font-size: clamp(1rem, 1.72vw, 2.05rem);
  text-transform: uppercase;
  letter-spacing: .02em;
  white-space: nowrap;
  opacity: .95;
}
.marquee__track li sub { font-size: .6em; }
/* logos are white-filled SVGs — normalise them to a common optical height */
.marquee__track li img {
  height: clamp(20px, 1.8vw, 34px);
  width: auto;
  max-width: clamp(120px, 14vw, 260px);
  object-fit: contain;
}
@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-100%); } }

/* =========================================================
   03 · STATISTICS
   ========================================================= */
.stats {
  position: relative;
  background: var(--record-red);
  padding: clamp(2rem, 4vw, 4.6rem) var(--gutter) clamp(3rem, 6vw, 7rem);
  overflow: hidden;
}
/* the FreshTracks mark — each stroke draws itself when the section scrolls in.
   Dash length is measured per path in JS, since getTotalLength() is the only
   reliable way to know it. */
.stats__mark {
  position: absolute; left: -10%; top: 50%;
  transform: translateY(-50%);
  width: min(52vw, 780px); height: auto;
  pointer-events: none;
  overflow: visible;
}
.stats__mark path {
  fill: none;
  stroke: var(--blue-note);
  /* non-scaling-stroke holds the line at a literal screen width, which is what
     the drawing needs — but then a mark that shrinks from 780 to ~200 keeps a
     2px line and reads three times heavier. Tie it to the mark's own scale. */
  stroke-width: clamp(.75px, .133vw, 2px);
  vector-effect: non-scaling-stroke;
  /* --len is overwritten with each path's real getTotalLength() before the draw
     starts. The default has to be here and comfortably longer than any path
     (the longest is ~2400) — without it var(--len) is invalid, stroke-dasharray
     falls back to `none`, and the whole mark renders solid until JS runs. */
  --len: 5000;
  stroke-dasharray: var(--len) var(--len);
  stroke-dashoffset: var(--len);
}
.stats__mark.is-drawn path {
  animation: draw 2.4s cubic-bezier(0.32, 0.1, 0.2, 1) forwards;
}
.stats__mark.is-drawn path:nth-child(2) { animation-delay: .28s; }
.stats__mark.is-drawn path:nth-child(3) { animation-delay: .56s; }
@keyframes draw { to { stroke-dashoffset: 0; } }

@media (prefers-reduced-motion: reduce) {
  .stats__mark path { stroke-dashoffset: 0; }
  .stats__mark.is-drawn path { animation: none; }
}

/* The mark is absolutely positioned, so it paints over the section's in-flow
   content whatever the DOM order says. It is vertically centred and only about
   50px shorter than the section, so anywhere the section resolves a little
   shorter — different fallback metrics, browser zoom — its strokes reach the
   top-left corner and cross out the (03_stats) label. Ordering it explicitly
   instead of relying on that margin. */
.stats__mark { z-index: 0; }
.stats > .label,
.stats__big,
.stats__row,
.stats__solid { position: relative; z-index: 1; }

.stats .label { color: var(--white); opacity: 1; }

/* frame 44-3806 — Extended throughout, not the Ultra width these used to take.
   .stats__big is split out because it is 220 against the other two at 120. */
.stats__big, .stats__solid {
  font-family: var(--font-ext);                 /* CHANEY Extended */
  text-transform: uppercase;
  letter-spacing: 0;
  color: var(--blue-note);
  font-weight: 400;
}
.stats__big {
  font-size: clamp(2.6rem, 11.458vw, 13.75rem); /* 220 @1920 */
  line-height: .782;                            /* 172 on 220 */
  --lh: .782;                  /* [data-chars] takes its leading from here */
}
.stats__big {
  margin: clamp(1rem, 2vw, 2.5rem) 0 clamp(2rem, 5vw, 5.5rem);
  margin-left: var(--edge-pull);
}

/* The frame puts this block at 1105 of 1920 — a little past halfway, left
   aligned, with its label alongside — rather than flush to the right gutter. */
.stats__row {
  position: relative;
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
  margin-left: 57.6%;
}
/* The block already runs to within 20px of the page edge, so there is no room
   for the label beside it — the frame tucks it alongside the first line
   instead, and flexed in a row it simply fell off the screen. */
.stats__row .label {
  position: absolute;
  top: .3em;
  right: 0;
  white-space: nowrap;
}
/* The front card of the collapsing stack: opaque in the section's own red with
   a Blue Note outline, exactly like its echoes, so it occludes them cleanly as
   they fold in. It used to take a blue fill that swept in left-to-right, but a
   solid blue block sitting under a set of light outlined echoes read as a
   different element rather than the one they collapse into. */
.stats__outline {
  font-family: var(--font-ext);
  font-size: clamp(1.9rem, 6.25vw, 7.5rem);   /* 120px @1920 */
  line-height: .79;
  text-transform: uppercase;
  text-align: left;
  color: var(--record-red);
  /* scales with the type: 2px against 120px, a hairline against 30px */
  -webkit-text-stroke: clamp(.75px, .104vw, 2px) var(--blue-note);
  paint-order: stroke fill;
}

/* The blue fill that sweeps in once the stack has folded in: a gradient
   clipped to the glyphs, sitting behind the red fill, which is why .is-filled
   drops `color` to reveal it. Safe only at that point, because by then the
   echoes have faded and there is nothing left behind to occlude.

   It lives on each line rather than on the block. On the block it is one
   painted layer clipped to the text of three children — and lines 2 and 3
   carry their own translate and opacity for the slide-in, so a compositor is
   free to paint them separately and they miss the fill entirely. That is
   machine-dependent, which is exactly how it showed up: only "200+" filling,
   and not reproducible. Per line, each one owns its own paint. */
.stats__outline span {
  background-image: linear-gradient(90deg,
      var(--blue-note) 0, var(--blue-note) calc(100% - .6em),
      rgba(0, 55, 171, .35) 100%);
  background-repeat: no-repeat;
  background-size: 0% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  transition: background-size 1.1s cubic-bezier(0.33, 0, 0.15, 1);
}
.stats__outline.is-filled { color: transparent; }
.stats__outline.is-filled span { background-size: 100% 100%; }
.stats__outline.is-filled span:nth-child(2) { transition-delay: .14s; }
.stats__outline.is-filled span:nth-child(3) { transition-delay: .28s; }

/* Collapsing echoes. Copies of the block sit above it in thinner outline,
   spread far enough apart to weave into each other, then fold down into the
   real one as you scroll past. main.js scrubs --collapse from 1 to 0 against
   scroll position, so the reader controls the pace — that's what makes the
   woven state readable rather than a flicker. They're absolutely positioned,
   so they rise into the margin under "33+ years in music." without moving
   anything. */
.stats__stack {
  position: relative;
  display: inline-block;
  /* Travel of the furthest copy. There is ~84px of clear ground between the
     block and "33+ years in music." above it, so this is what the stack has to
     live inside — and it is why there are four copies rather than more: fewer,
     further apart reads as separate rows, more of them and the top of the
     stack closes up into a single blur. */
  --spread: .67em;
}
.stats__outline span { display: block; }

/* "clients" and "served." take no stack — there is only 7px between lines — so
   they get their own entrance instead: each slides in from the right as the
   row reveals, a beat behind the one above. Only on the real block; the ghosts
   keep these lines hidden. */
.stats__outline:not(.stats__ghost) span:nth-child(n+2) {
  translate: 18% 0;
  opacity: 0;
  transition: translate .85s var(--ease), opacity .6s var(--ease);
}
.stats__row.is-in .stats__outline:not(.stats__ghost) span:nth-child(2) { translate: 0 0; opacity: 1; transition-delay: .12s; }
.stats__row.is-in .stats__outline:not(.stats__ghost) span:nth-child(3) { translate: 0 0; opacity: 1; transition-delay: .26s; }

@media (prefers-reduced-motion: reduce) {
  .stats__outline:not(.stats__ghost) span:nth-child(n+2) { translate: 0 0; opacity: 1; transition: none; }
}
.stats__ghost {
  position: absolute;
  inset: 0;
  margin: 0;
  pointer-events: none;
  /* Fill and stroke come from .stats__outline: each echo is opaque in the
     section's own colour, so it fully occludes the one behind it and its own
     stroke becomes the line that separates them. Stacking translucent outlines
     over outlines is exactly what read as blur. DOM order puts the furthest
     copy at the back, so they overlap like dealt cards. */
  /* One opacity for all of them, and only over the last stretch of the travel:
     fading them individually would let the copy behind show through, which is
     the blending we're getting rid of. By the time this bites they're nearly
     coincident anyway. */
  opacity: min(1, var(--collapse, 1) * 6);
  translate: 0 calc(var(--off, 0em) * var(--collapse, 1) * -1);
  /* just enough smoothing to take the steps out of a wheel scroll */
  transition: translate .18s linear, opacity .18s linear;
}
/* First line only. The face has 81px caps on an 89px line — 7px of clearance —
   so a copy of "clients" lifted far enough to read lands inside "200+", and
   three lines of unrelated strokes in one band is the weave we kept getting.
   The reference has no such problem because it is a single line. "200+" is the
   one line here with real space above it, so it takes the stack and the other
   two hold still. visibility keeps them in the layout, so the ghost's first
   line stays registered with the block's. */
.stats__ghost span:not(:first-child) { visibility: hidden; }

/* Steps shrink with depth, matching the reference's own ramp (its gaps run
   35%, 29%, 23%, 18% of a cap). The real block is always the last child, so
   the nearest ghost is :nth-last-child(2). Stroke and opacity stay uniform
   across the copies, as they do there. */
.stats__ghost:nth-last-child(2) { --off: calc(var(--spread) * .40); }
.stats__ghost:nth-last-child(3) { --off: calc(var(--spread) * .73); }
.stats__ghost:nth-last-child(4) { --off: var(--spread); }

@media (max-width: 768px) {
  /* The type bottoms out at 1.9rem here, so the desktop settings stop working:
     three echoes land inside a 25px line box and every copy overlaps two others,
     and a 2px stroke is three times heavier relative to the em than it is at
     84px. So: one echo, a hairline stroke, and a wider gap between the two. */
  .stats__stack { --spread: .55em; }
  /* keep only the echo nearest the block — the real <p> is the last child, so
     that's the second from the end however many ghosts main.js makes */
  .stats__ghost:not(:nth-last-child(2)) { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .stats__ghost { display: none; }
}
.stats__solid { text-align: right; margin-top: clamp(1.5rem, 3vw, 3.5rem); font-size: clamp(1.9rem, 6.25vw, 7.5rem); line-height: .79; }

@media (max-width: 768px) {
  /* The row stops being a row here, and the label is absolutely placed against
     the figure's top-right — which put it straight through the "200+". Back
     into the flow above the block, where column-reverse orders it. */
  .stats__row { flex-direction: column-reverse; align-items: flex-end; margin-left: 0; }
  .stats__row .label { position: static; margin-bottom: .5rem; }
  .stats__stack { width: 100%; }
}

/* =========================================================
   04 · SELECTED WORK
   ========================================================= */
.work {
  background: var(--blue-note);
  padding: clamp(1rem, 1.6vw, 2rem) var(--gutter) var(--section-y);
}
/* Sits inside the blue band, flush under the logo strip. It used to be pulled
   up to straddle the seam with the stats section — since Selected Work moved
   above stats, that offset collided with the logos. */
.work__title {
  position: relative;
  z-index: 2;
  margin: 0 0 clamp(1.5rem, 3vw, 3rem);
  margin-left: var(--edge-pull);
}
.work__grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: clamp(.9rem, 1.8vw, 2.15rem);   /* 34px @1920, matching the frame */
}
.work__item { grid-column: span 2; }
.work__item--wide { grid-column: span 3; }

/* ---- four-up, while the fifth film is uncleared ----
   Five tiles fill the six columns as 3+3 over 2+2+2. Four cannot: two wide and
   two square leave a third of the second row empty. So the four run as equal
   halves, all on the wide tile's proportions, with the right-hand column
   dropped by a tile's worth — four identical tiles in a quadrant is a table,
   the same four on a slant is a composition, and the slant is already the
   page's own, from the ribbon.

   To go back to five: delete this block and the work__grid--quad class in the
   markup, and restore the commented-out article. Confined to a media query
   because below 900px the grid is already two columns and below 560 it is one,
   where a stagger would just be a hole. */
@media (min-width: 901px) {
  .work__grid--quad .work__item,
  .work__grid--quad .work__item--wide { grid-column: span 3; }
  .work__grid--quad .work__thumb { aspect-ratio: 900 / 774; }
  .work__grid--quad .work__item:nth-child(2n) { margin-top: clamp(2rem, 5.5vw, 6.5rem); }
}

/* The tile is the button, not the little ring inside it: clicking anywhere on
   the picture opens the film. The glyph is a span now, so it takes no focus of
   its own and the focus ring outlines the whole tile — which reads as a
   selection rather than as a second circle drawn around the first. */
.work__thumb {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  text-align: start;          /* a <button> centres by default */
  /* 592 x 592 in the updated frame — the small tiles are square now */
  aspect-ratio: 1 / 1;
  overflow: hidden;
  --par-range: 7%;
}
/* The still sits on ::before rather than on the button, so it can be taller
   than the frame it shows through and slide inside it. 7% of overhang top and
   bottom is the room the parallax travels in; the still never reaches an edge,
   so no amount of scrolling can expose one. Script sets --par. */
.work__thumb::before {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: calc(var(--par-range) * -1);
  bottom: calc(var(--par-range) * -1);
  background: var(--img, linear-gradient(140deg, #3a3a3a, #1a1a1a));
  background-size: cover; background-position: center;
  translate: 0 var(--par, 0px);
}
/* No travel, so no overhang to hide — the still just fills the frame. */
@media (prefers-reduced-motion: reduce) {
  .work__thumb { --par-range: 0%; }
  .work__thumb::before { translate: none; }
}
.work__thumb:focus-visible { outline-offset: -4px; }
/* 900 x 774 in the frame, so much deeper than the old 2/1 letterbox */
.work__item--wide .work__thumb { aspect-ratio: 900 / 774; }
.work__thumb::after {
  content: ""; position: absolute; inset: 0;
  background: var(--blue-note);
  opacity: 0; transition: opacity .3s var(--ease);
  mix-blend-mode: multiply;
  /* decorative tint only — without this it covers the play button and
     swallows every click on it */
  pointer-events: none;
}
/* the thumbnail is the target, not the whole card with its meta block */
.work__thumb:hover::after { opacity: .35; }

.play {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: clamp(34px, 3vw, 58px); aspect-ratio: 1;
  border: 2px solid var(--white); border-radius: 50%;
  transition: background-color .25s var(--ease), transform .25s var(--ease);
}
/* A real-sized box clipped to a triangle, so it centres predictably. The small
   nudge right compensates for the triangle's centre of mass sitting left of its
   bounding box — without it a play glyph reads as off-centre in a circle. */
.play::before {
  content: "";
  position: absolute; left: 50%; top: 50%;
  width: 30%; height: 34%;
  background: var(--white);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  transform: translate(-50%, -50%) translateX(9%);
}
.work__thumb:hover .play { background: var(--record-red); transform: translate(-50%, -50%) scale(1.08); }

/* Leads where the still lags — see the parallax block in main.js. */
.work__meta { padding-top: .75rem; translate: 0 var(--par-meta, 0px); }
@media (prefers-reduced-motion: reduce) {
  .work__meta { translate: none; }
}
/* Touch devices hold the still still.
   Moving the ::before repaints it, and what it paints is a 1920px JPEG that
   decodes to some eleven megabytes. Four of those on a phone, re-rastered on
   every frame of a flick scroll, is more than iOS will keep: it drops the
   decoded copy, the re-decode misses the frame, and the tile paints empty —
   the still vanishes and comes back while everything else in the tile stays
   put. Pinned, the still is rasterized once and scrolls with the page.
   This has to sit after both blocks above: a media query adds no specificity,
   so it wins on source order or not at all. */
@media (hover: none) {
  .work__thumb { --par-range: 0%; }
  .work__thumb::before { translate: none; }
  .work__meta { translate: none; }
}
/* The first badge in a set is the category: outlined rather than filled, so it
   reads as a label and the ones after it as attributes. */
.work__meta .tag:first-child {
  background: transparent;
  color: var(--record-red);
  box-shadow: inset 0 0 0 1px currentColor;
}
/* the filled badges set their label in Blue Note, not white */
.work__meta .tag { color: var(--blue-note); }

.tag {
  display: inline-block;
  background: var(--record-red);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: clamp(.7rem, .83vw, 1rem);   /* 16px @1920 */
  line-height: 1;
  text-transform: uppercase; letter-spacing: 0;
  padding: .35em 1em; margin: 0 .3rem .5rem 0;
  border-radius: 999px;
}
.work__meta h3 {
  font-family: var(--font-display);            /* CHANEY Ultra Extended */
  font-size: clamp(.85rem, 1.04vw, 1.25rem);   /* 20 @1920 */
  text-transform: uppercase; font-weight: 400;
  line-height: 1.05;                           /* 21 on 20 */
  letter-spacing: 0;
}
/* Inconsolata Bold at the same 20px as the title, and full white — it used to
   be a size smaller at .85 opacity, which is what read as a caption. */
.work__meta p {
  font-family: var(--font-mono);
  font-weight: 700;                            /* Inconsolata Bold */
  font-size: clamp(.85rem, 1.04vw, 1.25rem);   /* 20 @1920 */
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: .02em;
  opacity: 1;
}

@media (max-width: 900px) {
  .work__grid { grid-template-columns: repeat(2, 1fr); }
  .work__item, .work__item--wide { grid-column: span 1; }
}
/* Below this the longest item can still outrun the column even at the 24px
   floor, so let it take a second line rather than run off the side. */
@media (max-width: 400px) {
  .menu__list a { white-space: normal; }
}

@media (max-width: 560px) {
  /* "Submit your brief" wants 213px and the front card has 168 to give at this
     size, and .btn is nowrap, so it ran straight out the side. Two lines here,
     filling the card's width and centred, so it reads as deliberate rather
     than clipped. Only below 560 — at that width it still fits on one line,
     and above it the desktop card is nowhere near tight. */
  .deck__card--front .btn { white-space: normal; text-align: center; }

  .work__grid { grid-template-columns: 1fr; }
}

/* =========================================================
   05 · WHY SYNC
   ========================================================= */
.why {
  background: var(--blue-note);
  padding: var(--section-y) var(--gutter);
}
.why__lede { margin: clamp(1rem, 2vw, 2rem) 0 clamp(1.5rem, 3vw, 3rem); }
.why__lede p {
  font-family: var(--font-head);                /* CHANEY Regular */
  font-size: clamp(.85rem, 1.458vw, 1.75rem);   /* 28 @1920 */
  text-transform: uppercase;
  line-height: .88;
  margin-top: .35rem;
}

/* frame 198-193 — five 273x250 cards, 55 apart */
.why__cards {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: clamp(.9rem, 2.865vw, 3.4375rem);        /* 55 @1920 */
}
.card {
  background: transparent;
  border: 1px solid var(--white);
  border-radius: clamp(8px, .625vw, 12px);      /* 12 @1920 */
  padding: clamp(.8rem, 1.25vw, 1.5rem) clamp(.7rem, 1.094vw, 1.3125rem);
  min-height: clamp(180px, 13.02vw, 15.625rem); /* 250 @1920 */
  /* The card is a plain vertical stack in the frame: heading, a 30 gap, then
     the copy. space-between pushed the copy onto the card's floor instead, so
     where a heading ran to two lines rather than three the copy dropped with
     it and the row lost its line. */
  display: flex; flex-direction: column; justify-content: flex-start;
  gap: clamp(.9rem, 1.5625vw, 1.875rem);        /* 30 @1920 */
  color: var(--white);
}
/* only the hovered card goes full-bleed white */
.card:hover, .card:focus-within {
  background: var(--white);
  border-color: var(--white);
  color: var(--blue-note);
}
/* Scoped past [data-reveal]. The card carries that attribute, and its
   transition shorthand — written later in this file at equal specificity — was
   replacing the card's own, so the inversion had been snapping rather than
   easing. Restating the reveal's opacity and translate here keeps the entrance
   intact. */
.why__cards .card {
  transition: background-color .3s var(--ease), color .3s var(--ease),
              border-color .3s var(--ease),
              opacity .7s var(--ease), translate .7s var(--ease);
}
.card h3 {
  /* CHANEY Regular, per frame 198-193 — it was set in Ultra Extended. Size and
     leading were already right: 20 on 18 at 1920, which is the .9 below.

     The frame's letter-spacing reads -50%, which is the same unusable number
     this panel has given before (2000% and 6600% elsewhere in the file). Half of
     20px would lay every glyph across the one before it, so the tracking here is
     left alone — at this size it is a fifth of a pixel either way. */
  font-family: var(--font-head);               /* CHANEY Regular */
  font-size: clamp(.85rem, 1.04vw, 1.25rem);   /* 20 @1920 */
  line-height: .9;                             /* 18 on 20 */
  letter-spacing: -.01em;
  text-transform: uppercase;
  font-weight: 400;
}
/* Inconsolata Bold at the same 20px as the card title in the frame */
.card p {
  font-family: var(--font-mono);
  font-weight: 700;                            /* Inconsolata Bold */
  font-size: clamp(.85rem, 1.04vw, 1.25rem);   /* 20 @1920 */
  line-height: .8;                             /* 16 on 20 */
  text-transform: uppercase;
  letter-spacing: .02em;
}

@media (max-width: 1100px) { .why__cards { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 640px)  { .why__cards { grid-template-columns: 1fr; } .card { min-height: 0; } }

/* =========================================================
   06 · SERVICES
   ========================================================= */
.services {
  background: var(--blue-note);
  padding: var(--section-y) var(--gutter);
  overflow: hidden;
}
.services__title { margin: .5rem 0 clamp(2rem, 4vw, 4.5rem); }

.services__body {
  display: grid;
  /* 2:1 puts the deck at the 1101 of 1728 it occupies in the frame */
  grid-template-columns: minmax(220px, 1fr) 2fr;
  gap: clamp(2rem, 4vw, 4rem);
  align-items: center;
}
.services__list ul { margin-top: .5rem; }
/* each line drives the deck — see main.js */
/* fit-content, or the box runs the full width of the column and the swap fires
   from empty space well to the right of the words */
.services__list li[data-service] {
  width: fit-content;
  cursor: pointer;
  transition: color .2s var(--ease);
}
.services__list li[data-service]:hover,
.services__list li[data-service]:focus-visible { color: var(--record-red); }
.services__list li {
  font-family: var(--font-head);                /* CHANEY Regular */
  font-size: clamp(.85rem, 1.458vw, 1.75rem);   /* 28 @1920 */
  text-transform: uppercase;
  line-height: .88;
}

/* --- card deck (frame 144-15) -------------------------------------------
   Cards are 440 squares in the frame: r20, 24 padding, 16 gap. Every measure
   below is a fraction of --card so the whole thing scales as one piece; the
   old mix of independent clamps let the type and the box drift apart.
   The fan measures 1101 x 619 around that card, and .deck is exactly those
   bounds, so no rotated corner falls outside and gets cut off. */
.deck {
  position: relative;
  width: 100%;
  aspect-ratio: 1101 / 619;
  /* The card is a fraction of the deck, not of the viewport: sized off vw it
     ignores the gutters and the fan's right edge lands under the section's
     overflow clip on a narrow screen. */
  container-type: inline-size;
  /* vw first, cqw only where the unit exists. An unsupported cqw makes the
     whole min() invalid, --card resolves to nothing and every card in the fan
     collapses — the one thing here that breaks rather than degrades. The vw
     figure is the same size at the design width; it just stops tracking the
     column exactly on browsers before ~2023. */
  --card: min(440px, 22.9vw);
  --card: min(440px, 39.96cqw);                   /* 440 of 1101 */
}
.deck__card {
  position: absolute;
  left: 50%; top: 50%;
  width: var(--card); height: var(--card);
  border-radius: calc(var(--card) * .0455);       /* 20 */
  padding: calc(var(--card) * .0545);             /* 24 */
  display: flex; flex-direction: column;
  gap: calc(var(--card) * .0364);                 /* 16 */
  transition: transform .45s var(--ease), box-shadow .45s var(--ease);
  box-shadow: 0 18px 40px rgba(0,0,0,.25);
}
/* Colour and tab placement only — which slot a card sits in comes from the
   [data-slot] rules further down, and the tab travels with the card. */
/* --tab-l is the tab's left edge as a fraction of the card, read off each
   card's selection outline in the frame. Figma's panel reports a Left for
   these nodes that is not card-relative — trusting it put two tabs off the
   right edge entirely. The tab is .6477 wide and has to stop short of the
   .0455 corner radius as well, or its end overhangs the curve with no card
   under it and reads as lifting off the edge — so .3068 is the true limit. */
.deck__card--red   { background: #BA0400; --tab: #F84200; --tab-l: .30; }
/* pushed as far right as it goes: the card in front covers this one's left
   third, and anything less buries the label behind it */
.deck__card--steel { background: #3498DB; --tab: #1E5B84; --tab-l: .3068; }
.deck__card--blue  { background: #0045D7; --tab: #1E67FF; --tab-l: .278; }
.deck__card--front { background: var(--record-red); --tab: #9F2C16; --tab-l: .286; }

/* The label is a folder tab riding on the card's top edge. It stays in the flow
   and is pulled up by margin rather than being positioned out of it: absolute
   reserves no space, and the title then ran underneath the tab. */
.deck__tab {
  align-self: flex-start;
  /* A flex item shrinks when the column overflows, and this one is pinned to
     the card's top edge — losing 1.7px opened a gap between tab and card on
     whichever card had the tallest copy. */
  flex: none;
  /* The tab clears the card's top edge completely — .0864 is its own height,
     on top of the .0545 padding it has to escape. */
  margin-top: calc(var(--card) * -.1409);
  margin-left: calc(var(--card) * (min(var(--tab-l), .3068) - .0545));
  /* The tab ends flush on the card's top edge, so the flow has to be pushed
     back down: the frame starts the title at 77 of 440, well below the 24
     padding, which is the air the cards read with. */
  margin-bottom: calc(var(--card) * .1386);
  width: calc(var(--card) * .6477);               /* 285 */
  height: calc(var(--card) * .0864);              /* 38 */
  display: grid; place-items: center;
  background: var(--tab);
  border-radius: calc(var(--card) * .0273) calc(var(--card) * .0273) 0 0;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: calc(var(--card) * .0409);           /* 18 */
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: .2em;
  /* letter-spacing trails the last glyph too, so a centred line sits half a
     space left of true centre — indent by that half, not the whole */
  text-indent: .1em;
  white-space: nowrap;
}
.deck__card h3 {
  font-family: var(--font-head);                  /* CHANEY Regular */
  font-weight: 400;
  font-size: calc(var(--card) * .0909);           /* 40 */
  line-height: .88;
  text-transform: uppercase;
}
.deck__card p {
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: calc(var(--card) * .0455);           /* 20 */
  line-height: 1;
  /* .2em in the frame, but at that tracking "From first brief to final cut."
     measures 398 against the 371 the card has and drops to two lines. .14em
     brings it to 364. The frame wraps it too; this is the designer asking for
     one line. */
  letter-spacing: .14em;
  text-transform: uppercase;
}
.deck__no {
  font-family: var(--font-ext);                   /* CHANEY Extended */
  font-size: calc(var(--card) * .1818);           /* 80 */
  line-height: 1.6;                               /* 128 */
  margin-top: auto;
  align-self: flex-end;
}
.deck__card--front h3 {
  font-family: var(--font-wide);                  /* CHANEY Wide */
  font-size: calc(var(--card) * .1545);           /* 68 */
  /* paired with the auto below, this splits the free space and lands the
     headline on the card's centre, where the frame puts it */
  margin-top: auto;
}
/* Not in the frame, which ends at the headline — kept as the only way into the
   brief from this section. */
.deck__card--front .btn { margin-top: auto; align-self: flex-start; }

@media (max-width: 900px) {
  .services__body { grid-template-columns: 1fr; }
  /* The frame's fan is 2.5 cards wide, which on one column would shrink the
     card until the 40px title fell under 13px. Pulling the spread in to 1.9
     buys back about half the card size; the box is retuned to the tighter
     bounds so the corners still stay inside it. */
  .deck {
    aspect-ratio: 1.858 / 1.413;
    --card: min(440px, 48vw);
    --card: min(440px, 53.82cqw);
  }
  /* Offsets are centred on the fan's own bounds, not eyeballed: the front card
     reaches 74% below centre against the back cards' 67% above, so every y is
     lifted by half that difference or it hangs out of the bottom of the box. */
  .deck__card[data-slot="0"] { --slot: translate(-33.6%, -11.4%) rotate(-12deg); }
  .deck__card[data-slot="1"] { --slot: translate(4.4%,    -5.4%) rotate(-6deg); }
  .deck__card[data-slot="2"] { --slot: translate(36.4%,  -11.4%) rotate(8deg); }
  .deck__card[data-slot="3"] { --slot: translate(-5.6%,   20.6%); }
}

/* =========================================================
   07 · HOW WE WORK
   ========================================================= */
.process {
  background: var(--blue-note);
  padding: var(--section-y) var(--gutter);
}
/* right, in line with the (our_process) label beneath it */
.process__title { text-align: right; }
.process__lede { text-align: right; margin: clamp(.5rem, 1vw, 1rem) 0 clamp(2rem, 4vw, 4.5rem); }
.process__lede p {
  font-family: var(--font-head);
  font-size: clamp(1rem, 1.46vw, 1.75rem);
  text-transform: uppercase;
  margin-top: .25rem;
}

.steps {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(1rem, 2vw, 2.5rem);
  padding-top: clamp(2rem, 3vw, 3.5rem);
}
/* The red timeline draws left-to-right; each step is revealed as the line
   reaches it, and the arrowhead lands once the rail is fully out. */
.steps::before {
  content: "";
  position: absolute; top: clamp(2rem, 3vw, 3.5rem); left: 0; right: 0;
  height: 2px; background: var(--record-red);
  transform: translateY(calc(clamp(28px, 2.6vw, 44px) / 2)) scaleX(0);
  transform-origin: left center;
  transition: transform 1.6s cubic-bezier(0.4, 0, 0.15, 1);
}
.steps.is-drawn::before {
  transform: translateY(calc(clamp(28px, 2.6vw, 44px) / 2)) scaleX(1);
}
.steps::after {
  content: "";
  position: absolute; top: clamp(2rem, 3vw, 3.5rem); right: 0;
  transform: translateY(calc(clamp(28px, 2.6vw, 44px) / 2 - 6px));
  border-left: 12px solid var(--record-red);
  border-top: 7px solid transparent;
  border-bottom: 7px solid transparent;
  opacity: 0;
  transition: opacity .25s var(--ease) 1.5s;
}
.steps.is-drawn::after { opacity: 1; }

.step {
  position: relative;
  opacity: 0;
  translate: 0 12px;
  transition: opacity .5s var(--ease), translate .5s var(--ease);
}
.steps.is-drawn .step { opacity: 1; translate: 0 0; }
.steps.is-drawn .step:nth-child(1) { transition-delay: .18s; }
.steps.is-drawn .step:nth-child(2) { transition-delay: .52s; }
.steps.is-drawn .step:nth-child(3) { transition-delay: .86s; }
.steps.is-drawn .step:nth-child(4) { transition-delay: 1.2s; }

@media (prefers-reduced-motion: reduce) {
  .steps::before { transform: translateY(calc(clamp(28px, 2.6vw, 44px) / 2)) scaleX(1); }
  .steps::after { opacity: 1; }
  .step { opacity: 1; translate: none; }
}
.step__icon {
  position: relative; z-index: 1;
  display: grid; place-items: center;
  width: clamp(28px, 2.6vw, 44px); aspect-ratio: 1;
  background: var(--record-red);
  border-radius: 50%;
  margin-bottom: clamp(1rem, 2vw, 2.4rem);
}
.step__icon { color: var(--blue-note); }
.step__icon svg { width: 58%; height: 58%; stroke-width: 1.4; }
/* frame 233-192 */
.step h3 {
  font-family: var(--font-ext);                    /* CHANEY Extended */
  font-size: clamp(1.1rem, 1.875vw, 2.25rem);      /* 36 @1920 */
  text-transform: uppercase; font-weight: 400;
  line-height: 1.32;
  letter-spacing: -.02em;
  margin-bottom: .5rem;
}
.step p {
  font-family: var(--font-mono);
  font-weight: 700;                                /* Inconsolata Bold */
  font-size: clamp(.85rem, 1.042vw, 1.25rem);       /* 20 @1920 */
  line-height: .95;                                /* 19 on 20 */
  letter-spacing: 0;
  text-transform: uppercase;
}

@media (max-width: 900px) {
  .steps { grid-template-columns: 1fr 1fr; }
  .steps::before, .steps::after { display: none; }
}
@media (max-width: 560px) { .steps { grid-template-columns: 1fr; } }

/* =========================================================
   08 · TEAM
   One member now: portrait left, name/role/bio always visible on the right.
   ========================================================= */
.team {
  background: var(--blue-note);
  padding: var(--section-y) var(--gutter);
}
.team__title { margin-bottom: clamp(2rem, 4vw, 4.5rem); }

/* the block sits centred in the section rather than hard against the left
   gutter, matching the Figma frame */
.member {
  display: grid;
  grid-template-columns: clamp(220px, 27vw, 515px) minmax(0, 1fr);
  gap: clamp(1.5rem, 2.4vw, 3rem);
  align-items: start;
  max-width: 1380px;
  margin-inline: auto;
}

.member__photo {
  aspect-ratio: 4 / 5;
  background: var(--img, linear-gradient(160deg, #d8d8d8, #a9a9a9));
  background-size: cover; background-position: center top;
  /* parallax offset, set from JS */
  transform: translateY(var(--drift, 0px));
  will-change: transform;
}

/* frame 145-359 */
.member__name {
  font-family: var(--font-head);                   /* CHANEY Regular */
  font-size: clamp(1.4rem, 2.5vw, 3rem);           /* 48 @1920 */
  line-height: .82;
  letter-spacing: 0;
  text-transform: uppercase;
  font-weight: 400;
}
.member__role {
  font-family: var(--font-mono);
  font-weight: 700;                                /* Inconsolata Bold */
  font-size: clamp(.95rem, 1.875vw, 2.25rem);      /* 36 @1920 */
  line-height: 1;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--record-red);
  margin-top: .2rem;
}

.member__bio {
  margin-top: clamp(2rem, 5vw, 5.5rem);
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: clamp(.85rem, 1.667vw, 2rem);         /* 32 @1920 */
  line-height: .875;                               /* 28 on 32 */
  letter-spacing: 0;
  text-transform: uppercase;
  /* the frame sets an 818 box, not a character count */
  max-width: min(100%, 42.6vw);
}
.member__bio p + p { margin-top: 1.4em; }

/* Underscore caret: rides whichever paragraph is being typed, then stays
   blinking at the end of the last one rather than disappearing. */
.member__bio p.has-caret::after {
  content: "";
  display: inline-block;
  width: .62em; height: .1em;
  background: var(--white);
  position: relative;
  top: .1em;
  margin-left: .12em;
  animation: caret .9s steps(1, end) infinite;
}
@keyframes caret { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

@media (max-width: 900px) {
  .member { grid-template-columns: 1fr; }
  .member__bio { margin-top: 1.5rem; }
}

@media (prefers-reduced-motion: reduce) {
  .member__photo { transform: none; }
  .member__bio p.has-caret::after { display: none; }
}

/* =========================================================
   09 · PLAYLISTS
   Record Red field; a Blue Note band carries the outlined title, the vinyl
   bleeds off the left edge over it, and the player sits on the red.
   ========================================================= */
.playlists {
  position: relative;
  background: var(--record-red);
  /* 1920 x 822 in the Figma frame — holding the ratio keeps the vinyl in
     proportion instead of overflowing a short section */
  min-height: min(42vw, 822px);
  padding: clamp(2.5rem, 5vw, 6rem) var(--gutter);
  display: grid;
  /* The band takes whatever the track list leaves, and in the frame it runs to
     78% of the width — so the list column is pulled in to give it back. */
  grid-template-columns: minmax(0, 1fr) clamp(240px, 19vw, 365px);
  gap: clamp(1rem, 1.8vw, 2.2rem);
  align-items: center;
  overflow: hidden;
}

/* --- the band --- */
.playlists__band {
  position: relative;
  z-index: 1;
  background: var(--blue-note);
  /* runs off the left edge of the section */
  margin-left: calc(-1 * var(--gutter));
  padding: clamp(1.5rem, 3vw, 3.2rem) clamp(1.25rem, 2.5vw, 3rem);
  text-align: right;
}

/* frame 85-5442 */
.playlists__title {
  font-family: var(--font-ext);                    /* CHANEY Extended */
  font-size: clamp(2.2rem, 6.25vw, 7.5rem);        /* 120 @1920 */
  line-height: .79;
  letter-spacing: 0;
  text-transform: uppercase;
  font-weight: 400;
  color: transparent;
  -webkit-text-stroke: clamp(.75px, .104vw, 2px) var(--white);
}
.playlists__sub {
  font-family: var(--font-head);                   /* CHANEY Regular */
  font-size: clamp(.8rem, 1.458vw, 1.75rem);       /* 28 @1920 */
  line-height: .88;
  letter-spacing: 0;
  text-transform: uppercase;
  margin-top: clamp(.75rem, 1.4vw, 1.4rem);
}

/* --- the vinyl --- */
.vinyl {
  position: absolute;
  /* the exported SVG pads the disc inside a 900 viewBox (disc is ~79% of it),
     so the box is larger and offset further left than the disc alone would be */
  left: clamp(-340px, -17.5vw, -120px);
  top: 50%;
  translate: 0 -50%;
  width: clamp(380px, 42vw, 810px);
  z-index: 2;
  pointer-events: none;
}
.vinyl svg { width: 100%; height: auto; }

/* The vinyl is the exported Figma SVG, used verbatim — fills, hairline strokes
   and the clip path all come from the file, so nothing here restyles it. */

/* turns only while something is actually playing */
.vinyl svg { transform-origin: 50% 50%; }
/* the animation is always attached and simply paused, so the disc keeps its
   angle between plays instead of snapping back to zero when the class drops */
.vinyl svg { animation: spin 9s linear infinite; animation-play-state: paused; }
.vinyl.is-spinning svg { animation-play-state: running; }
@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .vinyl svg, .vinyl.is-spinning svg { animation: none; }
}

@media (max-width: 900px) {
  .playlists { grid-template-columns: 1fr; }
  .vinyl { display: none; }
  .playlists__band { text-align: left; }
}

/* sits directly on the red field now, no panel behind it */
/* min-width: 0 because .player is a grid item, and a grid item's automatic
   minimum is its min-content width. Once the title stopped wrapping, its
   min-content became the whole title, so a long one blew the track out to
   500px inside a 390px phone — and an ancestor clips the overflow, so the
   player was simply cut off with no scrollbar to reach it. */
.player { position: relative; z-index: 3; min-width: 0; }

.player__head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem;
  margin-bottom: clamp(1.25rem, 2.2vw, 2.2rem);
}
/* min-width: 0 so the title can shrink inside the flex head; without it a long
   title pushes against the volume control instead of clipping. */
.player__head > :first-child { min-width: 0; }
/* One line, clipped. A title that wraps makes the head taller and shoves the
   progress bar, the transport and the whole queue down the page as playback
   moves from a short title to a long one. The full title stays in the DOM for
   screen readers. */
.player__title {
  font-family: var(--font-ext);                 /* CHANEY Extended */
  font-size: clamp(1rem, 1.354vw, 1.625rem);    /* 26 @1920 */
  text-transform: uppercase; font-weight: 400; line-height: 1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The writers, not the word Playlist. Long credit lists run wider than the
   header, so this one scrolls on its own rather than waiting to be hovered —
   it names the track that is playing, and nobody hovers a caption. Same
   mechanism as the queue and the client strip: a second copy travelling
   exactly its own width, added only when the line overflows. */
.player__kind {
  display: flex;
  overflow: hidden;
  font-family: var(--font-ext);                 /* CHANEY Extended */
  font-size: clamp(.7rem, .625vw, .75rem);      /* 12 @1920 */
  text-transform: uppercase;
  line-height: 1;
  margin-top: .35em;
  opacity: .6;
}
.player__kind em { flex: none; font-style: normal; white-space: nowrap; }
/* Paused, not unset: pausing the track freezes the credits where they are and
   pressing play carries on from there, rather than snapping back to the first
   name. Before the first play it simply sits at the start. */
.player__kind[data-scroll] em {
  padding-right: 2.5rem;
  animation: marquee var(--by-dur, 10s) linear infinite;
  animation-play-state: paused;
}
.player.is-playing .player__kind[data-scroll] em { animation-play-state: running; }
.player__kind[data-scroll] {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 1.5rem, #000 calc(100% - 2rem), transparent);
          mask-image: linear-gradient(90deg, transparent, #000 1.5rem, #000 calc(100% - 2rem), transparent);
}
@media (prefers-reduced-motion: reduce) {
  .player__kind[data-scroll] em { animation: none; }
}

/* --- progress: blue played, white remaining, CHANEY timecodes ---
   --fill is registered so it can be transitioned. timeupdate only fires a few
   times a second, so without this the bar advances in visible steps. Declaring
   an initial value also stops the first frame rendering from an unset var. */
/* A number rather than a percentage, because the fill position is now an
   expression mixing px and %, and only the fraction itself can be animated. */
@property --seek {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

@property --fill {
  syntax: '<percentage>';
  /* MUST inherit: the gradient lives on ::-webkit-slider-runnable-track, which
     reads --fill from the input. Registering it as non-inheriting made the
     pseudo-element fall back to the initial 0%, so the bar rendered all white. */
  inherits: true;
  initial-value: 0%;
}

.player__bar { display: flex; align-items: center; gap: clamp(.6rem, 1vw, 1.1rem); }
/* CHANEY has no tabular figure set, so font-variant-numeric is inert here and
   the digits have wildly uneven advances — "1" is .432em against "0" at .803em.
   The timecodes are flex items either side of a flex: 1 progress bar, so every
   tick of the clock resized them and the bar jumped to absorb the difference:
   visibly shorter at 0:00 than at 0:01, and again on every change of track as
   the total went from 2:27 to 3:38.

   Reserving the width of the widest "M:SS" — three "0"s and a colon, 2.729em —
   pins both boxes so the bar can't move. min-width rather than width so a track
   of ten minutes or more still fits, and em so it tracks the clamped size. */
.player__time {
  font-family: var(--font-head);
  font-size: clamp(.75rem, .85vw, 1rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  flex: none;
  min-width: 2.75em;
}
#timeTotal { text-align: right; }

/* The fill ends at the THUMB'S CENTRE, not at a plain percentage of the track.

   A range thumb is inset by half its width at each end so it never hangs off
   the track, so at value v its centre is at 6px + (track - 12px) * v, while a
   percentage of the track is at track * v. Those are not the same place, and
   --seek is eased while the thumb's position is not: for the first seconds the
   lagging fill edge sits to the LEFT of the thumb's left edge and a sliver of
   white track shows between the bar and the circle. It heals itself once the
   thumb's inset grows past the lag — at (track - 12) * .28 / 12 seconds, which
   is five on a narrow player and eight on a wide one, and is exactly the "only
   in the first 8s" of the report.

   Anchored to the centre, the fill edge lands under the circle at every value.
   The easing can lag all it likes; the thumb covers the join. */
.player__progress {
  -webkit-appearance: none; appearance: none;
  flex: 1; height: 14px;
  background: transparent; cursor: pointer; margin: 0;
  --thumb: 12px;
  --fill-at: calc(var(--thumb) / 2 + (100% - var(--thumb)) * var(--seek, 0));
  transition: --seek .28s linear;
}
/* no easing while dragging — the bar should track the pointer exactly */
.player__progress:active { transition: none; }
.player__progress::-webkit-slider-runnable-track {
  height: 4px;
  background: linear-gradient(90deg, var(--blue-note) var(--fill-at), var(--white) var(--fill-at));
}
.player__progress::-moz-range-track {
  height: 4px;
  background: linear-gradient(90deg, var(--blue-note) var(--fill-at), var(--white) var(--fill-at));
}
.player__progress::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 12px; height: 12px; margin-top: -4px;
  background: var(--blue-note); border-radius: 50%; border: 0;
  opacity: 0; transition: opacity .2s var(--ease);
}
.player__progress::-moz-range-thumb {
  width: 12px; height: 12px;
  background: var(--blue-note); border-radius: 50%; border: 0;
  opacity: 0; transition: opacity .2s var(--ease);
}
.player__bar:hover .player__progress::-webkit-slider-thumb,
.player__progress:focus-visible::-webkit-slider-thumb { opacity: 1; }
.player__bar:hover .player__progress::-moz-range-thumb { opacity: 1; }

/* --- transport: outlined glyphs, play in a ring --- */
.player__controls {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  gap: clamp(2rem, 4.5vw, 5rem);
  margin: clamp(1.25rem, 2.4vw, 2.4rem) 0 clamp(1.25rem, 2.2vw, 2rem);
}
.player__btn {
  display: grid; place-items: center;
  padding: 0;
  color: var(--white);
  transition: opacity .2s var(--ease);
}
.player__btn:hover { opacity: .7; }
.player__btn svg { width: clamp(26px, 2.1vw, 38px); height: auto; }

.player__btn--main {
  width: clamp(52px, 4.4vw, 78px); aspect-ratio: 1;
  border: 1.5px solid var(--white);
  border-radius: 50%;
}
.player__btn--main svg { width: 46%; }
.player__glyph-pause { opacity: 0; }
.player__btn--main.is-playing .player__glyph-play { opacity: 0; }
.player__btn--main.is-playing .player__glyph-pause { opacity: 1; }

/* --- volume sits at the end of the transport row --- */
/* Sits in the head row opposite the track title, not in the transport: the
   frame's transport is a symmetric prev/play/next, and pinning a fourth
   control to its right edge put it on top of Next as soon as the column
   narrowed. The slider is taken out of flow and opens leftward into the gap
   beside the title, so hovering never reflows the row. */
.volume {
  position: relative;
  flex: none;
  display: flex; align-items: center;
}
/* Above the chip: ::before is positioned and the button is not, so without this
   the chip paints straight over the icon it is supposed to sit behind. */
.volume__btn { width: 24px; height: 24px; position: relative; z-index: 2; }
.volume__btn svg { width: 22px; }
.volume__cross { opacity: 0; }
.volume__btn.is-muted .volume__waves { opacity: 0; }
.volume__btn.is-muted .volume__cross { opacity: 1; }

/* The slider opens over the title, and deliberately takes no space in flow.

   Opening it in flow was worse: it shortened the title, and on a wide screen
   that reflow fed back into the hover it was triggered by, so the control
   flickered open and shut and the title jittered with it. A control that
   resizes its own neighbours on hover can always do that; this one now changes
   nothing but its own paint.

   The reason it could not simply sit over the title before is that it was
   transparent — a 3px white track and a white thumb laid across white glyphs.
   ::before puts an opaque Record Red chip behind it, fading in from the left so
   the title reads as passing under the control rather than being chopped by it.
   pointer-events: none so the chip cannot alter what counts as hovering the
   volume, which is what has to stay stable. */
.volume__slider {
  position: absolute; right: calc(100% + .5rem); top: 50%; translate: 0 -50%;
  z-index: 2;
  -webkit-appearance: none; appearance: none;
  width: 0; height: 14px; opacity: 0;
  background: transparent; cursor: pointer; margin: 0;
  transition: width .25s var(--ease), opacity .25s var(--ease);
}
.volume::before {
  content: ''; position: absolute; z-index: 1;
  right: -.4rem; top: 50%; translate: 0 -50%;
  height: 1.75rem; width: 0; opacity: 0;
  border-radius: 999px;
  background: linear-gradient(90deg, transparent, var(--record-red) 1.5rem);
  pointer-events: none;
  transition: width .25s var(--ease), opacity .25s var(--ease);
}
.volume:hover::before,
.volume:focus-within::before {
  width: calc(100% + clamp(48px, 4vw, 72px) + 2.2rem);
  opacity: 1;
}
.volume:hover .volume__slider,
.volume:focus-within .volume__slider { width: clamp(48px, 4vw, 72px); opacity: 1; }
.volume__slider::-webkit-slider-runnable-track {
  height: 3px;
  background: linear-gradient(90deg, var(--white) var(--fill, 0%), rgba(255,255,255,.35) var(--fill, 0%));
}
.volume__slider::-moz-range-track {
  height: 3px;
  background: linear-gradient(90deg, var(--white) var(--fill, 0%), rgba(255,255,255,.35) var(--fill, 0%));
}
.volume__slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 10px; height: 10px; margin-top: -3.5px;
  background: var(--white); border-radius: 50%; border: 0;
}
.volume__slider::-moz-range-thumb {
  width: 10px; height: 10px; background: var(--white); border-radius: 50%; border: 0;
}

/* --- queue: CHANEY throughout, dimmed artist, hairline dividers --- */
.tracks { display: grid; }
.tracks__row { border-bottom: 1px solid rgba(255,255,255,.28); }
.tracks__row:first-child { border-top: 1px solid rgba(255,255,255,.28); }
.tracks__row.is-unavailable { opacity: .45; }

.tracks__btn {
  display: grid;
  grid-template-columns: auto auto 1fr auto;
  align-items: center;
  gap: clamp(.6rem, 1.1vw, 1.1rem);
  width: 100%; text-align: left;
  padding: clamp(.5rem, .9vw, .85rem) 0;
  transition: opacity .2s var(--ease);
}
.tracks__btn:not(:disabled):hover { opacity: .75; }
.tracks__btn:disabled { cursor: default; }

/* outlined cue triangle, matching the transport glyphs */
.tracks__cue {
  width: 11px; height: 13px;
  border-left: 1.5px solid currentColor;
  border-top: 1.5px solid transparent;
  border-bottom: 1.5px solid transparent;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  background: currentColor;
}
/* Row title is Extended at 15, the artist line under it is Wide at 13. */
.tracks__no,
.tracks__name,
.tracks__time {
  font-family: var(--font-ext);                 /* CHANEY Extended */
  text-transform: uppercase;
  line-height: 1;
}
.tracks__no   { font-size: clamp(.75rem, .781vw, .9375rem); font-variant-numeric: tabular-nums; }
/* min-width: 0 because a grid item will not shrink below its content without
   it, so neither line below would ever clip or scroll. */
.tracks__name { font-size: clamp(.75rem, .781vw, .9375rem); min-width: 0; }

/* One line, clipped. "New Heights (Full Length) Full Mix" is three lines
   otherwise, and one row at three times the height of its neighbours breaks the
   rhythm of the whole queue. The full title stays in the DOM, in the button's
   aria-label, and in a title attribute so it can still be read when it clips. */
.tracks__title {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Credits. A list too long for the row scrolls rather than clipping, because
   a truncated name credits nobody. Two copies each travel exactly their own
   width, so the loop is the same picture at both ends; the second copy is added
   by script only when the line actually overflows. */
.tracks__by {
  display: flex;
  overflow: hidden;
  margin-top: .3em;
}
.tracks__by em {
  flex: none;
  font-style: normal;
  font-family: var(--font-wide);                /* CHANEY Wide */
  font-size: clamp(.7rem, .677vw, .8125rem);    /* 13 @1920 */
  line-height: 1; opacity: .55;
  white-space: nowrap;
}
.tracks__by[data-scroll] em { padding-right: 2.5rem; }
/* The separator between names, in both the queue and the player header. Only
   slightly smaller — enough to stop a run of them reading as initials. */
.tracks__by .sep,
.player__kind .sep { font-size: .8em; opacity: .75; }
/* Fade rather than cut. A clipped credit ending in a hard vertical edge reads
   as colliding with the timecode beside it; scrolling, it also has to fade
   where it leaves. The right edge is soft whenever the line is too long for the
   row, the left only while it is moving — at rest the line starts there and
   dimming its first letters would be a fade over nothing. */
.tracks__by[data-scroll] {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 2rem), transparent);
          mask-image: linear-gradient(90deg, #000 calc(100% - 2rem), transparent);
}
.tracks__row:hover .tracks__by[data-scroll] {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 1.5rem, #000 calc(100% - 2rem), transparent);
          mask-image: linear-gradient(90deg, transparent, #000 1.5rem, #000 calc(100% - 2rem), transparent);
}
/* Hover only. The playing row used to scroll its credits too, which put a
   second thing in motion in a list you are reading; the header carries the
   playing track's writers now, so the queue has no reason to. */
.tracks__row:hover .tracks__by[data-scroll] em {
  animation: marquee var(--by-dur, 10s) linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .tracks__row:hover .tracks__by[data-scroll] em { animation: none; }
}
/* Duration and equaliser share one grid cell, so the column keeps the width of
   the widest of the two whichever is showing and the rows never shift sideways
   as playback moves down the queue. */
.tracks__time {
  font-size: clamp(.75rem, .781vw, .9375rem);
  font-variant-numeric: tabular-nums;
  display: grid; align-items: center; justify-items: end;
  /* Width of the widest "M:SS" — CHANEY has no tabular figures, so without a
     floor the column is a different width on every row. It also gives the
     equaliser a stable box to sit in the middle of. */
  min-width: 2.75em;
}
/* Centred rather than right-aligned: the bars are half the width of a timecode,
   so hanging them on the right edge reads as pushed out of line with the times
   above and below. */
.tracks__eq { justify-self: center; }
.tracks__time > * { grid-area: 1 / 1; transition: opacity .18s var(--ease); }

.tracks__eq { display: flex; align-items: flex-end; gap: 3px; height: 13px; opacity: 0; }
.tracks__eq i { width: 3px; background: currentColor; }
.tracks__eq i:nth-child(1) { height: 45%; }
.tracks__eq i:nth-child(2) { height: 100%; }
.tracks__eq i:nth-child(3) { height: 65%; }
.tracks__eq i:nth-child(4) { height: 85%; }

.tracks.is-playing .is-current .tracks__eq  { opacity: 1; }
.tracks.is-playing .is-current .tracks__dur { opacity: 0; }
.tracks.is-playing .is-current .tracks__eq i {
  animation: eq .9s ease-in-out infinite alternate;
  transform-origin: 50% 100%;
}
.tracks.is-playing .is-current .tracks__eq i:nth-child(1) { animation-delay: -.2s; }
.tracks.is-playing .is-current .tracks__eq i:nth-child(2) { animation-delay: -.6s; }
.tracks.is-playing .is-current .tracks__eq i:nth-child(3) { animation-delay: -.4s; }
/* Still the marker of which row is live, just held still. */
@media (prefers-reduced-motion: reduce) {
  .tracks.is-playing .is-current .tracks__eq i { animation: none; }
}

.tracks__row { opacity: .85; transition: opacity .2s var(--ease); }
.tracks__row.is-current { opacity: 1; }

/* =========================================================
   10 · CTA + FOOTER
   ========================================================= */
.cta {
  position: relative;
  background: var(--record-red);
  padding: clamp(2.5rem, 5vw, 6rem) var(--gutter) 0;
  overflow: hidden;
}
.cta__title {
  font-family: var(--font-ext);                 /* CHANEY Extended */
  font-size: clamp(1.9rem, 6.25vw, 7.5rem);     /* 120 @1920 */
  line-height: .79;
  letter-spacing: 0;
  text-transform: uppercase;
  text-align: right;
  font-weight: 400;
}
.cta__lede { position: relative; z-index: 2; text-align: right; margin: clamp(1rem, 2vw, 2.5rem) 0 clamp(2rem, 4vw, 4rem); }
.cta__lede p {
  font-family: var(--font-head);                /* CHANEY Regular */
  font-size: clamp(.85rem, 1.458vw, 1.75rem);   /* 28 @1920 */
  text-transform: uppercase; line-height: .88;
  margin-top: .5rem;
}
.cta__kicker { margin-top: 1rem !important; }

/* Diagonal ticker ribbon. In frame 57-4854 it does not run corner to corner:
   at 30 degrees its centreline leaves the right edge 43.7% down and meets the
   bottom 39.1% across. Pivoting on its own bottom-left corner puts that end
   where the frame does; the far end overruns the right edge and .cta clips it.
   `left` is tuned against the centreline, not the corner — the strip's own
   thickness offsets where it actually crosses an edge. */
.ribbon {
  position: absolute;
  /* The pivot is pushed on past the corner, down the strip's own axis, so the
     end cap falls outside the section and .cta cuts it. Sitting the pivot on
     the bottom edge instead left the angled end showing inside the section.
     Moving along the axis does not move the line, so the crossings hold. */
  left: 30%; right: -18%;
  bottom: -7.5vw;
  background: var(--blue-note);
  padding: clamp(.5rem, .9vw, 1rem) 0;
  transform-origin: 0 100%;
  transform: rotate(-30deg);
  display: flex;
  overflow: hidden;
  z-index: 1; /* sits over the outline wordmark, under the contact details */
}
.ribbon__track {
  display: flex; align-items: center;
  flex: none;
  animation: marquee 26s linear infinite;
}
.ribbon:hover .ribbon__track { animation-play-state: paused; }
/* frame 163-2537 */
.ribbon__track span {
  font-family: var(--font-ext);                    /* CHANEY Extended */
  font-weight: 400;
  font-size: clamp(.95rem, 2.083vw, 2.5rem);       /* 40 @1920 */
  line-height: 1;
  letter-spacing: 0;
  text-transform: uppercase;
  white-space: nowrap;
}
.ribbon__track i { font-style: normal; font-size: .7em; }
.ribbon__track > * { margin-right: clamp(1rem, 2vw, 2.5rem); }

/* footer */
.footer {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2rem;
  padding: clamp(4rem, 12vw, 12rem) 0 0;
}
.footer__contact, .footer__legal { position: relative; z-index: 2; }
.footer__contact a {
  /* block would stretch the box the full width of the column, so the hover
     state lit up under the ribbon and well past the end of the text */
  display: block;
  width: fit-content;
  font-family: var(--font-head);                /* CHANEY Regular */
  font-size: clamp(.85rem, 1.458vw, 1.75rem);   /* 28 @1920 */
  line-height: .88;
  text-transform: uppercase;
  margin-top: .35rem;
  transition: color .2s var(--ease);
}
.footer__contact a:hover, .footer__legal a:hover { color: var(--blue-note); }
/* The ribbon runs through this corner on its way out of the right edge, so the
   legal block drops below it. Translated rather than pushed with margin: the
   ribbon is anchored to the section's bottom, so growing the section moves the
   ribbon down by the same amount and the block never catches up. */
.footer__legal { text-align: right; transform: translateY(clamp(2.5rem, 4.5vw, 5.5rem)); }
.footer__legal a {
  display: block;
  width: fit-content;
  margin-left: auto;              /* keeps the shrink-wrapped box right-aligned */
  font-family: var(--font-head);                /* CHANEY Regular */
  font-size: clamp(.85rem, 1.458vw, 1.75rem);   /* 28 @1920 */
  line-height: .88;
  text-transform: uppercase;
  margin-top: .35rem;
}

.footer__wordmark {
  position: relative;
  z-index: 0;
  grid-column: 1 / -1;
  font-family: var(--font-display);             /* CHANEY Ultra Extended */
  font-size: clamp(2.5rem, 8.333vw, 10rem);     /* 160 @1920 */
  line-height: .79;
  letter-spacing: 0;
  text-transform: uppercase;
  color: transparent;
  -webkit-text-stroke: clamp(.75px, .104vw, 2px) var(--blue-note);
  /* The frame starts it at -25 of 1920, past the section's left edge, and lets
     it overshoot on the right; .cta clips both ends. Sitting on the gutter
     instead is what left the gap down the left-hand side. */
  margin-left: calc(-1 * var(--gutter) - 1.3vw);
  margin-right: calc(-1 * var(--gutter));
  margin-top: clamp(1rem, 3vw, 3rem);
  transform: translateY(.08em);
  user-select: none;
}

@media (max-width: 768px) {
  .footer { grid-template-columns: 1fr; }
  /* stacked under the contact block, so it is already clear of the ribbon and
     the desktop offset would just read as a gap */
  .footer__legal { text-align: left; transform: none; margin-top: 2rem; }
  /* the shrink-wrapped boxes are pushed right by margin-left:auto so the legal
     column stays right-aligned on desktop; in one column that shoves them off
     the side of the screen */
  .footer__legal a { margin-left: 0; }

  /* These two labels had no gap of their own — they were sitting on the first
     link's .35rem top margin, which is the spacing between one link and the
     next, not the spacing under a heading. (09_let's collaborate) gets .5rem
     from its paragraph and reads as a label above a block; these read as the
     first line of the list. So the gap goes on the label where it belongs, and
     the first link leaves the stack so the two don't add up — .5rem under both,
     the same as the label above the CTA. */
  .footer__contact .label,
  .footer__legal .label { margin-bottom: .5rem; }
  .footer__contact a:first-of-type,
  .footer__legal a:first-of-type { margin-top: 0; }

  /* The ribbon is absolutely positioned at a fixed offset, which only works
     while the copy above it is a known height. At this width the title and lede
     are far taller and the band cuts straight through them, so here it goes
     back into the flow — it already sits between the lede and the footer in the
     markup, so it lands where it belongs. The negative inline margin keeps it
     bleeding off both edges, and .cta's overflow:hidden clips it. */
  .ribbon {
    position: relative;
    /* .ribbon is an <a>. Absolute positioning was blockifying it; taking that
       away drops it back to display:inline, where the padding, the width and
       the negative margins all stop doing anything. flex rather than block: it
       blockifies the same way, and the ticker's copies are laid side by side by
       the flex container — as plain block they stacked, and the band came out
       three deep. */
    display: flex;
    /* All four, not just three. The desktop rule pins the band to the section's
       floor with `bottom` and swings it about its own bottom-left corner; left
       alone here, relative positioning kept the offset and the pivot threw the
       band up across the title. That is what was cutting through the copy. */
    left: auto; right: auto; top: auto; bottom: auto;
    transform-origin: 50% 50%;
    /* 30deg is a full-width diagonal; in one narrow column it is nearly
       vertical. Shallower here, and the band spans the width instead. */
    transform: rotate(-12deg);
    /* A rotated box still occupies its unrotated height in layout, so the
       corners overhang: (width x sin12)/2, about 47px at 390 against a band
       128% of the column. The vertical margin has to clear that or it cuts
       into the lede again. */
    margin: clamp(4.5rem, 17vw, 8rem) -14%;
  }
}

/* ---------- Scroll reveal ---------- */
/* uses the standalone `translate` property so it composes with, rather than
   overrides, layout transforms like .work__title and .member--raised */
[data-reveal] { opacity: 0; translate: 0 28px; transition: opacity .7s var(--ease), translate .7s var(--ease); }
[data-reveal].is-in { opacity: 1; translate: 0 0; }

/* =========================================================
   LOADING SCREEN
   Full-bleed Blue Note plate with the word in Record Red, matching the
   "Loading Screen (Blue Note)" frame. Sits over everything, then wipes up.
   ========================================================= */
.loader {
  position: fixed; inset: 0; z-index: 300;
  background: var(--blue-note);
  display: grid; place-items: center;
  overflow: hidden;
  transition: transform .8s cubic-bezier(0.76, 0, 0.24, 1),
              opacity .5s var(--ease) .3s;
}
.loader.is-done {
  transform: translateY(-100%);
  opacity: 0;
  pointer-events: none;
}

.loader__word {
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 11vw, 13rem);
  line-height: 1;
  letter-spacing: -.01em;
  text-transform: uppercase;
  color: var(--record-red);
  white-space: nowrap;
  /* the word itself eases up on entry so the plate never sits static */
  animation: loaderWord .9s cubic-bezier(0.16, 0.84, 0.28, 1) both;
}
@keyframes loaderWord {
  from { opacity: 0; transform: translateY(.18em); }
  to   { opacity: 1; transform: translateY(0); }
}

/* thin progress hairline under the word */
.loader::after {
  content: "";
  position: absolute; left: 0; bottom: 0;
  height: 3px; width: 100%;
  background: var(--record-red);
  transform-origin: left center;
  transform: scaleX(0);
  animation: loaderBar var(--loader-ms, 2400ms) cubic-bezier(0.3, 0, 0.2, 1) forwards;
}
@keyframes loaderBar { to { transform: scaleX(1); } }

/* while the loader is up, the page underneath must not scroll */
body.is-loading { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .loader__word { animation: none; }
  .loader::after { animation: none; transform: scaleX(1); }
}

/* =========================================================
   SERVICES DECK — hovering a back card shuffles it to the front
   Slot transforms live here; JS only reassigns which card holds which slot.
   ========================================================= */
/* Offsets are each card's centre in the frame measured from the fan's centre,
   as a fraction of the 440 card. translate(-50%,-50%) parks the card on the
   deck centre first, so the slot transform is purely the frame's own offset. */
.deck__card {
  transform: translate(-50%, -50%) var(--slot);
  cursor: pointer;
}
.deck__card[data-slot="0"] { --slot: translate(-64%,   -9.2%) rotate(-15deg); z-index: 3; }
.deck__card[data-slot="1"] { --slot: translate(5.6%,   -1.5%) rotate(-8deg);  z-index: 2; }
.deck__card[data-slot="2"] { --slot: translate(67.1%,  -9.2%) rotate(10deg);  z-index: 1; }
.deck__card[data-slot="3"] { --slot: translate(-10.8%, 20.3%);                z-index: 4; }

/* Hover must NOT move the card: it only says the card can be clicked. Moving it
   would pull it out from under the cursor, which un-hovers it, which snaps it
   back — the card oscillates and the deck reshuffles under you. Position
   changes come from a click alone. */
.deck__card:hover { filter: brightness(1.07); }
.deck__card[data-slot="3"] { cursor: default; }

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

/* =========================================================
   HERO PLATE
   The whole film is the click target now, and the cursor carries the play glyph
   instead of a tag sitting on the artwork. A data-URI cursor rather than a
   JS-driven follower: no per-frame work and nothing to fall out of sync.
   ========================================================= */
.hero__open {
  position: absolute;
  inset: 0;
  z-index: 4;                 /* above .hero__wrap, so the whole plate is the target */
  background: none;
  /* takes the cursor from the root — see the rule below */
  cursor: inherit;
}
/* The play glyph lives on the root rather than on the plate itself. A browser
   only re-resolves the cursor when the pointer moves, so scrolling the plate
   out from under a still pointer (trackpad, wheel) left the glyph showing over
   the rest of the page. JS toggles this class from the pointer's last known
   position, and restyling the root forces the resolve without a mousemove. */
html.is-over-reel {
  cursor: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22128%22%20height%3D%2228%22%20viewBox%3D%220%200%20151%2033%22%3E%3Crect%20width%3D%22151%22%20height%3D%2222%22%20transform%3D%22translate%280%208%29%22%20fill%3D%22%23FF3733%22%2F%3E%3Cpath%20d%3D%22M48.304%2033V3.096H58.48C60.816%203.096%2062.72%203.496%2064.192%204.296C65.696%205.096%2066.8%206.152%2067.504%207.464C68.24%208.776%2068.608%2010.248%2068.608%2011.88C68.608%2013.064%2068.4%2014.184%2067.984%2015.24C67.6%2016.264%2066.992%2017.176%2066.16%2017.976C65.36%2018.744%2064.336%2019.352%2063.088%2019.8C61.872%2020.248%2060.416%2020.472%2058.72%2020.472H53.824V33H48.304ZM53.824%2015.96H58.336C59.36%2015.96%2060.224%2015.784%2060.928%2015.432C61.632%2015.08%2062.16%2014.6%2062.512%2013.992C62.864%2013.384%2063.04%2012.68%2063.04%2011.88C63.04%2011.08%2062.864%2010.376%2062.512%209.768C62.16%209.16%2061.632%208.68%2060.928%208.328C60.256%207.976%2059.408%207.8%2058.384%207.8H53.824V15.96ZM72.736%203.096H78.16H78.64V3.528C78.448%203.72%2078.32%203.928%2078.256%204.152C78.224%204.376%2078.208%204.76%2078.208%205.304V28.584H91.936V33H72.736V3.096ZM111.616%2026.04H99.904L100.672%2022.152H110.704L111.616%2026.04ZM106%2012.936L99.952%2033H94.576L104.704%202.904H107.296L117.424%2033H111.952L106%2012.936ZM127.744%2033V21.192L118.816%203.096H124.384L130.576%2015.576L136.048%203.096H141.376L133.168%2021.24V33H127.744Z%22%20fill%3D%22white%22%2F%3E%3Cpath%20d%3D%22M36.3549%2017.9558L10.6881%205.1225C10.3265%204.94285%209.8983%204.9615%209.55415%205.17385C9.21%205.38735%209%205.76185%209%206.16664L9%2031.8333C9%2032.2381%209.21%2032.6126%209.55415%2032.8261C9.7408%2032.9416%209.95315%2033%2010.1666%2033C10.3451%2033%2010.5236%2032.9591%2010.6881%2032.8775L36.3548%2020.0441C36.7504%2019.8458%2037%2019.4421%2037%2019C37%2018.5578%2036.7504%2018.1541%2036.3549%2017.9558Z%22%20fill%3D%22white%22%2F%3E%3C%2Fsvg%3E") 20 16, pointer;
}
/* No hover to carry a cursor: the plate shows a real play tag instead. */
.hero__playtag { display: none; }
/* ---------- touch targets ----------
   Scoped to coarse pointers, so none of it can reach the desktop layout. The
   padding grows the hitbox and an equal negative margin cancels it, which
   keeps every one of these sitting exactly where it does now — the same trick
   the burger uses. 44px is the size a finger actually needs. */
@media (pointer: coarse) {
  .tracks__btn { min-height: 44px; }
  .player__btn { min-width: 44px; min-height: 44px; }
  .volume__btn { min-width: 44px; min-height: 44px; }
  /* NOT these two: .vplayer__play is the triangle itself, cut out of its own
     box with clip-path, so a 44px box draws a 44px triangle. The bar grows
     instead and the glyphs come up with it. */
  .vplayer__bar { height: 44px; }
  .vplayer__play { width: 18px; height: 21px; }
  .vplayer__mute { width: 26px; height: 26px; }
  .vplayer__mute svg { width: 24px; height: 24px; }
  .vplayer__full { width: 26px; height: 26px; }
  .vplayer__full svg { width: 23px; height: 23px; }
  .dock__toggle, .dock__eq { min-width: 44px; }
  /* inline links in running text: hitbox only, position unchanged */
  .footer__contact a, .footer__legal a, .brief__contact a, .menu__socials a {
    padding-block: 10px; margin-block: -10px;
  }
  .cookies__text a, .brief__legal a { padding-block: 8px; margin-block: -8px; }

  /* The first link in each footer block leaves the stack so the label's own
     .5rem is the whole gap — but here that link is carrying 10px of hitbox
     padding above its text, and the two added up to twice the gap the CTA
     label gets. Pull it back by the padding, exactly as margin-block does for
     the links below, and both pointer types land on the same .5rem. */
  .footer__contact a:first-of-type,
  .footer__legal a:first-of-type { margin-top: -10px; }
}

@media (hover: none), (pointer: coarse) {
  html.is-over-reel { cursor: pointer; }
  .hero__open { cursor: pointer; }
  /* In the flow directly under the headline, so it can never drift into the
     copy: .hero is justify-content: flex-end and the copy's height moves with
     the viewport, which is what made every absolute offset unreliable. */
  .hero__playtag {
    display: block;
    width: clamp(112px, 34vw, 151px); height: auto;
    margin: clamp(1rem, 5vw, 2rem) 0 clamp(.85rem, 3.5vw, 1.5rem);
    filter: drop-shadow(0 2px 10px rgba(0,0,0,.35));
  }
  .hero__open:active .hero__playtag { scale: .96; }
}
/* Nothing in the hero should take the cursor off the plate — the copy sits
   under it, and .hero itself is padded, so the padding band round the edges
   fell outside every child and showed the default arrow. */
.hero__wrap { position: relative; z-index: 3; pointer-events: none; }
.hero { cursor: inherit; }

/* =========================================================
   REEL PLAY — the red "▶ PLAY" tag revealed over the showreel
   ========================================================= */
.hero__media { pointer-events: auto; }

/* Sits at 62% of the media plate rather than dead centre so it clears the last
   headline line. The plate starts clamp(120px, 18vw, 343px) down and the tag is
   now a child of .hero, so that offset has to be added back: 62% of (H - T)
   measured from the top of the hero is 62% of H plus 38% of T. */

/* =========================================================
   VIDEO PLAYER (modal)
   ========================================================= */
/* The picture is not part of the modal's fade: main.js runs a FLIP on it so it
   grows out of the thumbnail that opened it and shrinks back into it on close.
   That only reads if the video stays fully opaque throughout, so the black
   ground moved to a ::before and the chrome fades on its own. */
.vplayer {
  position: fixed; inset: 0; z-index: 200;
  display: grid;
}
.vplayer[hidden] { display: none; }

.vplayer::before {
  content: "";
  position: absolute; inset: 0;
  background: #000;
  opacity: 0;
  transition: opacity .42s var(--ease);
}
.vplayer.is-open::before { opacity: 1; }

/* Scrim under the caption and transport. What sits behind them is whichever
   frame is on screen, so white type had nothing to hold it — a text-shadow
   alone loses on a bright shot. Sits over the film (z 1) and under the chrome
   (z 3). */
.vplayer::after {
  content: "";
  position: absolute; inset: auto 0 0 0;
  z-index: 2;
  /* Tall enough that the caption's top line sits inside the dark part of the
     ramp, not on its transparent edge — the kicker is the smallest type here
     and the first thing to disappear into a bright frame. */
  height: clamp(13rem, 34vh, 24rem);
  background: linear-gradient(180deg,
      rgba(0,0,0,0) 0%, rgba(0,0,0,.30) 46%, rgba(0,0,0,.62) 74%, rgba(0,0,0,.82) 100%);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s var(--ease);
}
.vplayer.is-open::after { opacity: 1; }
/* it still covers the viewport while the picture flies back to its thumbnail —
   don't let it swallow clicks meant for the page underneath */
.vplayer:not(.is-open) { pointer-events: none; }

.vplayer__close,
.vplayer__caption,
.vplayer__missing,
.vplayer__bar {
  opacity: 0;
  transition: opacity .25s var(--ease);
}
.vplayer.is-open .vplayer__close,
.vplayer.is-open .vplayer__caption,
.vplayer.is-open .vplayer__missing,
.vplayer.is-open .vplayer__bar { opacity: 1; }

/* Sized to the film's own aspect and centred, so the frame *is* the picture —
   no letterboxing inside it. That matters for the close animation: the frame
   is what shrinks into the thumbnail, and if it carried bars they would shrink
   too. --va is set from the file's real dimensions once metadata lands. */
.vplayer__frame {
  grid-area: 1 / 1;
  place-self: center;
  position: relative; z-index: 1;   /* over the black ground */
  /* Width is capped so the derived height still fits, which is what makes this
     letterbox rather than crop. A plain width/height of 100% would leave the
     aspect-ratio inert — both axes definite — and object-fit: cover would then
     cut the top and bottom off the film. */
  width: min(100%, calc(100vh * var(--va-num, 1.7778)));
  aspect-ratio: var(--va, 16 / 9);
  overflow: hidden;
  background: #000;
}
.vplayer__video {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;                /* the frame already matches the aspect */
  background: #000;
}

.vplayer__close {
  position: absolute; z-index: 3;
  top: clamp(1rem, 2.2vw, 2.6rem);
  right: clamp(1rem, 2.2vw, 2.6rem);
  width: 34px; height: 34px;
}
.vplayer__close::before, .vplayer__close::after {
  content: "";
  position: absolute; left: 50%; top: 50%;
  width: 100%; height: 2px;
  background: var(--white);
  transition: background-color .2s var(--ease);
}
.vplayer__close::before { transform: translate(-50%, -50%) rotate(45deg); }
.vplayer__close::after  { transform: translate(-50%, -50%) rotate(-45deg); }
.vplayer__close:hover::before, .vplayer__close:hover::after { background: var(--record-red); }

/* frame 57-4802: 10 between the kicker and the client, 4 between the client
   and the work. The two names sit tight together and the kicker stands off
   from the pair. */
.vplayer__caption {
  position: absolute; z-index: 3;
  left: clamp(1rem, 2.4vw, 2.9rem);
  bottom: clamp(4.5rem, 6vw, 7rem);
  text-shadow: 0 1px 2px rgba(0,0,0,.55), 0 2px 18px rgba(0,0,0,.5);
}
.vplayer__caption .label {
  font-size: clamp(.75rem, 1.042vw, 1.25rem);   /* 20 @1920 */
  margin-bottom: clamp(6px, .52vw, 10px);       /* 10 @1920 */
}
.vplayer__title { margin-bottom: clamp(2px, .208vw, 4px); }   /* 4 @1920 */
.vplayer__title, .vplayer__sub {
  font-family: var(--font-head);                /* CHANEY Regular */
  font-size: clamp(.95rem, 1.458vw, 1.75rem);   /* 28 @1920 */
  line-height: .88;
  letter-spacing: 0;
  text-transform: uppercase;
  font-weight: 400;
}

.vplayer__missing {
  grid-area: 1 / 1;
  align-self: center; justify-self: center;
  z-index: 2;
  font-family: var(--font-mono);
  font-size: clamp(.75rem, .9vw, 1rem);
  text-transform: uppercase;
  letter-spacing: .08em;
  text-align: center;
  padding: 1rem 1.5rem;
  border: 1px solid rgba(255,255,255,.4);
}
.vplayer__missing code { color: var(--record-red); }

/* transport: Record Red bar, Blue Note play + played progress */
.vplayer__bar {
  position: absolute; z-index: 3;
  left: clamp(1rem, 2.4vw, 2.9rem);
  right: clamp(1rem, 2.4vw, 2.9rem);
  bottom: clamp(1rem, 2.4vw, 2.6rem);
  height: 34px;
  display: flex; align-items: center; gap: .9rem;
  background: var(--record-red);
  padding: 0 .8rem;
}
.vplayer__play {
  flex: 0 0 auto;
  width: 15px; height: 17px;
  background: var(--blue-note);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
.vplayer.is-playing .vplayer__play {
  clip-path: none;
  width: 13px;
  background: linear-gradient(90deg, var(--blue-note) 0 38%, transparent 38% 62%, var(--blue-note) 62% 100%);
}

/* Same anchoring as the audio player's bar — see --fill-at there. This one is
   not eased, so it never showed the seam, but the thumb is inset by half its
   width here too and a percentage of the track is not where its centre is. */
.vplayer__seek {
  -webkit-appearance: none; appearance: none;
  flex: 1; height: 14px;
  background: transparent;
  cursor: pointer;
  margin: 0;
  --thumb: 11px;
  --fill-at: calc(var(--thumb) / 2 + (100% - var(--thumb)) * var(--seek, 0));
}
.vplayer__seek::-webkit-slider-runnable-track {
  height: 3px;
  background: linear-gradient(90deg, var(--blue-note) var(--fill-at), var(--white) var(--fill-at));
}
.vplayer__seek::-moz-range-track {
  height: 3px;
  background: linear-gradient(90deg, var(--blue-note) var(--fill-at), var(--white) var(--fill-at));
}
.vplayer__seek::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 11px; height: 11px; margin-top: -4px;
  background: var(--blue-note); border-radius: 50%; border: 0;
  opacity: 0; transition: opacity .2s var(--ease);
}
.vplayer__seek::-moz-range-thumb {
  width: 11px; height: 11px;
  background: var(--blue-note); border-radius: 50%; border: 0;
  opacity: 0; transition: opacity .2s var(--ease);
}
.vplayer__bar:hover .vplayer__seek::-webkit-slider-thumb,
.vplayer__seek:focus-visible::-webkit-slider-thumb { opacity: 1; }
.vplayer__bar:hover .vplayer__seek::-moz-range-thumb { opacity: 1; }

.vplayer__mute { flex: 0 0 auto; display: grid; place-items: center; width: 22px; height: 22px; padding: 0; }
.vplayer__mute svg { width: 20px; height: 20px; }
.vplayer__cross { opacity: 0; }
.vplayer__mute.is-muted .vplayer__waves { opacity: 0; }
.vplayer__mute.is-muted .vplayer__cross { opacity: 1; }

/* Same box as the mute button so the two read as a pair at the end of the bar.
   One glyph is shown at a time: corners pointing out to go in, pointing in to
   come back. On an iPhone the native player takes over and neither is on
   screen while it is up, which is why the state is driven by the events rather
   than by the click. */
.vplayer__full { flex: 0 0 auto; display: grid; place-items: center; width: 22px; height: 22px; padding: 0; }
.vplayer__full svg { width: 19px; height: 19px; }
.vplayer__shrink { opacity: 0; }
.vplayer__full.is-full .vplayer__expand { opacity: 0; }
.vplayer__full.is-full .vplayer__shrink { opacity: 1; }

body.vplayer-open { overflow: hidden; }

@media (max-width: 640px) {
  .vplayer__caption { bottom: clamp(4rem, 14vw, 6rem); }
}

/* =========================================================
   COOKIE CONSENT
   Blue Note bar pinned to the bottom, same buttons as the rest of the site.
   Analytics stay off until Accept is pressed.
   ========================================================= */
.cookies {
  position: fixed; z-index: 150;
  left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; justify-content: space-between;
  gap: clamp(1rem, 2.5vw, 3rem);
  flex-wrap: wrap;
  padding: clamp(1rem, 1.8vw, 1.6rem) var(--gutter);
  background: var(--blue-note);
  border-top: 2px solid var(--record-red);
  translate: 0 100%;
  transition: translate .45s var(--ease);
}
.cookies[hidden] { display: none; }
.cookies.is-in { translate: 0 0; }

.cookies__body { flex: 1 1 26rem; }
.cookies__text {
  font-family: var(--font-mono);
  font-size: clamp(.72rem, .83vw, .95rem);
  line-height: 1.5;
  max-width: 76ch;
  margin-top: .35rem;
}
.cookies__text a { text-decoration: underline; text-underline-offset: 3px; }
.cookies__text a:hover { color: var(--record-red); }

.cookies__actions { display: flex; gap: .6rem; flex: 0 0 auto; }
.cookies__actions .btn { cursor: pointer; }

@media (max-width: 560px) {
  .cookies { flex-direction: column; align-items: stretch; }
  /* flex-basis follows the main axis, so the 26rem that sets a sensible column
     width on desktop becomes 26rem of HEIGHT once the bar stacks — the bar grew
     to fill most of a phone screen with the text stranded at the top and the
     buttons at the bottom. Stacked, the body should just be as tall as its
     text. */
  .cookies__body { flex: 0 1 auto; }
  .cookies__actions .btn { flex: 1; text-align: center; }
}

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

/* =========================================================
   DESIGNER FEEDBACK — motion pass
   ========================================================= */

/* --- 4 · Selected Work thumbnails rise into place.
   Each card fades up and grows the last of the way into its slot, one after the
   next across the grid. main.js replays this when the player closes, so you
   come back to movement instead of a static wall of stills. --- */
.work__item[data-reveal] {
  translate: 0 34px;
  scale: .93;
  transition-property: opacity, translate, scale;
  transition-duration: .7s;
  transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
/* translate has to be restated here: the rule above matches [data-reveal].is-in
   for specificity but comes later in the file, so it was winning and the cards
   settled 34px low instead of at 0 */
.work__item[data-reveal].is-in { translate: 0 0; scale: 1; }

.work__grid .work__item:nth-child(1) { transition-delay: .00s; }
.work__grid .work__item:nth-child(2) { transition-delay: .09s; }
.work__grid .work__item:nth-child(3) { transition-delay: .18s; }
.work__grid .work__item:nth-child(4) { transition-delay: .27s; }
.work__grid .work__item:nth-child(5) { transition-delay: .36s; }

/* the footer arrives the same way */
.footer[data-reveal] {
  translate: 0 24px;
  transition-duration: .55s;
  transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
.footer[data-reveal].is-in { translate: 0 0; }   /* same specificity trap as above */

/* --- 5 + 12 · glitch, not a fade.
   Long calm stretch, then a short burst of hard-snapped jumps in opacity and
   horizontal offset — the steps() timing is what makes it read as broken
   signal rather than a smooth pulse. Uses `translate` so it composes with the
   positioning transforms already on both elements. --- */
@keyframes glitch {
  0%, 78%, 100% { opacity: 1;   translate: 0 0; }
  79%           { opacity: .2;  translate: -5px 0; }
  80%           { opacity: 1;   translate: 4px 0; }
  81%           { opacity: .55; translate: -3px 0; }
  82%           { opacity: 1;   translate: 2px 0; }
  83%           { opacity: .1;  translate: -4px 0; }
  84%           { opacity: 1;   translate: 3px 0; }
  85%           { opacity: .7;  translate: -1px 0; }
  86%           { opacity: 1;   translate: 0 0; }
  93%           { opacity: 1;   translate: 0 0; }
  94%           { opacity: .3;  translate: 3px 0; }
  95%           { opacity: 1;   translate: -2px 0; }
  96%           { opacity: 1;   translate: 0 0; }
}

.stats__mark.is-drawn path {
  animation: draw 2.4s cubic-bezier(0.32, 0.1, 0.2, 1) forwards;
}
/* The mark glitches as a whole once the blue fill has swept through "200+
   clients served." — main.js adds .is-live at the same moment. The negative
   delay starts the animation just before its burst, so the first one lands
   with the fill rather than 5s after it; then it comes round every 6.4s.
   `translate` composes with the transform already centring the mark. */
.stats__mark.is-live {
  animation: glitch 6.4s steps(1, end) -4.95s infinite;
}

/* --- 11 · team photos drift as you scroll: outer up, centre down --- */
.member { will-change: transform; }
.member__photo { transform: translateY(var(--drift, 0px)); transition: transform .12s linear; }

/* --- 12 · same glitch on the footer wordmark, offset so the two are never
   in sync --- */
.footer__wordmark { animation: glitch 5.7s steps(1, end) 1.5s infinite; }

@media (prefers-reduced-motion: reduce) {
  /* the glitch lives on the <svg> now, not the paths */
  .stats__mark.is-drawn,
  .stats__mark.is-drawn path,
  .stats__mark.is-live,
  .footer__wordmark { animation: none; }
  .member__photo { transform: none; }
}

/* =========================================================
   SUBMISSION FORM — frame `Submission Form`, 1920 × 1822
   A full-screen overlay on Blue Note, same pattern as the nav menu. The frame
   is taller than the viewport in the design, so it scrolls inside itself.
   ========================================================= */
.brief {
  position: fixed; inset: 0; z-index: 150;
  background: var(--blue-note);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  opacity: 0;
  transition: opacity .35s var(--ease);
}
.brief[hidden] { display: none; }
.brief.is-open { opacity: 1; }

.brief__inner {
  position: relative;
  min-height: 100%;
  /* The deep bottom padding is the design's: the frame runs on past the contact
     block, and that whitespace is what puts the character's head level with the
     bottom of the second field rather than across the middle of it. */
  padding: clamp(.75rem, 2.2vw, 2.6rem) var(--gutter) clamp(2.5rem, 11vw, 13rem);
}

.brief__top { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; }

/* same weight and hit area as the nav's burger, mirrored on the right */
.brief__close {
  position: relative;
  justify-self: end;
  width: 28px; height: 28px;
}
.brief__close::before, .brief__close::after {
  content: "";
  position: absolute; left: 50%; top: 50%;
  width: 100%; height: 2px;
  background: var(--white);
  transition: background-color .2s var(--ease);
}
.brief__close::before { transform: translate(-50%, -50%) rotate(45deg); }
.brief__close::after  { transform: translate(-50%, -50%) rotate(-45deg); }
.brief__close:hover::before, .brief__close:hover::after { background: var(--record-red); }

/* The design insets the whole form to a centred column — roughly 1160 of the
   1920 frame — rather than running it out to the page gutter. Only the top bar
   and the illustration sit outside it. */
.brief__title,
.brief__form,
.brief__contact {
  width: min(100%, 1160px);
  margin-inline: auto;
}

.brief__title {
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 4.35vw, 5.2rem);
  line-height: .88;
  text-transform: uppercase;
  color: var(--white);
  margin: clamp(1.5rem, 3.4vw, 4rem) 0 clamp(1.75rem, 3.6vw, 4.2rem);
}

/* --- fields --- */

.brief__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(1rem, 2vw, 2.4rem);
  row-gap: clamp(.9rem, 1.6vw, 1.7rem);
  margin-bottom: clamp(1.75rem, 3.5vw, 4rem);
}
.field--wide { margin-bottom: clamp(1.75rem, 3.5vw, 4rem); }

.field__label {
  display: block;
  font-family: var(--font-head);
  font-size: clamp(.85rem, 1.15vw, 1.4rem);
  line-height: 1.1;
  text-transform: uppercase;
  color: var(--white);
  margin-bottom: .5em;
}
.field__hint {
  font-family: var(--font-mono);
  font-size: clamp(.72rem, .72vw, .85rem);
  line-height: 1.35;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--white);
  margin: -.25em 0 .7em;
}

.field__input {
  display: block;
  width: 100%;
  font-family: var(--font-mono);
  /* Never below 16px: iOS Safari zooms the whole page in on a focused field
     under that, and does not zoom back out. */
  font-size: max(16px, clamp(.75rem, .8vw, .95rem));
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--white);
  background: var(--record-red);
  border: 1px solid var(--white);
  border-radius: 4px;
  padding: .95em 1.1em;
  transition: box-shadow .2s var(--ease), background-color .2s var(--ease);
}
.field__input--area {
  resize: vertical;
  min-height: clamp(5.5rem, 9vw, 8.5rem);
  line-height: 1.5;
}
.field__input::placeholder { color: rgba(255, 255, 255, .72); }
.field__input:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--white);
}
/* only after a submit attempt, so nothing turns red while you're still typing */
/* only after a submit attempt, so nothing turns red while you're still typing */
.brief__form.is-checked .field__input:invalid {
  background: #d92823;
  box-shadow: 0 0 0 1px var(--white);
}
/* The darker fill is deliberately quiet, so the rule is spelled out instead:
   once a submit has been rejected, every required label carries an asterisk.
   main.js tags the wrappers, so this doesn't depend on :has(). */
.brief__form.is-checked .field.is-required .field__label::after {
  content: "*";
  color: var(--record-red);
  margin-left: .3em;
}
/* one nudge on the rejected submit, so the change is noticed even if you were
   looking at the button rather than the fields */
.brief__form.is-shaken .field__input:invalid { animation: fieldShake .4s var(--ease); }
@keyframes fieldShake {
  0%, 100% { translate: 0; }
  20% { translate: -6px; }
  45% { translate: 5px; }
  70% { translate: -3px; }
}

@media (prefers-reduced-motion: reduce) {
  .brief__form.is-shaken .field__input:invalid { animation: none; }
}

/* the honeypot has to be reachable to a bot and invisible to everyone else —
   display:none would let a smarter bot skip it */
.brief__trap {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.brief__submit { margin-top: clamp(.5rem, 1.2vw, 1.2rem); }

.brief__status {
  font-family: var(--font-mono);
  font-size: clamp(.75rem, .8vw, .95rem);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--white);
  margin-top: 1em;
  min-height: 1.4em;
}
.brief__status--error { color: #ffd7d5; }

/* --- contact block --- */
.brief__contact {
  margin-top: clamp(2.5rem, 5vw, 5.5rem);
  display: flex; flex-direction: column; align-items: flex-start;
  gap: .15em;
}
.brief__contact .label { color: var(--white); opacity: .85; margin-bottom: .5em; }
.brief__contact a {
  font-family: var(--font-head);
  font-size: clamp(.85rem, 1.15vw, 1.4rem);
  line-height: 1.35;
  text-transform: uppercase;
  color: var(--white);
  transition: color .2s var(--ease);
}
.brief__contact a:hover { color: var(--record-red); }

/* --- illustration ---
   Painted only once assets/img/brief-art.svg exists: main.js probes [data-img]
   and sets --img on success, so a missing export leaves empty space rather
   than a broken graphic. */
.brief__art {
  position: absolute;
  right: 0;
  bottom: 0;
  width: clamp(180px, 26vw, 520px);
  height: auto;                     /* the viewBox carries the 479 × 613 ratio */
  display: block;
  pointer-events: none;
  overflow: visible;
}

/* The record head turns while the form is open, the same idea as the vinyl in
   the playlists section. transform-box has to be set: without it the origin is
   resolved against the whole SVG canvas, not the disc, and the head orbits. */
.brief__disc {
  transform-box: view-box;
  transform-origin: 241px 148.5px;  /* the spindle, measured off the artwork */
  animation: spin 9s linear infinite;
  animation-play-state: paused;
}
.brief.is-open .brief__disc { animation-play-state: running; }

/* FT SYNC <3 rises out of the clip window the markup puts around it */
.brief__tee {
  translate: 0 108px;
  transition: translate .9s cubic-bezier(0.16, 0.84, 0.28, 1) .35s;
}
.brief.is-open .brief__tee { translate: 0 0; }

@media (max-width: 900px) {
  .brief__grid { grid-template-columns: 1fr; }
  /* the character would sit on top of the contact details at this width */
  .brief__art { display: none; }

}

@media (prefers-reduced-motion: reduce) {
  .brief { transition: none; }
  .brief__disc { animation: none; }
  .brief__tee { translate: 0 0; transition: none; }
}

/* =========================================================
   BRIEF SENT — the plate that confirms a submission
   Same behaviour as the loading screen: held, then wiped upward off the top,
   which reveals the page underneath rather than just fading out.
   ========================================================= */
.sent {
  position: fixed; inset: 0; z-index: 320;
  background: var(--record-red);
  display: grid; place-items: center; align-content: center;
  gap: 1.2em;
  padding: var(--gutter);
  text-align: center;
  transition: transform .8s cubic-bezier(0.76, 0, 0.24, 1),
              opacity .5s var(--ease) .3s;
}
.sent[hidden] { display: none; }
.sent.is-done {
  transform: translateY(-100%);
  opacity: 0;
  pointer-events: none;
}

.sent__word {
  font-family: var(--font-display);
  font-size: clamp(2rem, 8.5vw, 10rem);
  line-height: 1;
  letter-spacing: -.01em;
  text-transform: uppercase;
  color: var(--white);
  animation: loaderWord .9s cubic-bezier(0.16, 0.84, 0.28, 1) both;
}
.sent__note {
  font-family: var(--font-mono);
  font-size: clamp(.85rem, .95vw, 1.15rem);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--white);
  max-width: 42ch;
  animation: loaderWord .9s cubic-bezier(0.16, 0.84, 0.28, 1) .12s both;
}

@media (prefers-reduced-motion: reduce) {
  .sent { transition: none; }
  .sent__word, .sent__note { animation: none; }
}

/* Disclosure under the submit button. Deliberately not a checkbox and not a
   gate: sending the brief is the agreement. */
.brief__legal {
  font-family: var(--font-mono);
  font-size: clamp(.72rem, .72vw, .85rem);
  line-height: 1.5;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--white);
  opacity: .8;
  margin-top: .9em;
  max-width: 52ch;
}
.brief__legal a { text-decoration: underline; text-underline-offset: .25em; }
.brief__legal a:hover { color: var(--record-red); }

/* =========================================================
   AUDIO DOCK
   A way to stop the playlist once the real player is off screen. Sits under the
   nav overlay, the cookie bar and the two modals, all of which either replace
   it or cover the page entirely.
   ========================================================= */
.dock {
  position: fixed;
  z-index: 90;
  /* the design gutter, not var(--gutter): past 1920 that one grows to centre
     the content column, and a floating control should stay by the edge */
  right: clamp(1.25rem, 4.84vw, 5.8rem);
  bottom: clamp(1rem, 2vw, 1.8rem);
  display: flex;
  align-items: stretch;
  /* White ground and a white ring: the panels are Record Red, so on the red
     sections the widget would otherwise dissolve into the page. The 2px gaps
     read as dividers and the ring holds it together on the blue sections too. */
  background: var(--white);
  box-shadow: 0 0 0 2px var(--white);
  gap: 2px;
  height: 48px;
  /* holds the width steady while (playing) / (paused) and the track name swap */
  min-width: 13.5rem;
  max-width: min(20rem, calc(100vw - clamp(1.25rem, 4.84vw, 5.8rem) * 2));
  translate: 0 calc(100% + 2rem);
  opacity: 0;
  transition: translate .4s var(--ease), opacity .3s var(--ease);
}
.dock[hidden] { display: none; }
.dock.is-in { translate: 0 0; opacity: 1; }

.dock__toggle,
.dock__meta,
.dock__eq {
  background: var(--record-red);
  transition: filter .2s var(--ease);
}
.dock__toggle:hover, .dock__meta:hover { filter: brightness(1.08); }

.dock__toggle {
  flex: 0 0 48px;
  display: grid; place-items: center;
  cursor: pointer;
}
/* same glyph pair as the menu's mini player */
.dock__icon {
  width: 15px; height: 17px;
  background: var(--white);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
.dock.is-playing .dock__icon {
  clip-path: none;
  width: 13px;
  background: linear-gradient(90deg, var(--white) 0 38%, transparent 38% 62%, var(--white) 62% 100%);
}

.dock__meta {
  /* Free to shrink. The dock's own min-width below is what keeps it from
     resizing as the label swaps; a hard floor here instead would push the eq
     panel out of the container whenever space got tight, leaving the ring
     behind at the old size. */
  flex: 1 1 auto;
  min-width: 0;
  display: flex; flex-direction: column; justify-content: center;
  gap: .15em;
  padding: 0 .7rem;
  overflow: hidden;
}
.dock__label {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--white);
  opacity: .8;
}
.dock__track {
  font-family: var(--font-head);
  font-size: 12px;
  line-height: 1.05;
  text-transform: uppercase;
  color: var(--white);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.dock__eq { flex: 0 0 42px; display: grid; place-items: center; }
.dock__eq > span, .dock__eq { }
.dock__eq i { width: 4px; background: var(--white); display: block; }
.dock__eq {
  display: flex; align-items: flex-end; justify-content: center;
  gap: 3px; padding: 13px 0;
}
.dock__eq i:nth-child(1) { height: 45%; }
.dock__eq i:nth-child(2) { height: 100%; }
.dock__eq i:nth-child(3) { height: 65%; }
.dock__eq i:nth-child(4) { height: 85%; }
.dock.is-playing .dock__eq i {
  animation: eq .9s ease-in-out infinite alternate;
  transform-origin: bottom;
}
.dock.is-playing .dock__eq i:nth-child(1) { animation-delay: -.2s; }
.dock.is-playing .dock__eq i:nth-child(2) { animation-delay: -.6s; }
.dock.is-playing .dock__eq i:nth-child(3) { animation-delay: -.4s; }

@media (max-width: 560px) {
  /* the equaliser is decoration; the stop button and the title are the point */
  .dock__eq { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .dock { transition: none; }
  .dock.is-playing .dock__eq i { animation: none; }
}

/* =========================================================
   PRIVACY POLICY OVERLAY
   Same full-screen Blue Note plate and close button as the brief, so the two
   overlays read as one family. The body is set in the mono face at a wider
   measure than the rest of the page: this is the only long-form reading on the
   site, and CHANEY is a display face that would be punishing at this length.
   ========================================================= */
.policy {
  position: fixed; inset: 0; z-index: 160;   /* over the brief, which can open it */
  background: var(--blue-note);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  opacity: 0;
  transition: opacity .35s var(--ease);
}
.policy[hidden] { display: none; }
.policy.is-open { opacity: 1; }

.policy__inner {
  position: relative;
  min-height: 100%;
  padding: clamp(.75rem, 2.2vw, 2.6rem) var(--gutter) clamp(3rem, 8vw, 8rem);
}
.policy__top { display: flex; align-items: center; justify-content: space-between; }

.policy__title {
  font-family: var(--font-ext);                  /* CHANEY Extended */
  font-size: clamp(2.2rem, 6.25vw, 7.5rem);      /* 120 @1920 */
  line-height: .79;
  text-transform: uppercase;
  font-weight: 400;
  margin-top: clamp(1.5rem, 4vw, 4rem);
}

.policy__body {
  /* 68 characters is the measure this face reads at; the gutters alone would
     leave lines twice that long on a wide screen. */
  max-width: 68ch;
  margin-top: clamp(2rem, 4vw, 4rem);
  font-family: var(--font-mono);
  font-size: clamp(.82rem, .88vw, 1.0625rem);
  line-height: 1.65;
}
.policy__body h3 {
  font-family: var(--font-ext);                  /* CHANEY Extended */
  font-size: clamp(.95rem, 1.15vw, 1.375rem);
  line-height: 1.05;
  text-transform: uppercase;
  font-weight: 400;
  margin-top: clamp(2rem, 3.2vw, 3.25rem);
  margin-bottom: .9rem;
}
.policy__body p { margin-bottom: 1.1rem; }
.policy__body p:last-child { margin-bottom: 0; }
.policy__body ul { margin: 0 0 1.1rem; padding-left: 1.2rem; list-style: disc; }
.policy__body li { margin-bottom: .6rem; }
.policy__body strong { font-weight: 700; }
.policy__body a {
  color: inherit;
  text-decoration: underline; text-underline-offset: 3px;
}
.policy__body a:hover { color: var(--record-red); }

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