/* ═══════════════════════════════════════════════════════════════════
   BASE — tokens, faces, reset, typography
   Palette is sampled from the two films: sage room walls, cream
   linen shirt, terracotta rug, warm wood, the olive-black office.
   One accent (clay). Everything else is value.
   ═══════════════════════════════════════════════════════════════════ */

/* ── faces ───────────────────────────────────────────────────────── */
@font-face {
  font-family: 'Sofia XC';
  src: url('../fonts/sofia-sans-extra-condensed-latin.woff2') format('woff2');
  font-weight: 200 900; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Sofia XC';
  src: url('../fonts/sofia-sans-extra-condensed-latinext.woff2') format('woff2');
  font-weight: 200 900; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}
@font-face {
  font-family: 'Sofia';
  src: url('../fonts/sofia-sans-latin.woff2') format('woff2');
  font-weight: 300 800; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Sofia';
  src: url('../fonts/sofia-sans-latinext.woff2') format('woff2');
  font-weight: 300 800; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}
@font-face {
  font-family: 'Newsreader';
  src: url('../fonts/newsreader-latin.woff2') format('woff2');
  font-weight: 200 700; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Newsreader';
  src: url('../fonts/newsreader-italic-latin.woff2') format('woff2');
  font-weight: 200 600; font-style: italic; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Newsreader';
  src: url('../fonts/newsreader-latinext.woff2') format('woff2');
  font-weight: 200 700; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

/* ── tokens ──────────────────────────────────────────────────────── */
:root {
  /* value scale, warm — from the linen and the olive-black wall.

     The three papers were lifted together by 0.022 of OKLab lightness,
     which is a nudge rather than a repaint. Together, because they are a
     scale: raising the ground alone would have walked it into `-lift`,
     which sits only 0.027 above it, and flattened the step the sound
     card and the pressed states rely on.

     In OKLab and not by adding to each channel, because a warm tone
     loses its warmth on the way to white if you do that — the blue
     channel has the most room left, so it climbs fastest and the linen
     turns grey. Hue and chroma are held; only lightness moves. */
  --paper:      #F0E9D9;
  --paper-lift: #F8F2E5;
  --paper-sink: #E3D9C5;
  --ink:        #16170F;
  --ink-lift:   #22241A;
  --ink-soft:   #3A3D2E;

  /* hue, used sparingly */
  --sage:       #93A383;
  --sage-deep:  #56603F;
  --clay:       #B4553C;   /* the rug. the only accent. */
  --wine:       #6E2C3E;   /* the sweater, and Lyft's mark */
  --navy:       #2E4A72;   /* WW's blue */
  --wood:       #C2996B;

  /* semantic — flipped by .on-ink */
  --bg:   var(--paper);
  --fg:   var(--ink);
  --fg-2: color-mix(in oklab, var(--ink) 62%, var(--paper));
  --fg-3: color-mix(in oklab, var(--ink) 34%, var(--paper));
  --rule: color-mix(in oklab, var(--ink) 18%, var(--paper));
  --accent: var(--clay);

  /* Both employer films are cropped to this ratio, and the lean derived
     from it is used in two places — the experience panels and the case
     studies, which have to set their words in the same column the rows
     are set in. One definition, so they cannot drift apart. See the long
     note in css/sections.css for what the lean is actually correcting. */
  --film: 1080 / 1340;
  --film-w: min(50vw, calc(100svh * var(--film)));
  --lean: calc((50vw - var(--film-w)) / 2);

  /* type */
  --f-display: 'Sofia XC', 'Arial Narrow', system-ui, sans-serif;
  --f-ui:      'Sofia', system-ui, -apple-system, sans-serif;
  --f-body:    'Newsreader', Georgia, 'Times New Roman', serif;

  /* measure & rhythm */
  --gutter: clamp(1.25rem, 4.2vw, 4.5rem);
  /* Every section stands on this — the chrome, `.wrap`, the footer — so
     the page has one left edge from the name at the top to the sign-off
     at the bottom, and one right edge to match.

     Two segments, because the three sizes it is cut to are not on one
     line: 6.25vw carries 1280→80 and 1600→100, then it steepens to reach
     1920→150. `max()` picks whichever segment is ahead, which puts the
     bend exactly at 1600 where the two meet, with no step in it. */
  /* Doubled from where it started — 80px at 1280 became 160, 150 at 1920
     became 300. The floor is left where it was, because the same doubling
     on a phone would spend a third of a 320px screen on margin and push
     the address off the side. */
  --gutter-wide: clamp(1.875rem, max(12.5vw, calc(31.25vw - 300px)), 24rem);
  --stack:  clamp(4.5rem, 11vh, 9rem);
  /* No `--maxw`. The column used to cap at 96rem and centre itself, which
     is what stopped the sections lining up with the chrome on anything
     wider than that — see `.wrap`. The inset is the whole layout now, and
     the things that need a measure of their own carry one. */

  /* The statement's run, shared by the two creed lines so all three sit in
     exactly the same column. The lede is 22ch of the display face at
     --stmt-fs; `ch` resolves against the element's own size, so it cannot
     be reused by type set any smaller. Resolving it to a length here can:
     9.15 is that face's ch-per-em × 22, measured off the rendered lede
     rather than assumed. Re-measure it if the display face changes. */
  /* Answers to the viewport's height as well as its width. Width alone is
     the wrong question for a block of display type: a 13" laptop is a
     wide screen and a short one, and set purely off `vw` the statement
     ran off the bottom of it. `min()` lets whichever dimension is tighter
     decide, so a short window gets smaller type rather than a sentence
     running past the fold.

     The height term has a ceiling it must not cross. The rewriting
     statement is `min-height: 100vh` and clipped, its taller half runs
     five lines at 0.9 leading (4.5em of block), and the section spends
     `13vh` on padding at each end — leaving 74vh for the type, so 74 ÷
     4.5 ≈ 16vh is the most it can ask for. 15.2 sits just under that.
     Change the leading, the line count or that padding and this has to be
     recut; raise it past 16 and the sentence gets clipped.

     Everything here is the original scale less 5%. */
  --stmt-fs:      clamp(2.1375rem, min(10.355vw, 15.2vh), 9.975rem);
  --stmt-measure: min(100%, calc(var(--stmt-fs) * 9.15));

  /* motion */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-io: cubic-bezier(0.65, 0, 0.35, 1);
}

.on-ink {
  --bg:   var(--ink);
  --fg:   var(--paper);
  --fg-2: color-mix(in oklab, var(--paper) 66%, var(--ink));
  --fg-3: color-mix(in oklab, var(--paper) 38%, var(--ink));
  --rule: color-mix(in oklab, var(--paper) 20%, var(--ink));
  --accent: #C9694C;   /* clay, lifted so it holds on ink */
}

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

/* No `scroll-behavior: smooth` here, and it must not come back.

   ScrollTrigger measures by moving the scroll position itself: a refresh
   hops the page to read every trigger's natural offset and then puts it
   back. Declared here, those hops stop being hops — the browser animates
   them, and ScrollTrigger reads the offsets while the page is still
   gliding, so every trigger on the page ends up pointing somewhere it is
   not. On a real browser after a resize the pinned statement came back
   with a start of 2355 for a section that begins at 8163, which is why it
   pinned itself over the experience panel.

   The smooth jump the nav wants is worth keeping, so `initAnchors()` in
   js/sections.js does it per click instead. That only smooths the scrolls
   we ask for, and leaves ScrollTrigger's own alone. */
html {
  -webkit-text-size-adjust: 100%;
  background: var(--ink);
}

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--f-body);
  font-size: clamp(1rem, 0.94rem + 0.28vw, 1.1875rem);
  line-height: 1.62;
  font-weight: 380;
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  text-rendering: optimizeLegibility;
}
body.is-loading { overflow: hidden; height: 100vh; }

img, video, canvas, svg { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

a { color: inherit; text-decoration: none; }
::selection { background: var(--clay); color: var(--paper-lift); }

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

/* ── typography primitives ───────────────────────────────────────── */
h1, h2, h3, h4 {
  font-family: var(--f-display);
  font-weight: 800;
  line-height: 0.84;
  letter-spacing: -0.018em;
  text-transform: uppercase;
  font-synthesis: none;
}

p { text-wrap: pretty; }
em { font-style: italic; }

/* micro-label: the technical voice of the site */
.u-label {
  font-family: var(--f-ui);
  font-size: clamp(0.625rem, 0.58rem + 0.12vw, 0.75rem);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--fg-3);
  font-feature-settings: 'tnum' 1;
}
.u-num { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }

/* One left edge for the whole page.

   This stood on `--gutter` while the chrome and the footer stood on
   `--gutter-wide`, so the sections in between never lined up with either:
   measured on a 1280 window the statement began at 54px against the
   mark's 80px. The display type is large enough that being a few dozen
   pixels off the page's own spine reads as no margin at all rather than
   as a different margin.

   Switching the token was half of it — the `max-width` had to go too, and
   the footer had been doing exactly that all along in `.end .wrap`. A
   capped column centres itself once the window is wider than the cap,
   which moves its left edge by half the leftover: at 1728 that put the
   text at 216px against the mark's 120px, further out of line than
   before, not less. No padding reconciles the two, because past the cap
   the overhang grows at half the viewport and the inset at a sixth of it.
   So `--gutter-wide` is the whole inset now, and the edge holds — 80px at
   1280, 120px at 1728, 192px at 2560.

   Nothing here needed the cap for its measure: the statement runs on
   `--stmt-measure` and the prose blocks carry their own `max-width`.
   `--gutter` is still right for things sitting *inside* a column, which
   is why the hero's cue and the experience panels keep it. */
.wrap {
  width: 100%;
  margin-inline: auto;
  padding-inline: var(--gutter-wide);
}

.sect-idx {
  display: block;
  color: var(--fg-3);
  padding-bottom: 1.1rem;
  border-bottom: 1px solid var(--rule);
  margin-bottom: clamp(2.25rem, 5vh, 4rem);
}

/* ── reveal primitives (driven by js/sections.js) ────────────────── */
[data-reveal],
[data-reveal-lines] .rl__in { will-change: transform, opacity; }

.no-motion [data-reveal],
.no-motion [data-reveal-lines] .rl__in { opacity: 1 !important; transform: none !important; }

/* ── cursor scroll hint ──────────────────────────────────────────── */
.cursor-tag {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 65;
  pointer-events: none;
  will-change: transform;
}
/* offset from the pointer via margin, not transform — GSAP owns the span's
   transform (the springy entrance, the idle bob) and must not fight a
   static translate here */
.cursor-tag span {
  display: block;
  margin: 0.7rem 0 0 0.9rem;
  transform-origin: 0 0;
  opacity: 0;
  /* type and padding both carry the same 1.2× off the original 0.625rem /
     0.46-0.8-0.4rem, so the pill grows without changing shape. The
     tracking is in `em`, so it comes along on its own. */
  padding: 0.552rem 0.96rem 0.48rem;
  border-radius: 100px;
  background: var(--ink);
  color: var(--paper-lift);
  font-family: var(--f-ui);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: 0 8px 22px -12px rgb(12 14 8 / 0.6);
}
@media (hover: none) { .cursor-tag { display: none; } }

/* ── the lens ─────────────────────────────────────────────────────
   The page is built around an aperture, so the pointer is one too: a dot
   where you actually are, and a ring a beat behind it that opens over
   anything you can act on.

   `mix-blend-mode: difference` does the theming. Paper against paper
   reads as ink, paper against ink reads as paper — so it stays visible
   across the page's alternating grounds and over the film itself,
   without anything having to track which section it is on. The chrome
   above it is drawn the same way.

   Centred on the pointer with negative margins rather than a translate,
   because GSAP owns these transforms — the follow, and the opening —
   and a static translate here would be overwritten by the first tween.
   Both start at `opacity: 0`; the script fades them in on the first
   real pointer move, so they never sit at 0,0 waiting. */
/* Above everything, including the case-study overlay (80) and the name
   in flight above it (90). It is the pointer: there is no state of the
   page where the right answer is to cover it up. */
.lens-ring,
.lens-dot {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  pointer-events: none;
  border-radius: 50%;
  opacity: 0;
  mix-blend-mode: difference;
  will-change: transform;
}
.lens-ring {
  width: 34px;
  height: 34px;
  margin: -17px 0 0 -17px;
  border: 1.5px solid var(--paper);
}
.lens-dot {
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  background: var(--paper);
}
/* nothing to attach to without a pointer */
@media (hover: none) {
  .lens-ring, .lens-dot { display: none; }
}

/* The class is added by js/cursor.js, never set here: if the script does
   not run there has to still be a cursor. `!important` because the reset
   hands buttons and links their own, and the lens stands in for all of
   them — a stray arrow appearing over one control and not the next is
   worse than either choice on its own. */
.has-lens,
.has-lens * { cursor: none !important; }

/* ── page chrome ─────────────────────────────────────────────────── */
.chrome {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 60;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1rem;
  padding-block: clamp(1rem, 2.2vh, 1.6rem);
  /* Measured off the viewport, not off the content column — the inset is
     cut to exact sizes, and the column's own overhang is larger than the
     widest of them by 1920. The footer is taken off the column to match;
     see `.end .wrap`. */
  padding-inline: var(--gutter-wide);
  mix-blend-mode: difference;
  color: #F2ECDE;
  pointer-events: none;
  transition: opacity 0.5s var(--ease);
}
.chrome > * { pointer-events: auto; }
/* out of the way while the film is full bleed — and not clickable while invisible */
.chrome.is-hidden { opacity: 0; }
.chrome.is-hidden > * { pointer-events: none; }

.chrome__mark { display: flex; align-items: baseline; gap: 0.5rem; justify-self: start; }
/* the monogram is gone; the name carries the mark on its own.
   The mark and the clock share a size (15px); the nav sits a step under
   at 12, so the row reads as a name and a place with the sections quieter
   between them. They still share a face, a weight and a tracking, which
   is what keeps it one row rather than three things next to each other.
   Down at 22.5rem they converge again — see that breakpoint for why. */
.chrome__full {
  font-family: var(--f-ui);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.9;
}

.chrome__nav {
  display: flex;
  gap: clamp(0.9rem, 2vw, 1.9rem);
  justify-self: center;
}
/* 12px, against the mark's and the clock's 15. That bends the one-size
   rule `.chrome__full` describes, deliberately — but the 22.5rem
   breakpoint below is what happens when the three drift too far apart, so
   it is worth knowing the rule is being spent here. */
.chrome__nav a {
  font-family: var(--f-ui);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  position: relative;
  padding-block: 0.2rem;
  /* blended against whatever scrolls beneath, so it needs enough weight
     to survive passing over the big display type */
  opacity: 0.86;
  transition: opacity 0.3s var(--ease);
}
/* The rule enters from the left and leaves to the right, so the hover
   reads as one stroke passing through rather than a line being drawn and
   rubbed out along its own path. Only `transform` transitions — the
   origin flips the instant the pointer goes, which is what sends the
   collapse out the far side. Same trick on every underlined link on the
   page; see `.end__mail-rule`, `.end__links` and `.exp__work-rule`. */
.chrome__nav a::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.45s var(--ease);
}
.chrome__nav a:hover { opacity: 1; }
.chrome__nav a:hover::after,
.chrome__nav a.is-active::after { transform: scaleX(1); transform-origin: left; }
.chrome__nav a.is-active { opacity: 1; }

/* place + clock on the right, opposite the mark */
/* `u-label` sizes it far smaller by default; overridden so it sits at the
   bar's one size with the mark and the nav. */
.chrome__loc {
  color: inherit;
  opacity: 0.68;
  white-space: nowrap;
  justify-self: end;
  font-size: 0.9375rem;
  letter-spacing: 0.14em;
}

@media (max-width: 60rem) {
  /* the centre nav needs the room; drop the clock to the right edge stays */
  .chrome__loc { display: none; }
  .chrome { grid-template-columns: auto 1fr; }
}
@media (max-width: 30rem) {
  .chrome__full { font-size: 0.6875rem; letter-spacing: 0.1em; }
}
@media (max-width: 46rem) {
  .chrome { grid-template-columns: auto 1fr; }
  .chrome__nav { justify-self: end; }
}
@media (max-width: 34rem) {
  .chrome__nav a:not([href="#experience"]):not([href="#contact"]) { display: none; }
  .chrome__nav { gap: 0.9rem; }
}
/* Down here the mark and the nav converge on 11px, because there is no
   room to be expressive with the difference. `.chrome__full` steps down
   at 30rem, and when the nav did not follow it the two remaining links
   ran 31px past the right edge at 320 with "Contact" cut in half.

   Still needed at 12px, and by more than it looks: the inset doubled, so
   the bar has 240px to work with at 320 rather than 260. Left at 12 the
   nav measures 158 beside a 97px mark — 15 over. At 11 it is 139, which
   clears by 4px. That is the whole margin, so anything added to this row
   has to be measured, not eyeballed.

   22.5rem rather than the last width that actually broke, because 360
   clears it by 34px and 375 by 26 — the step is placed where the slack
   runs out, not where the overflow starts. */
@media (max-width: 22.5rem) {
  .chrome__nav a { font-size: 0.6875rem; letter-spacing: 0.1em; }
}

/* ── preloader ───────────────────────────────────────────────────── */
.loader {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--ink);
  color: var(--paper);
  display: grid;
  place-items: end stretch;
  padding: var(--gutter);
  transition: none;
}
.loader__inner {
  width: 100%;
  display: grid;
  gap: clamp(0.9rem, 2vh, 1.4rem);
}
/* The count and the meter together are the link, so they are the hit area
   and nothing else is. Same treatment as the footer's mail address: the
   words take the accent, a rule washes across underneath. */
.loader__enter {
  display: grid;
  gap: clamp(0.55rem, 1.5vh, 0.95rem);
  width: 100%;
  /* the count is `max-content`; without this it could stretch the grid
     rather than being capped by its own `max-width` */
  min-width: 0;
  text-align: left;
  cursor: default;
}
.loader.is-ready-tap .loader__enter { cursor: pointer; }

/* The count is the mark. Zero-padded and tabular, so the three digits
   hold their width and the number never jitters as it climbs. */
.loader__count {
  display: block;
  width: max-content;
  max-width: 100%;
  font-family: var(--f-display);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 0.78;
  font-size: clamp(5rem, 21vw, 19rem);
  color: var(--paper);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  transition: color 0.45s var(--ease);
}
/* Once it is words rather than digits. The size is not set here — it is
   measured and fitted in js/preloader.js, because the run has to match
   the meter's exactly or the finished bar reads as a bar sitting under a
   label rather than as that label's own underline. */
.loader__count.is-cta {
  white-space: nowrap;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  line-height: 0.86;
  font-variant-numeric: normal;
  font-feature-settings: normal;
}

.loader__meter {
  position: relative;
  display: block;
  height: 1px;
  width: 100%;
  background: color-mix(in oklab, var(--paper) 18%, var(--ink));
  overflow: hidden;
  transition: height 0.5s var(--ease);
}
/* as an underline under 200px type, a hairline disappears */
.loader.is-cta-ready .loader__meter { height: 2px; }
.loader__bar {
  display: block;
  height: 100%;
  width: 100%;
  background: var(--paper);
  transform: scaleX(0);
  transform-origin: left;
}
/* the accent that washes across the finished bar, exactly as the footer's
   mail rule does — the completed meter is the underline */
.loader__wash {
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.75s var(--ease);
}

.loader.is-ready-tap .loader__enter:hover .loader__count,
.loader.is-ready-tap .loader__enter:focus-visible .loader__count { color: var(--accent); }
.loader.is-ready-tap .loader__enter:hover .loader__wash,
.loader.is-ready-tap .loader__enter:focus-visible .loader__wash { transform: scaleX(1); }

@media (prefers-reduced-motion: reduce) {
  .loader__count,
  .loader__meter,
  .loader__wash { transition: none; }
}
.loader__foot { display: flex; justify-content: space-between; align-items: baseline; }
.loader__foot .u-label { color: color-mix(in oklab, var(--paper) 45%, var(--ink)); }

/* kept only as the button's accessible name — a screen reader has no
   count to read, and "Click to enter" is the thing worth announcing */
.loader__enter-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.loader.is-done { pointer-events: none; }

/* ── sound ─────────────────────────────────────────────────────────
   One element, two lives: a welcome card sitting where the film has
   room for it, then a pill that stays reachable for the rest of the
   page. Fixed, so it survives every section without being re-mounted. */
/* The card and the pill are two independently anchored elements that
   crossfade. Animating one element's left/top/right/bottom from the
   bottom-left corner to the top-right one made it skate across the page
   and re-layout every frame; only opacity and transform move now.
   `.sound` itself must stay unpositioned and free of opacity, or it would
   become the containing block for its own fixed children. */
.sound { position: static; }

.sound__peek,
.sound__panel {
  position: fixed;
  z-index: 70;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s var(--ease), transform 0.55s var(--ease);
}
/* while the dock offset is being measured, nothing may animate */
.sound.is-measuring .sound__peek,
.sound.is-measuring .sound__panel { transition: none !important; }

/* A proper little now-playing chip, anchored bottom-right so it reads as a
   fixture of the site rather than a stray button. Frosted to sit over any
   ground, with the equaliser and the current track's name. */
.sound__peek {
  bottom: clamp(1.25rem, 3vh, 2rem);
  right: var(--gutter);
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.6rem 1.05rem 0.55rem 0.9rem;
  border: 1px solid color-mix(in oklab, var(--ink) 14%, transparent);
  border-radius: 100px;
  background: color-mix(in oklab, var(--paper-lift) 84%, transparent);
  backdrop-filter: blur(16px) saturate(1.1);
  color: var(--ink);
  white-space: nowrap;
  box-shadow: 0 16px 40px -22px rgb(22 23 15 / 0.5);
  transform: translateY(0.5rem) scale(0.96);
  /* --peek-delay lets js hold the pill back until the card has finished
     sliding away, so the two never cross paths on screen */
  transition: opacity 0.5s var(--ease) var(--peek-delay, 0s),
              transform 0.6s var(--ease) var(--peek-delay, 0s),
              border-color 0.35s var(--ease), box-shadow 0.35s var(--ease);
}
.sound__peek:hover {
  border-color: color-mix(in oklab, var(--ink) 30%, transparent);
  box-shadow: 0 20px 46px -20px rgb(22 23 15 / 0.55);
  transform: translateY(-2px);
}
.sound.is-shown.is-min:not(.is-open) .sound__peek:hover { transform: translateY(-2px); }
.sound__peek .sound__wave { height: 13px; color: var(--accent); }
.sound__peek-label {
  font-family: var(--f-display);
  font-weight: 700;
  text-transform: uppercase;
  font-size: 1rem;
  line-height: 0.9;
  letter-spacing: 0.005em;
  color: var(--ink);
}
.sound.is-shown.is-min:not(.is-open) .sound__peek {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* the card — softer than pure white, tuned to the site's warm paper, with
   a rounder corner and a gentler shadow so it feels of a piece with the page */
/* ONE anchor, always — bottom-left, where the welcome card opens. The
   docked position beside the pill is expressed as a translate, never as a
   change of left/right/bottom. That is the whole fix: box-offset properties
   are not in the transition list, so any anchor swap lands instantly while
   opacity is still fading — the card visibly teleports to the other corner
   and fades there. A transform is transitioned in lockstep with opacity, so
   there is no frame where the panel is both moved and still visible.
   js/sound.js measures --sound-dx/--sound-dy off the pill. */
.sound__panel {
  left: var(--gutter);
  bottom: clamp(3.5rem, 8vh, 5.5rem);
  width: min(21rem, calc(100vw - var(--gutter) * 2));
  padding: clamp(1.25rem, 2.6vh, 1.6rem) clamp(1.2rem, 2.4vw, 1.6rem) clamp(1.1rem, 2.2vh, 1.4rem);
  border: 1px solid color-mix(in oklab, var(--ink) 9%, transparent);
  border-radius: 16px;
  background: color-mix(in oklab, var(--paper-lift) 78%, #fff);
  backdrop-filter: blur(20px) saturate(1.15);
  box-shadow: 0 26px 64px -34px rgb(22 23 15 / 0.4), 0 2px 8px -4px rgb(22 23 15 / 0.18);
  transform-origin: 50% 100%;
  /* Leaving happens in two beats, never at once: the card fades out where
     it stands, and only once it is fully invisible does the transform run —
     quickly — to park it beside the pill. The delay on `transform` is what
     keeps the travel off screen; nothing about the move is ever watched. */
  transition: opacity 0.3s var(--ease-io),
              transform 0.3s var(--ease) 0.32s;
  transform: translate(0px, 0.6rem) scale(0.985);
}

/* welcome — settled at the anchor, over the film. Arriving is quicker than
   leaving and carries no delay; a transition uses the timing of the state it
   is heading toward, so these overrides only affect the way in. */
.sound.is-shown:not(.is-min) .sound__panel {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  transition: opacity 0.4s var(--ease), transform 0.5s var(--ease);
}

/* docked beside the pill: hidden at rest, revealed when opened. Both share
   the same translate, so Dismiss is a pure fade with zero movement. */
.sound.is-min .sound__panel {
  transform: translate(var(--sound-dx, 0px), var(--sound-dy, 0px)) scale(0.985);
}
.sound.is-min.is-open .sound__panel {
  opacity: 1;
  transform: translate(var(--sound-dx, 0px), var(--sound-dy, 0px)) scale(1);
  pointer-events: auto;
  transition: opacity 0.35s var(--ease), transform 0.45s var(--ease);
}

.sound__hello {
  font-family: var(--f-display);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  line-height: 0.9;
  font-size: clamp(1.75rem, 3.4vw, 2.5rem);
  color: var(--ink);
}
.sound__copy {
  margin-top: 0.35rem;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: color-mix(in oklab, var(--ink) 66%, var(--paper));
  text-wrap: pretty;
}

.sound__btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.95rem;
  padding: 0.6rem 1rem;
  border: 1px solid color-mix(in oklab, var(--ink) 26%, transparent);
  border-radius: 100px;
  font-family: var(--f-ui);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  transition: background 0.35s var(--ease), border-color 0.35s var(--ease),
              color 0.35s var(--ease);
}
.sound__btn:hover { border-color: var(--ink); }
.sound__btn[aria-pressed="true"] {
  background: var(--clay);
  border-color: var(--clay);
  color: var(--paper-lift);
}

/* Four bars that only move while sound is actually coming out. They follow
   `is-live` (there is audio), not `is-on` (the visitor wants audio). On a
   cold load `is-on` is set and `is-live` is not — no browser starts audio
   unprompted — and bars dancing over silence would be the page telling a
   small lie about itself. The slider thumb below stays on `is-on`: that
   one really is about intent. */
.sound__wave { display: inline-flex; align-items: flex-end; gap: 2px; height: 11px; }
.sound__wave i {
  width: 2px;
  height: 3px;
  background: currentColor;
  border-radius: 1px;
  transition: height 0.3s var(--ease);
}
.sound.is-live .sound__wave i { animation: eq 1.05s var(--ease-io) infinite; }
.sound.is-live .sound__wave i:nth-child(2) { animation-delay: 0.14s; }
.sound.is-live .sound__wave i:nth-child(3) { animation-delay: 0.28s; }
.sound.is-live .sound__wave i:nth-child(4) { animation-delay: 0.42s; }
@keyframes eq {
  0%, 100% { height: 3px; }
  50%      { height: 11px; }
}
@media (prefers-reduced-motion: reduce) {
  .sound.is-live .sound__wave i { animation: none; height: 7px; }
}

.sound__tracks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.9rem;
}
.sound__track {
  padding: 0.42rem 0.7rem 0.38rem;
  border: 1px solid color-mix(in oklab, var(--ink) 18%, transparent);
  border-radius: 100px;
  font-family: var(--f-ui);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 58%, #fff);
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease),
              background 0.3s var(--ease);
}
.sound__track:hover {
  color: var(--ink);
  border-color: color-mix(in oklab, var(--ink) 40%, transparent);
}
.sound__track.is-active {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper-lift);
}

.sound__vol {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) 2ch;
  align-items: center;
  gap: 0.7rem;
  margin-top: 1rem;
  padding-top: 0.9rem;
  border-top: 1px solid color-mix(in oklab, var(--ink) 14%, transparent);
}
.sound__vol .u-label { color: color-mix(in oklab, var(--ink) 50%, var(--paper)); }

.sound__range {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 14px;
  background: none;
  cursor: pointer;
}
.sound__range::-webkit-slider-runnable-track {
  height: 1px;
  background: color-mix(in oklab, var(--ink) 26%, transparent);
}
.sound__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 11px;
  height: 11px;
  margin-top: -5px;
  border-radius: 50%;
  background: var(--ink);
}
.sound__range::-moz-range-track {
  height: 1px;
  background: color-mix(in oklab, var(--ink) 26%, transparent);
}
.sound__range::-moz-range-thumb {
  width: 11px; height: 11px; border: 0;
  border-radius: 50%; background: var(--ink);
}
.sound.is-on .sound__range::-webkit-slider-thumb { background: var(--clay); }
.sound.is-on .sound__range::-moz-range-thumb { background: var(--clay); }

/* ── room for a finger ───────────────────────────────────────────────
   The card's controls are set small on purpose and they read well, but a
   31px pill and a 14px slider are both under what a thumb can reliably
   land on. Only the box grows: the pills keep their padding and centre
   their own text, and the slider's 1px track stays centred in a taller
   input, so nothing moves. Pointer devices are untouched. */
@media (hover: none) {
  .sound__btn,
  .sound__track { min-height: 2.75rem; }
  .sound__range { height: 2.75rem; }
}

/* A real button now, not a faint text link — full width so it reads as the
   card's way out rather than an afterthought. */
.sound__dismiss {
  display: block;
  width: 100%;
  margin-top: 1.1rem;
  padding: 0.72rem 1rem 0.68rem;
  border: 1px solid color-mix(in oklab, var(--ink) 24%, transparent);
  border-radius: 100px;
  font-family: var(--f-ui);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease),
              color 0.3s var(--ease);
}
.sound__dismiss:hover {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper-lift);
}

@media (max-width: 52rem) {
  .sound__panel { bottom: clamp(4.5rem, 11vh, 7rem); }
  .sound__copy { font-size: 0.875rem; }
}
