/* ==========================================================================
   MMG — landing page
   Ported from Figma "Desktop" (MW2vv5LZJ31VD4HZAjFGjv, node 2:47).
   Desktop is pixel-faithful to the 1512pt artboard; below 1180px the layout
   reflows to single-column.

   Display face: Crake Trial, self-hosted, same as the artboard. The trial cut
   carries only A–Z a–z 0–9 , - . so apostrophes, + and % fall through to
   Italiana — which is exactly the substitution Figma performs on the artboard.
   Replace fonts/CrakeTrial-*.woff2 with the licensed web cut when you buy it;
   no other change is needed.
   ========================================================================== */

@font-face {
  font-family: 'Crake';
  src: url('fonts/CrakeTrial-Light.woff2') format('woff2');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Crake';
  src: url('fonts/CrakeTrial-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Crake';
  src: url('fonts/CrakeTrial-Medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Crake';
  src: url('fonts/CrakeTrial-SemiBold.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Crake';
  src: url('fonts/CrakeTrial-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ══════════════════════════ DESIGN TOKENS ══════════════════════════
   The single source of truth. Components (nav, footer, buttons) expose
   their own --component-* variables built on these, so a colour variant
   is a class that swaps variables — never a rewrite of rules. */
:root {
  /* type — display for headlines, body for copy, mono for eyebrows/labels */
  --font-display: 'Crake', 'Italiana', 'Times New Roman', serif;
  --font-body: 'Raleway', system-ui, -apple-system, sans-serif;
  --font-mono: 'Geist Mono', ui-monospace, 'SF Mono', monospace;

  /* palette — the OFFICIAL brand colours, by their given names */
  --midnight-chaser: #02255b;
  --the-grind: #0b3e71;
  --evening-indigo: #5ba1cb;
  --coastal-mist: #aedae5;
  --audacity: #e9bd7a;
  --ground-state: #ddc191;

  /* palette — semantic aliases the page is written in */
  --cream: #fbf4ea;                    /* page ground (not in the swatch set) */
  --navy: var(--midnight-chaser);
  --navy-hover: #04326f;   /* navy one step up, for pressed/hover fills */
  --blue: #003a6c;
  --ink: #213448;          /* the dark slate used for solid UI fills */
  --ink-hover: #2d4761;
  --white: #fff;
  --black: #000;

  /* palette — supporting */
  --rule: rgba(0, 0, 0, 0.2);         /* hairlines on cream */
  --ink-soft: rgba(0, 0, 0, 0.5);     /* secondary text on cream */
  --glass-light: rgba(255, 255, 255, 0.04);  /* the nav's resting glass */
  --glass-navy: rgba(2, 24, 56, 0.14);       /* the nav's glass over cream */

  /* layout */
  --content: 1272px;
  --gutter: 120px;
}

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

/* the collage rings and hero layers deliberately bleed past the viewport;
   clip (not hidden) contains them without making the page a scroll container */
html { scroll-behavior: smooth; overflow-x: clip; }
/* Anchor targets stop clear of the fixed bar. Without this every in-page
   link (Program, Community, Apply…) parked its section's first ~90px
   underneath the navbar, so you landed mid-heading. */
#features, #how-it-works, #program, #who, #community, #apply, #footer {
  scroll-margin-top: 120px;
}
/* no visible scrollbar, any engine — the page still scrolls exactly the
   same; its motion is the position cue */
html { scrollbar-width: none; }
html::-webkit-scrollbar { width: 0; height: 0; display: none; }

body {
  margin: 0;
  background: var(--cream);
  color: #000;
  font-family: var(--font-body);
  font-weight: 500;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
}

img { display: block; max-width: 100%; }
p, h1, h2, h3, ul { margin: 0; }
ul { padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }

.credibility, .step__inner, .who, .collage, .close, .footer__inner {
  width: min(var(--content), 100% - var(--gutter) * 2);
  margin-inline: auto;
}

/* ── shared atoms ───────────────────────────────────────────────────────── */

.pill {
  display: flex;
  align-items: center;
  gap: 9px;
  height: 37.892px;
  padding: 8px 13px 8px 11px;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 16px;
  line-height: 1;
  white-space: nowrap;
}
/* No backdrop-filter here. It was blurring the sky behind the pill, which read
   as a panel sitting on the hero rather than type sitting in it. The text is
   white on a dark sky and holds without help. */
.pill--light { color: #fff; }
.pill--navy { color: var(--blue); }
.pill--flush { padding-left: 0; }

.pill__dot {
  width: 8.813px;
  height: 8.813px;
  margin-inline: 1.8px;
  border-radius: 31.878px;
  background: currentColor;
  transform: rotate(-45deg);
  flex: none;
  position: relative;
}

/* The dot itself never moves and never dims — a ring swells out of it and
   fades. Animating the dot's own scale or opacity is what reads as blinking;
   sending something outward from a fixed point reads as a pulse.

   This was a box-shadow spread first, and it was invisible for a reason worth
   remembering: the shadow was at 40% opacity when its spread was 0 and fully
   transparent by the time it had any size, so it was never both visible AND
   large enough to see. A real ring on a pseudo-element separates the two — it
   starts at the dot's own size and full strength, then grows and fades. */
.pill__dot::after {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  animation: pill-ping 2.8s cubic-bezier(0.2, 0.6, 0.35, 1) infinite;
  pointer-events: none;
}

@keyframes pill-ping {
  0%   { transform: scale(1);   opacity: 0.65; }
  70%  { transform: scale(2.9); opacity: 0; }
  100% { transform: scale(2.9); opacity: 0; }
}

.eyebrow {
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 16px;
  line-height: 1;
  text-transform: uppercase;
  color: rgba(0, 0, 0, 0.6);
}
.eyebrow--navy { color: var(--navy); }

/* ══════════════ BUTTON (primitive) ══════════════
   One base, colour variants that only swap the variables. A new button
   colour is four custom properties, never a new rule set. */
.btn {
  /* colour API */
  --btn-bg: transparent;
  --btn-bg-hover: var(--btn-bg);
  --btn-ink: var(--white);
  --btn-edge: transparent;                 /* 1px outline (ghost) */
  --btn-ring: transparent;                 /* 1px inset keyline (solid fills) */
  --btn-lift: 0 10px 22px -8px rgba(2, 15, 40, 0.35);  /* hover shadow */

  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 48px;
  height: 48px;
  padding: 16px;
  border: 1px solid var(--btn-edge);
  border-radius: 9999px;
  background: var(--btn-bg);
  color: var(--btn-ink);
  box-shadow: inset 0 0 0 1px var(--btn-ring);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 13px;
  line-height: 16px;
  text-align: center;
  white-space: nowrap;
  /* the BASE transition is the EXIT: the decel curve dumps most of its
     travel in the first ~80ms, which is right for the way in (pop) and
     wrong for the way out (cut) — so leaving glides home on a longer,
     even clock, and the :hover rule re-arms the quick entry */
  transition: transform 0.5s ease, box-shadow 0.5s ease,
              background-color 0.35s ease, opacity 0.18s ease;
}
/* A hover is three things at once, or it reads as a colour swatch changing:
   the button comes TOWARD you (lift + shadow underneath), and the fill
   shifts. The press answers back — down to rest, shadow gone, faster than
   the way up, which is what makes it feel mechanical rather than painted. */
.btn:hover {
  transform: translateY(-2px);
  background: var(--btn-bg-hover);
  box-shadow: inset 0 0 0 1px var(--btn-ring), var(--btn-lift);
  transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 0.22s cubic-bezier(0.22, 1, 0.36, 1),
              background-color 0.22s ease, opacity 0.18s ease;
}
.btn:active {
  transform: translateY(0);
  box-shadow: inset 0 0 0 1px var(--btn-ring);
  transition-duration: 0.09s;
}
.btn--ghost { --btn-edge: rgba(255, 255, 255, 0.7); --btn-bg-hover: rgba(255, 255, 255, 0.12); --btn-lift: 0 8px 18px -8px rgba(0, 0, 0, 0.3); }
.btn--white { --btn-bg: var(--white); --btn-ink: var(--black); --btn-ring: rgba(0, 0, 0, 0.09); --btn-bg-hover: #f1e9db; }
.btn--navy { --btn-bg: var(--navy); --btn-bg-hover: var(--navy-hover); --btn-ring: rgba(0, 0, 0, 0.09); }

/* ── hero ───────────────────────────────────────────────────────────────── */

.hero {
  position: relative;
  /* 1280, not 982. The fade needs somewhere to happen that is NOT the first
     screen: at 982 the hero was barely taller than a viewport, so the
     transition necessarily started above the fold. The extra ~300px is scroll
     you pass through with the globe still whole, and only then does it go.

     Everything positioned against this box is pinned in absolute px rather than
     percentages so the above-the-fold composition is untouched — see .hero__sky
     and centerOffset in globe.js, both re-solved for the new height. */
  height: 1280px;
  overflow: hidden;
  /* #46372e is the sky photograph's own bottom edge, sampled from the file.
     The hero is 1280px but .hero__sky ends at y=994, so 286px of this shows —
     and at #000 that read as a black band cutting across the ground. Matching
     the picture's last row makes the extension invisible: the ground simply
     continues into the fade. Re-sample this if the sky image is ever replaced. */
  background: #46372e;
  isolation: isolate;
}

/* The globe sinks into the page over the hero's last 260px.

   Two things were tried and both were worse. Masking the hero fades the SKY out
   as well, so it reads as the picture being erased rather than the globe
   settling. And running the fade for the whole 387px from the fold down turns
   that stretch to grey haze — half-opacity cream over a near-black globe is
   mud, and the longer it runs the more of it there is.

   260px is short enough to stay clean and, now that the hero is 1280px, it
   begins at 1020px — below the fold on any ordinary window, which is the point.
   The first screen is the hero exactly as it was.

   It has to land on var(--cream) by 88% rather than on the last pixel row: a
   hair short of solid and the straight cut returns as a faint line.

   pointer-events: none because this sits over the part of the canvas you are
   most likely to grab when dragging the globe. */
.hero::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 260px;
  z-index: 3;
  pointer-events: none;
  /* A continuous 0 to 100% ramp. It landed on full cream at 88% before and held
     flat for the last 12%, which put a band of solid colour at the foot of the
     fade instead of letting it run all the way out. The intermediate stops are
     there to ease it — a straight two-stop ramp over 260px of near-black bands
     visibly — and the last one is exactly var(--cream), which is the page, so
     there is no join to see. */
  background: linear-gradient(to bottom,
    rgba(251, 244, 234, 0) 0%,
    rgba(251, 244, 234, 0.14) 28%,
    rgba(251, 244, 234, 0.42) 55%,
    rgba(251, 244, 234, 0.76) 79%,
    var(--cream) 100%);
}

.hero__sky {
  position: absolute;
  left: 50%;
  /* 413px absolute: this was calc(50% - 78px), which resolved to 413 against
     the old 982px hero. Held as a flat number so a taller hero does not move
     the sky down with it. */
  top: 413px;
  width: 1758px;
  height: 1162px;
  transform: translate(-50%, -50%);
  filter: blur(5px);
}
.hero__sky img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scaleX(-1);
}
.hero__sky-veil {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.5) 14.587%, rgba(0, 0, 0, 0.25) 100%);
}

.hero__globe {
  position: absolute;
  left: 50%;
  top: 530.94px;
  width: 1656px;
  height: 1538.275px;
  transform: translateX(-50%);
  max-width: none;
  mix-blend-mode: plus-lighter;
  pointer-events: none;
}
.hero__globe--2 { opacity: 0.3; }

/* The particle globe covers the whole hero and places its own sphere inside
   that box (see `diameter` / `centerOffset` in globe.js).

   Normal blending, NOT the plus-lighter the photographic layers use. Under
   plus-lighter the ocean dots and the ocean body are near black, so they add
   almost nothing and drop out — you get only the bright peaks and the orb reads
   as scattered dust rather than a dotted globe. The globe carries its own
   translucent body instead, which is what the reference does. */
canvas.hero__globe {
  inset: 0;
  top: auto;
  width: 100%;
  height: 100%;
  transform: none;
  mix-blend-mode: normal;
  touch-action: pan-y;
  cursor: grab;
  pointer-events: auto;
}
canvas.hero__globe:active { cursor: grabbing; }

/* The photographic globe is a FALLBACK ONLY and is hidden by default. It used
   to show first and cross-fade out once the canvas started, which meant the
   hero always played a photo and then swapped to the real thing. Now nothing
   stands in for the globe: if WebGL2 is there you only ever see the globe, and
   the photo appears solely when the script marks the hero as failed. */
img.hero__globe { opacity: 0; }
.globe-fallback img.hero__globe--1 { opacity: 1; }
.globe-fallback img.hero__globe--2 { opacity: 0.3; }


/* nav */
/* ══════════════ NAVBAR (component) ══════════════
   Persistent: fixed to the viewport for the whole page. The variables below
   are its colour API — a variant class only swaps variables. The scroll
   script in index.html applies .nav--on-light over light sections and
   removes it over dark ones. */
.nav {
  /* colour API — defaults are the on-dark (hero) look */
  --nav-bg: var(--glass-light);
  --nav-shadow: none;
  --nav-logo: var(--white);
  --nav-link: rgba(255, 255, 255, 0.72);
  --nav-link-hover: var(--white);
  --nav-link-hover-bg: rgba(255, 255, 255, 0.08);

  position: fixed;
  left: 50%;
  top: 30px;
  transform: translateX(-50%);
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 592px;
  height: 68px;
  padding: 16px;
  border-radius: 20px;
  background: var(--nav-bg);
  box-shadow: var(--nav-shadow);
  backdrop-filter: blur(40px);
  transition: background 0.35s ease, box-shadow 0.35s ease, opacity 0.7s ease 0.95s;
}
/* variants — new colourways go here, one class each */
.nav--on-dark { /* the defaults above */ }
.nav--on-light {
  --nav-bg: var(--glass-navy);
  --nav-shadow: 0 10px 32px rgba(2, 15, 40, 0.08);
  --nav-logo: var(--navy);
  --nav-link: var(--navy);
  --nav-link-hover: var(--navy);
  --nav-link-hover-bg: rgba(2, 37, 91, 0.08);
}
.nav__logo svg { width: 36px; height: 28.557px; color: var(--nav-logo); transition: color 0.35s ease; }

/* ══ LOGO (drawable) ══
   Nav and footer carry the design file's single-stroke mark, same path as
   the preloader's. At rest the line is simply whole; hovering replays the
   opening draw in place — the stroke empties and completes again, on the
   preloader's own curve. 4980 covers the path's measured 4978-unit length. */
.logo-draw path {
  fill: none;
  stroke: currentColor;
  stroke-width: 47.15;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.nav__logo:hover .logo-draw path,
.footer__brand .logo-draw:hover path {
  animation: logo-redraw 1.2s cubic-bezier(0.65, 0, 0.35, 1);
}
@keyframes logo-redraw {
  from { stroke-dasharray: 4980; stroke-dashoffset: 4980; }
  to { stroke-dasharray: 4980; stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .nav__logo:hover .logo-draw path,
  .footer__brand .logo-draw:hover path { animation: none; }
}
.nav__logo { display: flex; align-items: center; width: 124px; padding-inline: 8px; }
.nav__links { display: flex; align-items: center; gap: 12px; }
.nav__links a {
  display: flex;
  align-items: center;
  height: 32px;
  padding: 4px 16px;
  border-radius: 16px;
  font-size: 14px;
  font-weight: 400;
  line-height: 24px;
  color: var(--nav-link);
  transition: color 0.35s ease, background-color 0.18s ease;
}
.nav__links a:hover { color: var(--nav-link-hover); background: var(--nav-link-hover-bg); }
.nav__actions { display: flex; flex: 1 0 0; min-width: 0; align-items: center; justify-content: flex-end; gap: 12px; }
.nav__login, .nav__apply {
  display: flex;
  align-items: center;
  height: 36px;
  padding: 6px 16px;
  border-radius: 20px;
  font-size: 14px;
  font-weight: 500;
  line-height: 24px;
  transition: background-color 0.18s ease;
}
.nav__login { background: rgba(0, 0, 0, 0.1); color: rgba(255, 255, 255, 0.84); }
.nav__login:hover { background: rgba(0, 0, 0, 0.22); color: #fff; }
.nav__apply { background: var(--ink); color: #fff; }
.nav__apply:hover { background: #2d4761; }

/* ══ MOBILE MENU (component) ══
   The 9-dot button replaces the bar's actions on phones; the overlay it
   opens is the whole screen. It sits UNDER the bar (45 vs 50), so the
   logo and the dots stay put and the same button closes it. */
.nav__menu {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  /* a raw <button> ships UA chrome — flatten it to just the dots */
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  border-radius: 16px;
  color: var(--nav-logo);
  transition: color 0.35s ease, background-color 0.18s ease;
}
/* No pill behind the glyph, in any state — the dots and the X are the
   control, and a tap highlight or hover chip around them read as a second
   button inside the bar. -webkit-tap-highlight-color kills the grey flash
   iOS paints on tap, which is what showed up on click. */
.nav__menu,
.nav__menu:hover,
.nav__menu:active,
.nav__menu:focus { background: none; -webkit-tap-highlight-color: transparent; }
/* two glyphs share the button: the dots twist away as the X twists in */
.nav__menu { position: relative; }
.nav__menu svg {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}
.nav__menu-x { opacity: 0; transform: rotate(-45deg); }
html.menu-open .nav__menu-dots { opacity: 0; transform: rotate(45deg); }
html.menu-open .nav__menu-x { opacity: 1; transform: rotate(0deg); }

.menu {
  position: fixed;
  inset: 0;
  z-index: 45;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: var(--navy);
  padding: 140px var(--gutter) 40px;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
html.menu-open .menu { opacity: 1; visibility: visible; transition: opacity 0.35s ease; }
/* the page behind is FROZEN while the menu is up: overflow on both roots
   kills wheel/keyboard scroll, touch-action on the overlay keeps iOS from
   dragging the page through it, overscroll-behavior stops chain-scrolling */
html.menu-open, html.menu-open body { overflow: hidden; }
.menu { touch-action: none; overscroll-behavior: contain; }
/* Over the open menu the bar is NOT a bar: no fill, no shadow, no blur —
   just the logo and the X sitting on the navy field. The chrome only ever
   existed to separate it from the page behind, and here there is no page. */
html.menu-open .nav {
  --nav-bg: transparent;
  --nav-shadow: none;
  --nav-logo: var(--white);
  backdrop-filter: none;
}

.menu__links { display: flex; flex-direction: column; align-items: flex-start; gap: 28px; }
.menu__links a {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(40px, 12vw, 56px);
  line-height: 1;
  color: var(--cream);
  /* each destination rises into place on its own beat once the field is up */
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
html.menu-open .menu__links a { opacity: 1; transform: none; }
html.menu-open .menu__links a:nth-child(2) { transition-delay: 0.06s; }
html.menu-open .menu__links a:nth-child(3) { transition-delay: 0.12s; }
html.menu-open .menu__links a:nth-child(4) { transition-delay: 0.18s; }

/* the preloader's lockup, quoted at the menu's foot */
.menu__lockup { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; color: var(--cream); }
.menu__lockup .logo-draw { width: 44px; height: 35px; }
.menu__word { font-family: var(--font-display); font-size: 40px; line-height: 1; color: var(--cream); }
.menu__word span:first-child { font-weight: 500; }
.menu__word span:last-child { font-weight: 300; }
.menu__tag { font-size: 12px; line-height: 1; letter-spacing: 0.02em; text-transform: capitalize; color: rgba(251, 244, 234, 0.72); }

@media (min-width: 641px) { .menu { display: none; } }

.hero__content {
  position: absolute;
  left: 50%;
  top: 175px;
  transform: translateX(-50%);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 40px;
  width: min(1273px, 100% - var(--gutter) * 2);
}
.hero__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 48px;
  width: 100%;
}
.hero__title {
  width: 681px;
  flex: none;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 100px;
  line-height: 1;
  letter-spacing: -3px;
  text-transform: capitalize;
  color: #fff;
}
.hero__aside { width: 423px; flex: none; display: flex; flex-direction: column; gap: 24px; }
.hero__aside p { font-size: 20px; line-height: 28px; color: #fff; }
.hero__buttons { display: flex; gap: 12px; }

/* ── credibility ────────────────────────────────────────────────────────── */

.credibility {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 40px;
  margin-top: 56px;
  position: relative;
  z-index: 1;
}
.stats { display: flex; align-items: center; gap: 96px; }
/* text-align as well as align-items: the flex centring only places the label
   BOX, so a label long enough to wrap set its own lines flush left inside
   that centred box — which is why the two-line one looked out of step. */
.stat { display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center; }
.stat__figure {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 72px;
  line-height: 1.4;
  color: var(--navy);
}
/* Crake has no + or %, so they fall back to Italiana — nudged up in size and
   pulled in so they sit with the Crake numerals instead of floating off them */
.stat__figure .sym { font-size: 1.06em; margin-inline: -0.05em; }

/* Counting digits change width as they run, which walks the + and % about and
   shifts the whole figure. Tabular figures hold every digit to one width, so
   the line stays put and only the values move. inline-block stops the span
   collapsing its own trailing space while a value is briefly shorter. */
/* the figure is one short token and must never break — a stray wrap puts the
   % on its own line, which is what an over-reserved width once caused */
.stat__figure { white-space: nowrap; }

/* Crake Trial has no tabular figures, so tnum is a request the face cannot
   honour and narrow digits really are narrower. The reserved width therefore
   leaves slack mid-count; centring splits it either side so it reads as optical
   spacing rather than a gap torn open before the suffix. */
.stat__num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  display: inline-block;
  text-align: center;
}

/* Each item resolves on its own scroll position, --r running 0 to 1 (written by
   the script at the foot of index.html). Default 1 is the rest state, so with
   the script gone, or under reduced motion, the section simply reads. The floor
   of 0.1 rather than 0 matches the reference, where a line entering is faint
   rather than absent — you can see it coming. */
.stat,
.giants > *,
.argument__block .eyebrow,
.argument__block h2 .line {
  opacity: calc(0.1 + 0.9 * var(--r, 1));
}

/* the headline's line breaks were <br>; the script wraps each line in a span
   and this keeps them stacked */
.argument__block h2 .line { display: block; }

/* WHY THE LINE RESOLVE NEEDED THIS. The gradient is painted by the h2 and
   clipped to its text, and the glyphs themselves are transparent — so every
   visible pixel comes from the h2's background, one level ABOVE the lines.
   Fading a .line therefore faded a span that paints nothing, and the headline
   sat there at full ink no matter what --r said. (It worked until the display
   gradient arrived: before that the h2 was a plain colour and the ink belonged
   to the line.)

   So once the script has split the headline, each LINE carries the gradient
   instead. Sized to the whole h2 and offset by its own top, the 175deg ramp is
   reconstructed exactly as it was — line one navy, line two running to gold —
   and the same 8s animation runs on the lines so the middle stop still travels.
   The h2's own background is dropped in that case, or it would paint the
   unfaded copy straight over them. --h2h and --lt are written by rebuild(). */
.argument__block h2:has(.line) { background-image: none; }
.argument__block h2 .line {
  background-image: linear-gradient(175deg,
    var(--hg1) 0%,
    var(--hg2) var(--hgp),
    var(--hg3) 100%);
  background-size: 100% var(--h2h, 100%);
  background-position: 0 calc(-1 * var(--lt, 0px));
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  animation: headline-ramp 8s ease-in-out infinite alternate;
}

/* DESCENDERS WERE BEING EATEN. background-clip: text paints only inside the
   element's own box, and line-height: 1 makes that box exactly 1em — while the
   face's box is 1em plus 0.19em of descent. Measured on the built page at 72px:
   the line box is 72px, the baseline sits at 65px, and descender ink reaches
   79.4px, so 7.4px of every p, g and y fell outside the paint and, with the
   fill transparent, simply disappeared — the p of "Map" cut flat.

   The box is extended below and pulled back by the same amount, so the gradient
   reaches the descenders and the layout does not move (padding and margin
   cancel, so the flex outer size is unchanged). 0.18em rather than the 0.14em
   that just clears it: at 0.14em the two-line .close__title measured only 1.6px
   of headroom, which is too thin to survive a copy change or a deeper glyph.

   .argument__block h2 is deliberately NOT here: at line-height 1.3 its ink ends
   at 41px inside a 41.6px box, so it never clipped, and padding it would move
   the --h2h/--lt ramp its split lines are sliced from. */
.who__title,
.collage__text h2,
.close__title,
.step__name {
  padding-bottom: 0.18em;
  margin-bottom: -0.18em;
}

/* The eyebrow and each headline line fan out from the block's centre line as it
   descends. Without it the headline is a rigid slab; with it the lines drift a
   little against each other and the group reads as alive. Transform-only, so
   nothing reflows — the gaps change optically and the layout never moves. */
.argument__block .eyebrow,
.argument__block h2 .line {
  transform: translateY(var(--dy, 0px));
  will-change: transform;
}

/* Parallax lives on the base rules below, because .argument__block already
   carries translateX(-50%) for its centring — a second transform declaration
   would replace it and throw every headline off centre. */
.stat__label { font-size: 16px; font-weight: 400; line-height: normal; color: rgba(0, 0, 0, 0.8); }

.giants { display: flex; align-items: center; gap: 40px; }
.giants__label {
  width: 130px;
  flex: none;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: normal;
  color: rgba(0, 0, 0, 0.5);
}
.giants__title {
  font-family: var(--font-display);
  font-size: 22px;
  line-height: normal;
  color: var(--navy);
  white-space: nowrap;
}

/* ── premise / failure / answer ─────────────────────────────────────────── */

.argument {
  position: relative;
  width: min(1512px, 100%);
  margin: 55px auto 0;
  height: 1792px;
}
/* columns are 437 / 639 / 436 of the 1512 artboard, kept proportional so the
   grid survives the 1180–1512 range before the layout stacks */
/* The stack, bottom to top: text (1) → veil (2) → rules (3). The text rides
   under the veil; the veil swallows it into the background near the rules;
   the rules stay crisp above everything.

   Every line breathes 0 → 100 → 0 along its own length: it is born from
   nothing, carries full ink through the middle, and dies back into the page —
   no line ever just stops. */
.rule { position: absolute; z-index: 3; }

.rule--h {
  left: 0;
  width: 100%;
  height: 1px;
  background: linear-gradient(to right, transparent 0, var(--rule) 50%, transparent 100%);
}

/* The verticals begin in mid-air 55px above the section and used to stop dead
   at both ends — a hairline with two blunt terminations, which is what made the
   grid read as stuck on top of the page rather than part of it. They fade in
   and out over 140px instead, so the line arrives and leaves rather than
   starting and stopping. The horizontals need none of this: they run full bleed
   and terminate off the side of the viewport, where there is no end to see. */
.rule--v {
  top: -55px;
  bottom: 0;
  width: 1px;
  background: linear-gradient(to bottom, transparent 0, var(--rule) 50%, transparent 100%);
}
.rule--v1 { left: 28.9021%; }
.rule--v2 { left: 71.164%; }

/* Each photo IS a grid cell: its edges sit on the horizontal rules at 0 / 530 /
   1060 / 1590 and on the verticals at 28.9021% and 71.164%. So the cell is a
   frame that never moves and always clips, and the parallax happens to the
   picture inside it. The frame is what holds the photo on the grid.

   This replaces a version that scaled the photo 1.11 and translated the whole
   element: that pushed all four edges off the grid at once, so the photo
   crossed the vertical rule and broke the horizontals it was supposed to meet. */
.argument__frame {
  position: absolute;
  width: 28.9021%;
  /* 530, not 529. The rules sit at 0 / 530 / 1060 / 1590, so a 529px cell
     stopped one pixel short of the rule beneath it and left a hairline of cream
     under every photo. The width already meets rule--v1 exactly at 28.9021%;
     this makes the bottom edge meet its rule the same way. */
  height: 530px;
  overflow: hidden;
}
.argument__frame--left { left: 0; }
.argument__frame--right { left: auto; right: 0; width: 28.836%; }

/* NO BOX AT ALL up here. The cell is the phone's clipping cut (see the phone
   block); display: contents means it generates nothing on the desktop grid, so
   the picture's containing block is still the frame and every measurement on
   this page is the one it was before the cell existed. */
.argument__cell { display: contents; }

/* The picture is 360px taller than its frame and sits 180px above it, so it
   overhangs 180px at each end. --q runs 0 to 1 across the cell's whole traverse
   of the viewport (written by the script in index.html), and the picture
   travels ±150px against that 180px of overhang — 30px of cover in hand at both
   extremes, so the frame can never show through however hard this is driven.

   The source photographs are 1680 × 720. A 530px cell about 437px wide needs
   them scaled to cover, and at 890px tall that is a 1.24× vertical upscale —
   the practical ceiling. More travel than this needs taller source files, not
   a bigger number here.

   The picture DESCENDS as its cell rises — it lags the frame carrying it, the
   way a distant background does. The text descends too but travels 380px to
   this 200px, so they move together at different rates and the difference is
   the parallax.

   To push it harder raise the travel and the overhang TOGETHER: travel must
   stay under the overhang. 0.5 is the resting position, so with the script
   absent the picture sits centred in its cell. */
.argument__img {
  position: absolute;
  left: 0;
  top: -180px;
  width: 100%;
  height: calc(100% + 360px);
  object-fit: cover;
  transform: translateY(calc((var(--q, 0.5) - 0.5) * 300px));
  will-change: transform;
}

.argument__block {
  position: absolute;
  left: 50%;
  /* Centring AND drift in one declaration: .argument__block needs translateX
     for its centring, and a second transform property would replace it rather
     than add to it, throwing every headline off centre.

     --s is the block's own band progress, not its position on screen: 0 as the
     reference line enters its band, 1 as that line leaves. So a headline
     crosses its whole cell over exactly the stretch of scroll it owns, and
     lands as the next one starts.

     380px is the whole band. Each block is 114px tall at 207px inside a 530px
     cell — dead centre, since (530-114)/2 is 208 — so ±190px carries it from
     17px under the rule above to 19px above the rule below. There is no more
     travel available without the text colliding with a rule.

     --vis defaults to 1, so with the script gone, under reduced motion, or
     below 1180px, all three headlines simply read. */
  transform: translateX(-50%) translateY(calc((var(--s, 0.5) - 0.5) * 380px));
  opacity: var(--vis, 1);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 15px;
  width: min(479px, 38.4%);
  text-align: center;
}
.argument__block--wide { width: min(581px, 38.4%); }
.argument__block h2 {
  font-size: 32px;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.96px;
  color: #000;
}

/* ── system intro ───────────────────────────────────────────────────────── */

/* The intro fills the viewport and pins there while the steps ride up over it.
   .stage is its containing block, so it unsticks once the last step has gone
   past instead of staying stuck for the rest of the page.

   The steps must paint ON TOP of it. Both are positioned elements, so with no
   z-index the later one in the document wins — which is the steps — but the
   z-index is stated anyway because this only reads correctly in one order and
   it should not depend on source position to stay that way. */
.stage { position: relative; }

.system {
  position: sticky;
  top: 0;
  min-height: 100vh;
  overflow: hidden;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 0;
}
.steps { position: relative; z-index: 1; }
/* the artboard scales this fill to 153.91% × 141.9% and offsets it up-left,
   which is what puts the doorway of light behind the headline */
.system__bg { position: absolute; inset: 0; overflow: hidden; }
.system__bg img {
  position: absolute;
  left: -26.93%;
  top: -10.99%;
  width: 153.91%;
  height: 141.9%;
  max-width: none;
  object-fit: cover;
}
.system__bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.2);
}
/* The intro stays pinned behind the steps, which is the point — but its
   HEADLINE should not. Once the cards are covering the screen the only place
   the text can appear is in the 32px gaps between them, where it reads as a
   stray line of type behind the page. The sky stays; the words go. --sys is
   written by the script as the steps arrive. */
.system__content {
  opacity: var(--sys, 1);
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 60px;
  width: min(599px, 100% - var(--gutter) * 2);
  text-align: center;
}
.system__lead { display: flex; flex-direction: column; align-items: center; gap: 32px; }
.system__title {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 72px;
  line-height: 1;
  letter-spacing: -2.16px;
  text-transform: capitalize;
  color: #fff;
  /* --sysT is the entrance, written by the script from the stage's climb:
     the headline condenses out of blur and rises, resolved before the
     paragraph's words start arriving. Defaults keep it whole without JS. */
  opacity: var(--sysT, 1);
  transform: translateY(calc((1 - var(--sysT, 1)) * 26px));
  filter: blur(calc((1 - var(--sysT, 1)) * 14px));
}
.system__body { font-size: 20px; line-height: 28px; color: #fff; }

/* ── steps ──────────────────────────────────────────────────────────────── */

.steps { display: flex; flex-direction: column; gap: 32px; }
.steps__stack { display: flex; flex-direction: column; gap: 32px; }
.step__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 66px;
  height: 801px;
}
.step__text { width: 599px; flex: none; display: flex; flex-direction: column; align-items: flex-start; gap: 48px; }
.step__name {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 72px;
  line-height: 1;
  letter-spacing: -2.16px;
  text-transform: capitalize;
  color: var(--navy);
}
.step__body { display: flex; flex-direction: column; gap: 24px; }
.step__body p:last-child { width: 538px; max-width: 100%; font-size: 20px; line-height: 28px; color: rgba(0, 0, 0, 0.6); }
.step__media { width: 607px; height: 641px; flex: none; border-radius: 12px; overflow: hidden; }
.step__media img { width: 100%; height: 100%; object-fit: cover; }

/* ── steps: pinned stack (desktop only) ─────────────────────────────────────
   Each panel is sticky at the top of its own containing block, so panel N+1
   rises up and covers panel N rather than replacing it. The margin below each
   panel is the dwell — the stretch of scroll where a panel sits alone before
   the next one starts to climb over it.

   --p is written per panel by the script at the foot of index.html: 0 when the
   panel's top edge is at the bottom of the viewport, 1 once it is pinned. Every
   default here is the rest state, so with the script absent (or reduced motion,
   or a narrow viewport) the section is simply four static panels. */
@media (min-width: 1181px) {
  /* The header is what pins; the cards do not. They scroll up over it as
     ordinary cards on a navy field that never moves — which is the artboard's
     own arrangement, and it replaces a version where each panel pinned itself
     full-height and the four stacked one over another.

     Geometry is the artboard's: cards 40px in from the page edge, 801px tall,
     32px apart, on #213448. Their 80px padding plus that 40px inset puts the
     content at 120px, which is the page gutter — so the text column lines up
     with every other section even though the card does not. */
  /* Cards again, but with nothing painted behind them: the section is
     transparent, so what shows between and around them is the sticky header
     still sitting there. The cards are the only cream on screen, and the sky
     runs the whole length of the section behind them.

     They sit on the page's own measure rather than the artboard's 40px inset —
     card edge to card edge is the same 1272 as every other section, so there is
     one margin down the page. The 64px padding is the artboard's 80 scaled by
     the same ratio the card was (1272/1432), so the inside keeps its
     proportions even though the card is narrower than the artboard's. */
  .steps {
    width: min(var(--content), 100% - var(--gutter) * 2);
    margin-inline: auto;
    display: flex;
    flex-direction: column;
    gap: 32px;
    /* THE RUNWAY (the NatGeo lesson): 40vh of scroll where the pinned intro
       simply HOLDS — sky and finished words, no card yet. Without it the
       first card began rising the instant the intro pinned, and the page's
       most cinematic moment had zero dwell. The steps script measures all
       its geometry live, so the chain needs no retuning; the intro's word
       windows in index.html are extended to pour into this stretch. */
    padding-top: 40vh;
    padding-bottom: 120px;
  }

  /* The CHAIN, at TRUE 1:1. Every later card overlaps card one's flow slot
     completely (-833px = card 801 + gap 32), so all four are pinned from the
     moment the first one locks — and the steps script then admits them ONE
     AT A TIME: card 2's slide begins the instant card 1 locks, card 3's the
     instant card 2 lands, and so on. One card in flight, never two. A card
     is offscreen right until its turn, so the stacked flow never shows. */
  .steps__stack { display: flex; flex-direction: column; gap: 32px; }
  .steps__stack .step + .step { margin-top: -833px; }
  /* The room the chain needs: exactly three slides (one per later card) and
     not a pixel more — the moment the last card lands, the whole deck
     releases and rides on. A CONTENT spacer, not padding — a sticky element
     is bounded by its parent's content box, so padding-bottom never extended
     the range. */
  .steps__stack::after { content: ''; height: calc(var(--slide, 880px) * 3 - 70px); flex: none; }

  /* The deck: each card pins at the same line, so the one behind sits still
     while the next rides up and lands exactly over it. The geometry closes
     itself: equal heights and one shared top mean the last card reaches the
     line just as the stack's bottom edge arrives, so all four release together
     and leave as one. The card is 801px tall (the artboard's height — the
     photo plus the inner padding), and the top centres that in the viewport;
     on screens shorter than the card it overhangs the top rather than the
     bottom, so the release is what you see. */
  .step {
    position: sticky;
    /* sits a little above centre so the card keeps air under it, but never
       higher than 104px — the fixed nav ends at 98px */
    top: max(104px, 50vh - 445px);
    background: var(--cream);
    border-radius: 24px;
    padding: 0 64px;
    overflow: hidden;
    /* Cream cards landing on cream cards: the overlap has no edge of its own,
       so the shadow IS the separation. Weighted to the left because every card
       after the first arrives from the right and its left edge is the seam you
       actually see; the softer downward pass keeps the stack sitting on the
       sky rather than floating over it. Tinted --midnight-chaser instead of
       black so it stays inside the palette. */
    box-shadow:
      -34px 0 70px -28px rgba(2, 37, 91, 0.42),
      0 26px 64px -30px rgba(2, 37, 91, 0.34);
    /* The approach is PURELY horizontal for every card after the first — and
       the vertical hold is the browser's own sticky, never a script chasing
       it (a per-frame translateY correction fought the compositor and made
       the cards dance). A card climbs to its pin OFFSCREEN to the right,
       pins, and only then slides in — so on screen it only ever moves
       horizontally. The first card keeps --px at rest and arrives from
       below. */
    transform: translateX(calc((1 - var(--px, 1)) * 105vw));
    will-change: transform;
  }

  /* the card is the container now, so the inner row simply fills it */
  .step__inner { width: 100%; max-width: none; margin-inline: 0; }

  /* the artboard pair is 599 + 66 + 607 = 1272, exactly the content measure at
     1512px and wider. Between 1181px and 1400px the gutter drops to 64px and
     the measure is narrower than that, so let both columns give rather than run
     the photo off the right edge. No effect at 1400px and up, where they fit. */
  .step__text { flex: 0 1 599px; min-width: 0; gap: 48px; }
  /* 641 flat, not a viewport fraction. The card is a fixed 801px again rather
     than a full-height pinned panel, so the photo no longer has to shrink to
     fit a short screen. */
  .step__media { flex: 0 1 607px; min-width: 0; height: 641px; }

  /* The reveal survives the pin being removed: the script's progress is
     1 - top/vh, which still runs 0 to 1 as a card climbs the screen. It is now
     an entrance rather than something staged against a pin. */
  .step__media {
    clip-path: inset(calc((1 - var(--pw, 1)) * 100%) 0 0 0 round 12px);
  }
  /* the photo rides slower than the card it sits in, so it reads as depth */
  .step__media img {
    transform: scale(1.1) translateY(calc((1 - var(--p, 1)) * 38px));
    will-change: transform;
  }

  /* The copy arrives from below AND out of focus: the blur rides the same
     staged variable as the lift, so both resolve together and there is no
     second timeline to drift against. 12px is past the legibility edge on
     purpose — the words condense out of light rather than fading in. */
  .step__text > .eyebrow,
  .step__name {
    opacity: var(--pt, 1);
    transform: translateY(calc((1 - var(--pt, 1)) * 22px));
    filter: blur(calc((1 - var(--pt, 1)) * 12px));
  }
  .step__body {
    opacity: var(--pb, 1);
    transform: translateY(calc((1 - var(--pb, 1)) * 22px));
    filter: blur(calc((1 - var(--pb, 1)) * 12px));
  }

  /* the section after this is static, and the cards overlap the header, so it
     needs its own layer to be sure it paints over anything that runs long */
  .who { position: relative; z-index: 1; }
}

/* The cards are ordinary scrolling cards now, so nothing here needs undoing
   except the entrance itself: no unveil, no lift, everything at its rest state.
   The header's pin stays — it is layout, not motion. */
@media (min-width: 1181px) and (prefers-reduced-motion: reduce) {
  .step__media { clip-path: none; }
  .step__media img { transform: none; }
}

/* ── who it's for ───────────────────────────────────────────────────────── */

.who { display: flex; flex-direction: column; gap: 100px; padding-top: 83px; }
.who__head { display: flex; align-items: flex-end; justify-content: space-between; gap: 80px; }
.who__head > div { width: 438px; flex: none; display: flex; flex-direction: column; gap: 32px; }
.who__head .pill { padding: 0; }
.who__title {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 64px;
  line-height: 1;
  letter-spacing: -1.92px;
  text-transform: capitalize;
  color: var(--navy);
}
.who__aside { width: 411px; flex: none; font-size: 20px; line-height: 28px; color: #000; }

/* Full-bleed window. The row is wider than the page and slides inside it as the
   section crosses the viewport, so cards enter from one edge and leave by the
   other rather than sitting still in a gutter. */
.who__rail {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  overflow: hidden;
}
/* The ring is draggable where the script runs it. touch-action keeps the
   VERTICAL axis native — a thumb can still scroll the page across the rail;
   only sideways movement belongs to the ring. Text selection is suspended
   solely while a drag is actually happening, or letting go after a pull
   would leave a name highlighted. */
@media (min-width: 1181px) {
  .who__rail:has(.who__row--live) { cursor: grab; touch-action: pan-y; }
  .who__rail.is-dragging { cursor: grabbing; }
  .who__rail.is-dragging .member {
    user-select: none;
    -webkit-user-select: none;
  }
}
/* Flex is the fallback, not the mechanism. With no JS or under reduced motion
   this stays a plain centred row of cards. The script switches it to display:
   block and each card places itself, because the reference's slots are not
   evenly pitched and no flex row can produce that. */
.who__row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  height: 542px;
}

/* One size in the markup; the hierarchy is entirely positional.

   --e is 1 for whichever card is passing the centre of the screen and falls to
   0 at the edges. 0.55 to 1.0 is measured off the reference, where the cards
   run 142 / 148 / 203 / 212 / 256 px across a 1100px frame — the outermost is
   0.55 of the middle one. Scaled about the centre, not the base, because the
   cards sit on a common horizontal axis rather than on a shelf. */
/* In the fallback these sit in the flex row above. Once the script takes over
   the row it places them itself, writing position and scale into one transform
   off one number, so the two cannot disagree with each other. */
.who__row--live { display: block; }
.who__row--live .member { position: absolute; left: 50%; top: 50%; }

.member {
  position: relative;
  flex: none;
  width: 352px;
  height: 542px;
  will-change: transform;
  border-radius: 20px;
  overflow: hidden;
  background:
    linear-gradient(90deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0.5) 100%),
    linear-gradient(180deg, rgb(147, 180, 231) 0%, rgb(216, 198, 188) 100%);
}

/* each cutout keeps the artboard's own fill treatment: the two small cards
   stretch the source to the frame, the mid cards crop it off-centre, the hero
   card covers. Matching these one for one is what keeps the row on-model. */
.member__frame { position: absolute; left: 50%; transform: translateX(-50%); overflow: hidden; }
.member__frame img { position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; object-fit: fill; }

/* Every cutout now stands to one common figure height. The widths are that
   height times each photograph's own aspect, taken from the artboard's pairs
   (80×238, 97×323, 91×238), so nobody is stretched to match anybody else. */
.member__frame { height: 323px; top: 172px; }
.member__frame--b { width: 108.6px; }
.member__frame--c { width: 97px; }
.member__frame--d { width: 123.5px; }
.member__frame--c img { object-fit: cover; }
.member__frame--b img { inset: auto; left: -55.12%; top: 0.12%; width: 193.66%; height: 99.79%; }
.member__frame--d img { inset: auto; left: -46.26%; top: 0.12%; width: 170.25%; height: 99.79%; }

/* The reference shows no text at all on its outermost cards — at that size it
   would only be noise. This clears by --e 0.35 and is fully there by 0.70;
   opacity clamps its own range, so no clamp() is needed. */
.member__meta {
  position: absolute;
  left: 40px;
  top: 40px;
  width: 272px;
  display: flex;
  flex-direction: column;
  gap: 20px;
  /* The reference carries no text at all on its ±2 cards, which sit at scale
     0.552, and full text on its ±1 cards at 0.800. So the threshold belongs
     between those two, not at the extremes. opacity clamps its own range. */
  opacity: calc((var(--e, 1) - 0.6) / 0.16);
}
.member__name { font-family: var(--font-display); font-weight: 400; line-height: 1; color: var(--navy); font-size: 24px; letter-spacing: -0.72px; }
.member__deed { font-weight: 500; line-height: normal; color: rgba(0, 0, 0, 0.7); font-size: 24px; letter-spacing: -0.72px; }
.member__deed b { font-weight: 500; color: var(--navy); }

/* ── collective minds ───────────────────────────────────────────────────── */

/* Several screens tall, with a stage pinned inside it. The height is the scroll
   BUDGET — how long the ring has to turn — and the stage is what you actually
   look at while it does. 260vh gives about a screen and a half of travel after
   the stage arrives, which is what turns this from a section you pass into one
   you sit inside.

   The stage is not clipped: the ring is wider than a short viewport is tall, and
   letting the tiles bleed past the edges is the point of it. */
/* 320px, not 160. The declared gap is not the gap you see: the tiles orbit at
   a 460px radius from the stage's centre, and before the stage pins that centre
   sits half a viewport down the section — so the topmost tile reaches back above
   the section's own top edge and eats into the space. At 160 the real clearance
   from the member row was 147px, which is nothing between two sections this
   busy. */
/* 200vh, down from 260: the words finish writing about half-way through the
   traverse, and the stretch after them was all ring and no news */
.collage { position: relative; height: calc(200vh - 200px); margin-top: 320px; }
.collage__stage {
  position: sticky;
  top: 0;
  height: 100vh;
}
.collage__ring {
  position: absolute;
  width: 708.804px;
  height: 708.804px;
  border: 0.68px solid #d9d9d9;
  border-radius: 50%;
}
.collage__ring--l { left: -375px; top: 90.98px; }
.collage__ring--r { left: 793.73px; top: 508.65px; }

/* The eight tiles ride a circle around the text and turn on it as you scroll.

   Each keeps its own size — the varied sizes are what stop it reading as a
   clock face — but position is no longer authored. left/top pin every tile to
   the centre of the section and the script writes --ox / --oy, its offset along
   the ring. The -50% keeps the tile centred on its own point, so a tile at the
   top of the circle sits astride it rather than hanging from it.

   The tiles never rotate themselves: they travel a circular path while staying
   upright, which is the difference between an orbit and a fairground wheel. */
.collage__img {
  position: absolute;
  left: 50%;
  top: 50%;
  border-radius: 20px;
  object-fit: cover;
  background: #d9d9d9;
  transform: translate3d(calc(-50% + var(--ox, 0px)), calc(-50% + var(--oy, 0px)), 0);
  will-change: transform;
}
/* A tile's own trail. Blurred, so it reads as a slow shutter rather than as
   three copies of the same picture, and inert to the pointer. Opacity is
   written per frame from how fast the ring is turning; 0 at rest. */
/* No blur here. A 4px blur on two dozen large images, re-rasterised every time
   their transform changes — which is every scroll frame — is one of the most
   expensive things this page could ask for, and it was asking for it precisely
   while scrolling, when the frame budget is already spent. The trail reads from
   opacity falloff alone. */
.collage__echo {
  opacity: 0;
  pointer-events: none;
}

.c-1 { width: 157.822px; height: 197.459px; }
.c-2 { width: 139.427px; height: 174.105px; }
.c-3 { width: 209px;     height: 276px; }
.c-4 { width: 275.947px; height: 174.105px; }
.c-5 { width: 246.721px; height: 286.721px; }
.c-6 { width: 258px;     height: 306px; }
.c-7 { width: 203.557px; height: 231.543px; }
.c-8 { width: 146px;     height: 184px; }

.collage__text {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 40px;
  width: 394px;
  text-align: center;
}
.collage__text h2 {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 64px;
  line-height: 1;
  letter-spacing: -1.92px;
  text-transform: capitalize;
  color: var(--navy);
}
.collage__text p { width: 368px; max-width: 100%; font-size: 20px; line-height: 28px; color: #000; }

/* ── close / cta ────────────────────────────────────────────────────────── */

.close { display: flex; flex-direction: column; gap: 100px; margin-top: 160px; }
.close__head { display: flex; align-items: flex-end; justify-content: space-between; gap: 144px; }
.close__lead { width: 705px; flex: none; display: flex; flex-direction: column; gap: 32px; }
.close__title {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 64px;
  line-height: 1;
  letter-spacing: -1.92px;
  text-transform: capitalize;
  color: var(--navy);
}
.close__aside { display: flex; flex-direction: column; align-items: flex-start; gap: 20px; }
.close__aside p { width: 413px; max-width: 100%; font-size: 20px; line-height: 28px; color: #000; }

.close__row { display: flex; align-items: center; gap: 20px; height: 612px; }
.close__card { position: relative; flex: 1 1 0; min-width: 0; height: 100%; border-radius: 20px; overflow: hidden; }

/* 2.616 is the artboard's own ratio between the 565px card and the 216px ones,
   expressed as flex-grow instead of a fixed width. Stating it that way is what
   lets the wide slot move: at rest the composition is exactly the one that was
   designed, and pointing at a card only changes which card is holding it. */
.close__card--wide { flex-grow: 2.616; }
.close__card img { width: 100%; height: 100%; object-fit: cover; }

/* The row squeezes, after Stripe's carousel. Its canvas is 1232 CSS px wide and
   its active card measures about 809 of that — two thirds — with the rest
   compressed to slivers beside it. flex-grow 6 against 1 puts ours at 808 of a
   1272 measure with three 135px slivers, which is the same proportion arrived
   at from our own geometry rather than by copying a pixel value.

   That is a much harder squeeze than the resting 2.616, and the point of it:
   the difference between a card politely widening and a row that visibly gives
   way to the one you are looking at.

   Hover and click do DIFFERENT things, which is the Stripe behaviour. Hovering
   only peeks — the sliver widens enough to say it can be opened and the active
   card gives a little ground. Clicking is what actually opens it, and the
   selection then STAYS: a :hover rule snaps back the instant the pointer
   leaves, which reads as the row resisting you.

   1.8 against 1 turns a 135px sliver into about 222px while the open card eases
   from 808 to 742. Enough to feel answered, not enough to be mistaken for
   having opened. */
@media (min-width: 1181px) {
  /* Decelerating hard, so an opening card covers most of its distance almost
     at once and then settles — the long tail is what makes it read as the row
     giving way rather than a width animating. One curve does both the peek and
     the open because the distances differ, not the timing. */
  .close__row--live .close__card { transition: flex-grow 0.55s cubic-bezier(0.22, 1, 0.36, 1); cursor: pointer; }
  .close__row--live .close__card img { transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1); }
  .close__scrim,
  .close__caption { transition: opacity 0.4s ease; }

  .close__row--live .close__card { flex-grow: 1; }
  .close__row--live .close__card.is-peek { flex-grow: 1.8; }
  .close__row--live .close__card.is-peek img { transform: scale(1.02); }
  .close__row--live .close__card.is-active { flex-grow: 6; }
  .close__row--live .close__card.is-active img { transform: scale(1.04); }

  /* The caption belongs to its own card, so it is only there when that card is
     the one being shown rather than cropped into a sliver. */
  /* The caption must not reflow while its card collapses. At width:100% the
     line re-wraps and re-centres on every frame of the squeeze, which is the
     cheap part — the words visibly shuffle on the way out. Given its own
     natural width it keeps one setting for the whole transition and the card's
     overflow simply crops it, evenly from both sides because it is centred on
     the card's middle. It also leaves faster than the card closes, so it is
     gone before there is any cropping to notice. */
  /* .close__row scopes it only to raise specificity: the base .close__caption
     rule is further down the file, and at equal weight the later one wins. */
  .close__row .close__caption {
    width: max-content;
    max-width: none;
    padding-inline: 0;
    transition: opacity 0.22s ease;
  }

  .close__row--live .close__card:not(.is-active) .close__scrim,
  .close__row--live .close__card:not(.is-active) .close__caption { opacity: 0; }

  .close__row--live .close__card:focus-visible { outline: 2px solid var(--navy); outline-offset: 3px; }
}
.close__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0) 30.439%, #000 100%);
}
.close__caption {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  padding-inline: 24px;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 40px;
  line-height: normal;
  letter-spacing: -1.2px;
  text-align: center;
  color: #fff;
}

/* ── footer ─────────────────────────────────────────────────────────────── */

/* ══════════════ FOOTER (component) ══════════════
   Persistent close of every page. The variables are its colour API and
   .footer--cream carries today's look — a new colourway (e.g. a dark page)
   is a variant class swapping these, the layout never changes. The mesh
   image and the wordmark gradient stay per-variant by design. */
.footer {
  --footer-link: var(--black);
  /* Both of these were too faint to read against the mesh: the headings sat
     at 0.5 alpha and the "coming soon" entries at 0.45, over a ground that
     is itself mid-toned where the gradient turns blue. 0.68 / 0.58 keep the
     hierarchy — heading above link above pending — while clearing the 4.5:1
     that body text needs. */
  --footer-heading: rgba(0, 0, 0, 0.68);
  --footer-muted: rgba(0, 0, 0, 0.58);
  --footer-brand-ink: #353535;
  --footer-rule: #71717a;

  position: relative;
  margin-top: 120px;
}
/* variants */
.footer--cream { /* the defaults above */ }
.footer > .footer__inner { position: relative; z-index: 2; }
.footer__rule { display: block; height: 1px; background: var(--footer-rule); opacity: 0.35; }
.footer__rule--cream { background: var(--cream); opacity: 0.5; }

.footer__top { display: flex; align-items: flex-start; justify-content: space-between; gap: 80px; padding-top: 60px; }
.footer__brand { display: flex; flex-direction: column; gap: 30px; }
.footer__brand svg { width: 74.257px; height: 58.904px; color: #1d1d1b; }
.footer__brand p { font-weight: 500; font-size: 20px; line-height: 25px; text-transform: capitalize; color: var(--footer-brand-ink); }
/* the tagline answers the preloader word: any letter under the cursor melts
   to its negative and back — same eased invert, same 0.35s clock */
.footer__ch { display: inline-block; transition: filter 0.35s ease; }
.footer__ch:hover { filter: invert(1); }
.footer__nav { display: flex; gap: 80px; }
.footer__col { font-weight: 600; font-size: 16px; line-height: 1; color: var(--footer-heading); margin-bottom: 19px; }
.footer__nav ul { display: flex; flex-direction: column; gap: 12px; }
/* Rest at 0.7, hover to full presence on a 600ms glide. The "weight" is a
   text-shadow, not font-weight, for two measured reasons: a static font
   cannot tween weight (it snapped to the other face mid-transition, which
   read as an instant cut on the way out), and real 500 is WIDER — the
   longest link fattening pushed its whole column and shoved the next one
   sideways. A hairline shadow thickens the glyphs without touching a
   single metric, and it interpolates smoothly in both directions. */
.footer__nav a {
  font-weight: 400; font-size: 14px; line-height: 20px; color: var(--footer-link);
  /* inline-block because transforms do nothing on a plain inline box */
  display: inline-block;
  /* 0.82, not 0.7: the rest state still reads as quieter than a hovered
     link, but stays comfortably legible where the mesh turns blue */
  opacity: 0.82;
  text-shadow: 0 0 0 rgba(0, 0, 0, 0);
  transform: translateX(0);
  /* the transform's bezier peaks past 1 — the link overshoots its stop and
     settles back, which is what reads as a spring. Fade and thickening ride
     the same 600ms on a plain ease; only the motion bounces. */
  transition: opacity 0.6s ease, text-shadow 0.6s ease,
              transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.footer__nav a:hover { opacity: 1; text-shadow: 0 0 0.5px currentColor; transform: translateX(6px); }
/* A footer entry that has no destination yet. It keeps the row's type exactly
   and only steps back in weight of colour, so the column still reads as a list
   rather than as a link that failed. */
.footer__soon { font-weight: 400; font-size: 14px; line-height: 20px; color: var(--footer-muted); }

.footer__base {
  position: relative;
  margin-top: 143px;
  padding-bottom: 40px;
}
/* The mesh drifts rather than sits. It is one image, moved very slowly on a
   long alternating loop — 38s out and 38s back — with a scale, a small
   rotation and a translate together, so no single one of them reads as "an
   image sliding" and it comes across as the light in the gradient shifting.

   The 1.28 base scale is what buys the room: the layer is full-bleed, so at
   1.0 any rotation or translate would swing an edge into view. Everything the
   keyframes do stays inside that overscan.

   translateX(-50%) has to be restated in every frame — it is what centres the
   full-bleed layer, and a transform in a keyframe replaces the base one rather
   than adding to it. */
/* The mesh is 581px tall and .footer__base is only ~342px, so 239px of the
   image sits ABOVE its own container — and that overhang is the whole soft fade
   from cream into blue. An overflow:hidden here clipped exactly that and left a
   hard horizontal seam across the page. There must be no clip on this element.

   The colour animates by panning the image horizontally, which moves where the
   light pools without moving the element at all — so there is no edge to expose
   and no seam to create. background-size stretches the WIDTH only: at 124% x
   100% the top row of the image stays the top row, which is the cream that
   makes the fade work. Scaling it proportionally instead would push that cream
   above the box and bring the seam straight back.

   A slow filter runs with it so the blue itself warms and cools rather than
   just sliding. An earlier pass laid a soft-light wash over the top instead,
   which turned the right-hand side grey. */
.footer__base::before {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  width: 100vw;
  height: 581px;
  background: url('img/mesh-dark.webp') no-repeat;
  background-size: 124% 100%;
  background-position: 50% 100%;
  z-index: 0;
  /* The top of the image has to become the page, and matching its cream to the
     page's cream by eye is a losing game — the pan moves which pixels sit at
     the top edge, and any filter shifts them again. Fading the layer out
     instead means the page shows through and there is nothing left to match. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 26%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 26%);
  animation: footer-mesh 30s ease-in-out infinite alternate;
}

/* no brightness here: it lifted the whole layer away from the page's own cream
   and put a visible band across the join */
@keyframes footer-mesh {
  0%   { background-position: 34% 100%; filter: saturate(1) hue-rotate(-5deg); }
  100% { background-position: 66% 100%; filter: saturate(1.14) hue-rotate(5deg); }
}

/* NO z-index and NO container-type here. This originally protected the
   wordmark letters' difference-blend hover, which has since been removed;
   the rule stays because adding either one now would force a stacking
   context and change paint order against the mesh ::before for no gain.
   The container-query sizing it used to provide is solved arithmetically
   on the wordmark. */
.footer__base .footer__inner { position: relative; }

/* sized against the column, not the viewport, so the wordmark always spans
   the full 1272 measure the way it does on the artboard (244.5px at that
   width). Was 19.222cqw against the inner as a container — but the
   container-type isolated the letters' blend (above), so the same number
   is computed from the column formula instead: 19.222% of
   min(1272px, 100vw − 2·gutter).
   "Master" is Medium and "Mind" is Light, same as the artboard. */
.footer__wordmark {
  font-family: var(--font-display);
  font-size: min(244.5px, calc(19.222vw - var(--gutter) * 0.38444));
  line-height: 0.97;
  letter-spacing: -0.03em;
  text-align: center;
  white-space: nowrap;
  color: var(--cream);
}
.footer__wordmark span:first-child { font-weight: 500; }
.footer__wordmark span:last-child { font-weight: 300; }
/* The preloader word's arrival, replayed down here: the script splits the
   wordmark into .wch letters and toggles .is-in each time it enters the
   viewport. Letters rest hidden (they only exist once the split ran, so
   no-JS still reads) and condense in on the preloader's own keyframes,
   staggered by index. `both` keeps them whole after landing — until the
   observer re-arms them by dropping the class. */
.footer__wordmark .wch {
  display: inline-block;
  opacity: 0;
  filter: blur(9px);
  transform: translateY(0.07em);
}
.footer__wordmark.is-in .wch {
  animation: pre-letter 0.55s cubic-bezier(0.22, 1, 0.36, 1)
    calc(var(--i, 0) * 0.05s) both;
}
@media (prefers-reduced-motion: reduce) {
  .footer__wordmark .wch { opacity: 1; filter: none; transform: none; animation: none; }
}
.footer__legal {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  margin-top: 20px;
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: var(--cream);
}
.footer__legal a { transition: opacity 0.6s ease; }
.footer__legal a:hover { opacity: 0.7; }
.footer__base .footer__rule--cream { margin-top: 20px; }

/* ══ responsive ══════════════════════════════════════════════════════════ */

@media (max-width: 1400px) {
  :root { --gutter: 64px; }
}

@media (max-width: 1180px) {
  :root { --gutter: 40px; }

  /* hero — 1.15 screens tall on phones: one screen of composition plus a
     breath before features (1.3 dwelt too long). svh, not vh: vh counts
     the browser chrome and overshoots while the URL bar is visible.
     min-height rather than height because the box clips (overflow hidden)
     and a short screen must be allowed to grow to fit the copy instead of
     cutting it. */
  .hero {
    height: auto; min-height: 115vh; min-height: 115svh; padding: 120px 0 48px;
    /* THE SEAM, solved — and proved rather than guessed: painting this
       background bright red drew a RED hairline across the page at the
       hero's foot, which is what identified it. 115svh is a FRACTIONAL
       height (1064.9px on a 926 viewport), so the box's last pixel row is
       composited at partial coverage and the element's own background is
       what shows through it — #46372e, dark brown, right across the page.
       That brown is a DESKTOP concern (there the sky photo stops short of
       the box and the colour is its bottom edge); here the sky covers the
       hero completely with object-fit: cover, so the background is never
       seen — except in that one blended row. Cream makes it invisible. */
    background: var(--cream);
  }
  /* the bottom fade commits to SOLID cream for its last stretch — on phones
     the globe's edge sat exactly on the box edge and read as a hard line;
     burying it well inside the fade hides any canvas/image seam for good */
  .hero::after {
    height: 220px;
    background: linear-gradient(to bottom,
      rgba(251, 244, 234, 0) 0%,
      rgba(251, 244, 234, 0.55) 45%,
      var(--cream) 82%,
      var(--cream) 100%);
  }

  /* Air, no rule. A hairline between hero and the proof band was tried and
     read as a seam rather than as punctuation — the sections separate
     perfectly well on space alone. */
  .credibility { margin-top: 96px; }
  .giants { margin-top: 72px; }
  /* stays positioned so it keeps painting above the absolutely placed sky and
     globe layers — going static drops it behind them */
  .hero__content {
    position: relative;
    left: auto;
    top: auto;
    transform: none;
    width: min(var(--content), 100% - var(--gutter) * 2);
    margin-inline: auto;
    z-index: 3;
  }
  /* 20px between the pill and the headline: at the desktop 40 the tag read
     as a separate object floating above the title rather than as its kicker */
  .hero__content { gap: 20px; }
  .hero__row { flex-direction: column; align-items: flex-start; gap: 40px; }
  .hero__title { width: 100%; font-size: clamp(48px, 8vw, 88px); letter-spacing: -2px; }
  .hero__aside { width: 100%; max-width: 480px; }
  /* 12px SHORT of the bottom, deliberately. A fractional box height means
     the hero's last pixel row is composited at partial coverage, and EVERY
     layer that reaches it bleeds through as a hairline — proved by probe:
     painting the hero's background red drew a red line, painting this veil
     blue drew a blue one. The fade's last ~40px are solid cream, so ending
     the photograph and its dark veil 12px early costs nothing visible and
     leaves only cream at the edge, where cream-on-cream cannot show. */
  .hero__sky { left: 0; top: 0; width: 100%; height: calc(100% - 12px); transform: none; }
  /* same reason for the particle canvas — clipped rather than resized,
     because its box dimensions are what globe.js draws against */
  .hero__globe { clip-path: inset(0 0 12px 0); }
  /* img only — the canvas must keep filling the hero box, and `height: auto`
     would collapse it to its intrinsic ratio */
  img.hero__globe {
    width: 150vw;
    height: auto;
    top: auto;
    bottom: -6vw;
  }
  .globe-fallback img.hero__globe--1 { opacity: 0.7; }
  .globe-fallback img.hero__globe--2 { opacity: 0.2; }
  .hero__sky-veil {
    background: linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.45) 55%, rgba(0, 0, 0, 0.3) 100%);
  }
  .nav { width: min(592px, 100% - var(--gutter) * 2); }

  /* THE BAR GETS OUT OF THE WAY. It is a floating pill 68px tall over a page
     whose headlines run to the top edge of a phone — every screenshot of this
     site taken mid-page has the bar sitting across a line of type. It tucks on
     the way down and returns on the first upward move, which is also exactly
     when it is wanted: reaching for the menu is an upward gesture. Never while
     the menu it opens is on screen, and never in the hero, where it is part of
     the composition (the script holds it until 260px down). */
  .nav {
    transition:
      background 0.35s ease,
      box-shadow 0.35s ease,
      opacity 0.7s ease 0.95s,
      transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  }
  .nav--tucked { transform: translate(-50%, calc(-100% - 30px)); }
  .menu-open .nav--tucked { transform: translateX(-50%); }

  /* credibility */
  .stats { flex-wrap: wrap; justify-content: center; gap: 40px 56px; }
  .stat__figure { font-size: 56px; }
  .giants { flex-wrap: wrap; justify-content: center; gap: 20px 32px; }
  .giants__label { width: auto; text-align: center; }

  /* ── THE ARGUMENT, IN ONE COLUMN ──────────────────────────────────────
     The desktop section is three photographs standing on a ruled grid with
     the copy travelling down the middle of it. Stacked for a phone, that
     drawing used to be thrown away wholesale: the rules were switched off,
     the copy went ranged left, and each photograph became a full-width
     landscape card with 20px corners — a different section wearing the same
     words, and the one part of the site a phone could not recognise.

     This is the same drawing in one column, and it is the arrangement
     godaylight.com uses at 460px, which is the reference for this section:
     a centred block of copy, then a band with a hairline at its top and its
     bottom holding a centred portrait cut of the photograph. Two elements
     swap jobs to build it — the FRAME becomes the band, full bleed so its
     rules run off both edges of the screen and have no ends to terminate,
     and the CELL becomes the picture's cut, standing on both rules in the
     artboard's own 437:530 proportion. A box cannot both bleed past the
     screen and clip its own contents, which is why there are two.

     The cell's own edges are the verticals: the horizontals run past them
     on both sides, so the crossing reads as a grid rather than as a border
     drawn around a photograph.

     Source order is frames first (the desktop grid wants that paint order),
     so `order` rebuilds the reading order into pairs: each block, then its
     own photograph, three times.

     KEYED ON data-i, NOT ON POSITION. This was written as nth-child against a
     count of "rules, 3 frames, veil, 3 blocks" — but there is no veil element
     in the section, so every index after the frames was one too high: block 1
     matched nothing and fell to order 0, block 3's rule matched nothing at
     all, and a phone read the argument as premise, failure, photo, answer,
     photo, photo. Two thoughts ran back to back with no picture between them
     and the section ended on two pictures with no words. An ordinal the
     element carries itself cannot come apart from it that way. */
  .argument { height: auto; margin-top: 96px; margin-bottom: 96px; display: flex; flex-direction: column; gap: 0; padding-inline: var(--gutter); }
  .argument__rules { display: none; }
  .argument__frame,
  .argument__block { position: static !important; transform: none !important; }
  /* only the copy keeps the old full width — the band sets its own below,
     wider than the section, which is how the rules reach the screen edges */
  .argument__block { width: 100% !important; }
  .argument__block[data-i="1"] { order: 1; }
  .argument__frame[data-i="1"] { order: 2; }
  .argument__block[data-i="2"] { order: 3; }
  .argument__frame[data-i="2"] { order: 4; }
  .argument__block[data-i="3"] { order: 5; }
  .argument__frame[data-i="3"] { order: 6; }

  /* THE BAND. The negative margin cancels the section's gutter, so the two
     hairlines run edge to edge; it must NOT clip, or the rules would be cut
     back to the cell. Same --rule the desktop grid is drawn in. */
  .argument__frame {
    height: auto;
    width: calc(100% + var(--gutter) * 2) !important;
    margin-inline: calc(var(--gutter) * -1);
    border-radius: 0;
    overflow: visible;
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
  }
  /* THE CELL: the artboard's cell proportion, centred, and the only thing
     here that clips. 60% of the screen is the reference's measure; the cap
     keeps it from swelling into a slab on a 640px phone.

     --rev is its arrival. The cut opens from the middle out the first time
     the band comes on screen — a photograph should not fade in on a cream
     page, that reads as a slow load rather than as an entrance. */
  .argument__cell {
    display: block;
    position: relative;
    width: 60%;
    max-width: 320px;
    margin-inline: auto;
    aspect-ratio: 437 / 530;
    overflow: hidden;
    clip-path: inset(var(--rev, 0%) 0%);
    transition: clip-path 1.1s cubic-bezier(0.16, 1, 0.3, 1);
  }
  /* THE CLOSED STATE IS ARMED FROM SCRIPT, exactly like .m-in, and for the
     same reason: it is the state nothing must be left in. The stylesheet's own
     default is open, so with the script gone — or under reduced motion, where
     it returns before it arms anything — every picture is simply there. Only
     .m-veil closes one, and only .m-live opens it again. */
  .argument__frame.m-veil .argument__cell { --rev: 16%; }
  .argument__frame.m-veil.m-live .argument__cell { --rev: 0%; }

  /* THE PICTURE INSIDE IT. 50px of overhang at each end, and --qc carries it
     across exactly that over the stretch where the cell's edges are on
     screen (index.html). It LAGS the cell, as the desktop picture lags its
     own — it descends while the cell carrying it climbs.

     --zin is the settle: the picture arrives 18% oversize and comes to rest,
     which is what makes the cut open ONTO something rather than reveal
     something already parked. It rides the `scale` property and not the
     transform, so the per-frame drift written above it is never interrupted
     by a transition — one of them is a scrubbed value and the other is a
     one-shot, and a single transform declaration cannot be both. */
  .argument__img {
    /* 50px each end, which is 100px of travel inside a cell 284px tall on a
       390 screen and 366px tall on a 500 — 35% and 27% of the cell's own
       height, bracketing the 34% measured off the reference. */
    --over: 50px;
    position: absolute;
    left: 0;
    top: calc(var(--over) * -1);
    width: 100%;
    height: calc(100% + var(--over) * 2);
    transform: translateY(calc(var(--qc, 0) * var(--over)));
    scale: var(--zin, 1);
    transition: scale 1.2s cubic-bezier(0.16, 1, 0.3, 1);
    will-change: transform, scale;
  }
  .argument__frame.m-veil .argument__img { --zin: 1.18; }
  .argument__frame.m-veil.m-live .argument__img { --zin: 1; }

  /* Centred, like the artboard and like the reference. Ranged left was the
     old stacking's own invention and it broke the section's symmetry: the
     rules are centred, the cell is centred, and the copy sat off to one
     side of both. The padding is the band's air — 64px above and below the
     copy, so every rule is the same distance from the words it separates. */
  .argument__block {
    align-items: center;
    text-align: center;
    padding-block: 64px;
    gap: 16px;
  }
  /* The headline is the section's voice and 28px flat was a caption of it.
     8.2vw lands 32px at 390 and 38 by 464, where it stops — past that the
     three-line headline starts taking four. */
  .argument__block h2 { font-size: clamp(28px, 8.2vw, 38px); letter-spacing: -0.8px; }

  /* ── A LINE AT A TIME ─────────────────────────────────────────────────
     Every principal headline on a phone, not just this section's. Each
     visual line is held in a box that clips and climbs out of it — the
     reference's signature move, and the reason its copy reads as spoken
     rather than switched on. It replaces a 26px fade for the whole heading,
     which is the one entrance that says nothing about what is arriving.

     The box is 0.2em deeper than the line so the descenders of the settled
     line are not cut, and the line starts 130% down, which is past that
     deeper box. Delays come from splitOne; the whole thing is armed by
     .m-live on the group, so with no script — or under reduced motion, where
     the script returns before it splits anything — the headline simply
     reads. */
  .argument__block h2 .line-mask,
  .step__name .line-mask,
  .who__title .line-mask,
  .close__title .line-mask {
    display: block;
    overflow: hidden;
    padding-bottom: 0.2em;
    margin-bottom: -0.2em;
  }
  .argument__block h2 .line,
  .step__name .line,
  .who__title .line,
  .close__title .line {
    transform: translateY(130%);
    transition: transform 1.05s cubic-bezier(0.16, 1, 0.3, 1) var(--md, 0s);
  }
  .argument__block.m-live h2 .line,
  .step__text.m-live .step__name .line,
  .who__head.m-live .who__title .line,
  .close__head.m-live .close__title .line { transform: none; }

  /* THE PAINT HAS TO TRAVEL WITH THE LINE. These headings are transparent
     glyphs over a gradient held one level up, so a line that moves without
     carrying its own copy of that gradient slides out from under its own
     ink. Same fix the argument headline already carries: the ramp moves onto
     each line, sized to the whole heading and offset by the line's own top,
     so it is reconstructed unbroken across them — navy on the first line,
     gold by the last. --h2h and --lt come from rebuild(). The descender
     padding comes too: it was on the heading, and the paint is not. */
  .step__name:has(.line),
  .who__title:has(.line),
  .close__title:has(.line) { background-image: none; }
  .step__name .line,
  .who__title .line,
  .close__title .line {
    display: block;
    padding-bottom: 0.18em;
    margin-bottom: -0.18em;
    background-image: linear-gradient(175deg,
      var(--hg1) 0%,
      var(--hg2) var(--hgp),
      var(--hg3) 100%);
    background-size: 100% var(--h2h, 100%);
    background-position: 0 calc(-1 * var(--lt, 0px));
    background-repeat: no-repeat;
    -webkit-background-clip: text;
    background-clip: text;
    animation: headline-ramp 8s ease-in-out infinite alternate;
  }

  /* A heading whose lines carry the arrival takes no second one of its own.
     The class is set by splitOne and only while the split is actually in
     place, so a heading that never got split keeps the group entrance. */
  .is-split.m-in { opacity: 1; transform: none; }

  /* no orphan words on small screens: headings balance their lines, body
     copy keeps its last line from being a single word. Eyebrows are
     deliberately NOT balanced — balancing split "WHERE INTENTION / GETS
     ITS SHAPE" into two even halves, when the line should simply run as
     far as it fits before turning. */
  h1, h2, h3 { text-wrap: balance; }
  p { text-wrap: pretty; }
  .eyebrow { text-wrap: normal; line-height: 1.5; }
  /* The display headings are set at line-height 1 for the artboard's big
     desktop sizes. At phone sizes the same ratio reads as two lines jammed
     together — 1.14 opens them without loosening the setting. */
  .hero__title,
  .system__title,
  .step__name,
  .who__title,
  .close__title,
  .collage__text h2,
  .argument__block h2 { line-height: 1.14; }

  /* ── THE INTRO PINS AND THE CARDS CLIMB OVER IT ───────────────────────
     The desktop arrangement, and the section's whole idea: the sky is the
     ground the programme happens against, not a slide that scrolls past and
     is gone. A phone used to get a dark panel followed by four steps on
     cream, which is the same words with the idea taken out.

     VERTICAL ONLY. Above 1180 every card after the first climbs to its pin
     offscreen to the right and then slides in horizontally, on a chain the
     script drives. None of that is here: a card simply rises up the screen
     and covers the sky, which is the one move a thumb is already making.
     Nothing about it is scripted — sticky is layout, so it holds with the
     script gone and under reduced motion, exactly as the desktop pin does.

     100vh, not svh: on a phone vh is the LARGE viewport, so the sky covers
     the screen whether the browser's chrome is showing or not. svh would
     leave a strip of page under it the moment the chrome retracted. */
  .system {
    position: sticky;
    top: 0;
    height: 100vh;
    min-height: 100vh;
    padding: 0;
  }
  .system__title { font-size: clamp(40px, 6.4vw, 64px); letter-spacing: -1.4px; margin-top: 0; }
  /* set like the hero: ranged left, not centred on the column */
  .system__content { gap: 40px; align-items: flex-start; text-align: left; }
  .system__lead { align-items: flex-start; }

  /* THE HERO'S ENTRANCE, borrowed — for every principal text block at this
     width, not just the system intro. The desktop entrances are scrubbed
     off pinned stages and scroll bands that do not exist down here, so JS
     marks each text element .m-in (hidden, 26px low) and its group .m-live
     when the group first comes on screen. Same rise, same curve as the
     hero; --md is the element's slot in its group's stagger, set inline by
     the script. Only JS ever adds .m-in, so with no script the copy simply
     shows. */
  .m-in {
    opacity: 0;
    transform: translateY(26px);
    transition:
      opacity 0.7s ease var(--md, 0s),
      transform 0.7s cubic-bezier(0.22, 1, 0.36, 1) var(--md, 0s);
  }
  .m-live .m-in { opacity: 1; transform: none; }

  /* THE RUNWAY, the same lesson as the desktop's 40vh: a stretch of scroll
     where the pinned sky simply HOLDS, finished words and no card yet.
     Without it the first card starts rising the instant the intro pins and
     the page's most cinematic moment gets no dwell. 30vh here rather than
     40: a phone screen is a third of the copy's width, so the same fraction
     of it is a much longer read.

     The tail is sky again — the cards are gone and the ground they rode on
     goes with them, which is what makes the release read as an ending. */
  .steps { padding-top: 30vh; padding-bottom: 100px; gap: 24px; }
  .steps__stack { gap: 24px; }

  /* EACH STEP IS A CARD NOW. Cream on a night sky needs no hairline to
     separate it — the shadow is only there to seat the card on the sky
     rather than let it float over it, and it is tinted --midnight-chaser's
     navy so it stays inside the palette, same as the desktop deck's.

     12px of sky either side, not the full gutter: the card's own padding
     already insets the words, and paying both would leave a phone reading a
     300px measure. Card edge 12 + padding 20 puts the copy 32px in, against
     the 20px it had with no card at all. */
  .step {
    width: calc(100% - 24px);
    margin-inline: auto;
    background: var(--cream);
    border-radius: 20px;
    overflow: hidden;
    box-shadow: 0 24px 60px -28px rgba(2, 37, 91, 0.45);
  }
  /* the card is the container now, so the inner column simply fills it and
     the 60px that used to be the section's air is the card's padding */
  .step__inner {
    flex-direction: column;
    align-items: flex-start;
    gap: 40px;
    height: auto;
    width: auto;
    margin-inline: 0;
    padding: 40px 20px 44px;
  }
  /* the cards overlap nothing below them, but the section after this one
     must be certain to paint over a sky that is still pinned when a long
     card runs past the stage's end */
  .who { position: relative; z-index: 1; }
  .step__text { width: 100%; gap: 28px; }
  .step__name { font-size: clamp(44px, 7vw, 64px); letter-spacing: -1.4px; }
  .step__body p:last-child { width: 100%; }
  /* The step photograph gets the argument cell's arrival and its drift, so a
     picture behaves the same way everywhere on the page: the frame opens from
     the middle out, the image settles out of a small oversize, and from then
     on it lags the frame carrying it. The overhang is smaller here (28px)
     because this frame is landscape and wide — the same travel that reads as
     depth in a portrait cut reads as sliding in a letterbox. */
  .step__media {
    width: 100%;
    height: min(560px, 70vw);
    clip-path: inset(var(--rev, 0%) 0%);
    transition: clip-path 1.1s cubic-bezier(0.16, 1, 0.3, 1);
  }
  .step__media.m-veil { --rev: 14%; }
  .step__media.m-veil.m-live { --rev: 0%; }
  .step__media img {
    --over: 28px;
    height: calc(100% + var(--over) * 2);
    margin-top: calc(var(--over) * -1);
    transform: translateY(calc(var(--qc, 0) * var(--over)));
    scale: var(--zin, 1);
    transition: scale 1.2s cubic-bezier(0.16, 1, 0.3, 1);
    will-change: transform, scale;
  }
  .step__media.m-veil img { --zin: 1.14; }
  .step__media.m-veil.m-live img { --zin: 1; }

  /* who */
  .who { padding-top: 72px; gap: 56px; }
  .who__head { flex-direction: column; align-items: flex-start; gap: 32px; }
  .who__head > div { width: 100%; }
  .who__title { font-size: clamp(40px, 6.4vw, 56px); letter-spacing: -1.4px; }
  .who__aside { width: 100%; max-width: 520px; }
  /* no rail, no travel, no emphasis — it is a swipeable row at this width */
  .who__rail { width: auto; margin-left: 0; overflow: visible; }
  .who__row {
    height: auto;
    width: auto;
    gap: 20px;
    align-items: flex-end;
    justify-content: flex-start;
    transform: none;
    overflow-x: auto;
    /* FULL BLEED. The scroller itself runs edge to edge — negative margins
       cancel the section's gutter — while its own padding keeps the first
       card on the page's margin. Without this the strip was clipped inside
       the measure and a card was cut off at both sides instead of running
       out of the screen. scroll-padding keeps snapping honest against that
       padding. */
    margin-inline: calc(var(--gutter) * -1);
    padding-inline: var(--gutter);
    scroll-padding-inline: var(--gutter);
    padding-bottom: 8px;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    /* no bar: the cards advance themselves and can still be swiped */
    scrollbar-width: none;
  }
  .who__row::-webkit-scrollbar { width: 0; height: 0; display: none; }
  /* the meta block was pinned at left:40 with a fixed 272 width — 312px of
     demand inside a card that is only as wide as the phone, so the deed ran
     off the right edge. Insetting from BOTH sides lets it wrap instead. */
  .member__meta { left: 24px; right: 24px; top: 28px; width: auto; }
  .member__name { font-size: 22px; }
  .member__deed { font-size: 19px; letter-spacing: -0.4px; }
  /* the card must always FIT the screen it is swiped on — 352px fixed ran
     off the right edge of anything narrower than ~430px and a card could
     never be seen whole. The photo inside is centred and narrower than any
     phone, so only the card's own box needs to give. */
  .member { scroll-snap-align: center; transform: none; opacity: 1; width: min(352px, calc(100vw - var(--gutter) * 2)); }

  /* collage → a SCATTER with the words in its middle, not a contact sheet.
     Two columns, but every tile carries its own height and its own vertical
     offset, so the two sides never line up and the wall reads as pinned
     pictures. Four above the text, four below — the copy sits in the
     centre of the arrangement the way it sits inside the desktop ring. */
  .collage {
    position: relative;
    height: auto;
    margin-top: 96px;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 14px 12px;
    align-items: start;
  }
  /* display:contents so the tiles are the grid's own children again */
  .collage__stage { display: contents; }
  .collage__ring { display: none; }
  /* the ring's long-exposure clones are desktop-only paint: here they were
     16 INVISIBLE grid rows (opacity 0, 190px each) — two thirds of a
     five-thousand-pixel section nobody could see */
  .collage__echo { display: none; }
  /* THE WORDS SIT IN THE FIELD, not between two banks of it. Splitting the
     tiles into "some, then the copy, then some more" read as three separate
     blocks; here the pictures run as ONE uninterrupted wall and the copy
     floats over its middle, so images pass behind the line instead of
     stopping for it. A cream haze — the page's own colour, radial so it has
     no edge — is what keeps the words legible over whatever is behind. */
  .collage__text {
    position: absolute;
    /* PAST the gutters on both sides, deliberately: the haze must not end
       anywhere a picture is, or its edge draws a line across them. Out here
       it ends over the page's own margin, where cream meets cream. */
    left: calc(var(--gutter) * -1);
    right: calc(var(--gutter) * -1);
    top: 50%;
    z-index: 2;
    width: auto;
    height: auto;
    /* --haze is the fade, the padding and the gradient's stops all at once,
       and that is the point: they cannot drift apart. */
    --haze: 88px;
    padding: var(--haze) var(--gutter);
    text-align: center;
    pointer-events: none;
    /* A BAND, not an ellipse. The radial version reached the box edge while
       still partly opaque, and that hard stop was the "mask line" — this one
       is transparent at both its own edges, so it has nothing to cut against:
       it thickens into solid cream behind the words and thins away again.

       STOPS IN PIXELS, NOT PERCENTAGES, and that is the fix. On percentages
       the solid core was 38%-62% of a box whose copy filled 22%-78%, so the
       headline's first line and the paragraph's last sat on 0.86 cream at
       best and on a photograph at worst — navy type over a lit picture,
       which is where this section looked unfinished. Worse, the numbers only
       held at one copy length: a paragraph that took one more line pushed
       itself further out of its own ground.

       Tied to --haze instead, full cream covers exactly the content box at
       any length, and both fades live entirely inside the padding, where
       there is nothing to be legible over. */
    background: linear-gradient(to bottom,
      rgba(251, 244, 234, 0) 0,
      rgba(251, 244, 234, 0.9) calc(var(--haze) * 0.5),
      var(--cream) var(--haze),
      var(--cream) calc(100% - var(--haze)),
      rgba(251, 244, 234, 0.9) calc(100% - var(--haze) * 0.5),
      rgba(251, 244, 234, 0) 100%);
    /* -50% centres it on the field; --mdr is the script's counter-drift */
    transform: translateY(calc(-50% + var(--mdr, 0px)));
  }
  /* THE FULL MEASURE, and it was checked rather than assumed: at 20px the
     screen's own width sets 49 characters a line here, inside the 45-75 the
     canon asks for. Capping it at 400 to "tighten" the block was measured
     too — 41 characters, below the range, and the paragraph took a sixth
     line for the trouble. A centred block gets shorter by having fewer
     lines, not narrower ones. */
  .collage__text p { width: 100%; margin-inline: auto; }
  .collage__text h2 { font-size: clamp(40px, 6.4vw, 56px); }
  /* the headline and its paragraph are one statement — 40px of air read as
     two, with the copy floating clear of the words it belongs to */
  .collage__text { gap: 28px; }
  /* the ring is a desktop idea; here the tiles are pinned pictures that
     DRIFT UPWARD against the page while the text sinks, so the wall opens
     as you descend into it. --mri is written by the script, capped at
     ±20px, which is why the copy reserves 64px either side. */
  .collage__img {
    position: static;
    transform: translateY(var(--mri, 0px));
    width: 100% !important;
    height: 200px !important;
    border-radius: 16px;
    will-change: transform;
  }
  /* THE SCATTER. Sizes and offsets are per tile — addressed by their own
     classes rather than nth-of-type, because the ring's echo clones are
     img siblings too and would throw the counting off. No ordering: the
     eight run in one continuous field that the copy floats over. No two
     neighbours share a height, and the right column rides lower than the
     left, so the rows never read as rows. */
  .c-1 { height: 208px !important; }
  .c-2 { height: 262px !important; margin-top: 34px; }
  .c-3 { height: 244px !important; margin-top: -14px; }
  .c-4 { height: 186px !important; margin-top: 22px; }
  .c-5 { height: 236px !important; }
  .c-6 { height: 190px !important; margin-top: 40px; }
  .c-7 { height: 196px !important; margin-top: -10px; }
  .c-8 { height: 252px !important; margin-top: 18px; }

  /* close */
  .close { margin-top: 96px; gap: 56px; }
  .close__head { flex-direction: column; align-items: flex-start; gap: 32px; }
  .close__lead { width: 100%; }
  .close__title { font-size: clamp(40px, 6.4vw, 56px); letter-spacing: -1.4px; }
  .close__aside p { width: 100%; max-width: 520px; }
  /* clear air between the line and the button it is asking you to press */
  .close__aside { gap: 40px; }
  /* a SLIDESHOW, not a stack: four full-height cards in a row you swipe,
     one snapped at a time. Stacked vertically they were four screens of
     scrolling for what is one choice. */
  .close__row {
    flex-wrap: nowrap;
    height: auto;
    gap: 16px;
    overflow-x: auto;
    /* full bleed, same as the members strip: the scroller reaches both
       screen edges so cards run off them instead of being clipped inside
       the measure */
    margin-inline: calc(var(--gutter) * -1);
    padding-inline: var(--gutter);
    scroll-padding-inline: var(--gutter);
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .close__row::-webkit-scrollbar { width: 0; height: 0; display: none; }
  .close__card,
  .close__card--wide {
    flex: 0 0 auto;
    width: min(340px, calc(100vw - var(--gutter) * 2));
    height: 420px;
    scroll-snap-align: center;
  }
  /* every card carries its caption here — there is no "open" one to single
     out, because the snapped card IS the one being looked at */
  .close__row--live .close__card .close__scrim,
  .close__row--live .close__card .close__caption { opacity: 1; }
  .close__caption { font-size: 28px; }

  /* footer */
  .footer { margin-top: 96px; }
  .footer__top { flex-direction: column; gap: 48px; }
  .footer__nav { flex-wrap: wrap; gap: 40px 56px; }
  .footer__base { margin-top: 96px; }
  .footer__legal { flex-direction: column; align-items: flex-start; gap: 8px; font-size: 14px; }
}

@media (max-width: 640px) {
  :root { --gutter: 20px; }
  .nav { height: 60px; padding: 12px; }
  .nav__logo { width: auto; }
  .nav__links { display: none; }
  /* the bar reduces to logo + 9-dot menu; the actions live inside the menu */
  .nav__actions { display: none; }
  .nav__menu { display: flex; }

  /* footer nav stays two columns side by side — only the type grows. The
     desktop 80px gutter is what made them read as separated islands on a
     phone; 40px keeps both groups on one comfortable line at 320. */
  .footer__nav { gap: 40px; }
  .footer__col { font-size: 18px; margin-bottom: 16px; }
  .footer__nav a { font-size: 17px; line-height: 24px; }
  .footer__soon { font-size: 17px; line-height: 24px; }
  .footer__nav ul { gap: 14px; }
  .hero__title { letter-spacing: -1.2px; }
  /* the hero's pill is the longest line on the page and it must stay ONE
     line — at 16px it ran off a 320px screen. 4.1vw lands ~13px at 320 and
     is back to 16 by 390. */
  .pill { font-size: clamp(12px, 4.1vw, 16px); }
  /* the band's height is its cell's — it was a fixed 240px landscape card
     before the section was rebuilt on the grid, and that number left the band
     shorter than the picture standing in it */
  /* keep the slideshow's fixed-width cards — a flex-basis of 100% under
     nowrap made all four share one screen at 83px each */
  .close__card,
  .close__card--wide { flex: 0 0 auto; height: 380px; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .btn:hover { transform: none; }
  .pill__dot::after { animation: none; opacity: 0; }
  .footer__base::before { animation: none; }
  .close__card,
  .close__card img,
  .close__scrim,
  .close__caption { transition: none; }
  .close__row .close__card:hover img { transform: none; }
}

/* ── preloader ───────────────────────────────────────────────────────────────
   The page opens on the same sky it uses later, held while the mark draws
   itself and then becomes the full lockup.

   The mark is one filled outline path, so it cannot be "drawn" as a fill. It is
   stroked instead — dasharray set to its own measured length by the script, and
   the offset run to zero — which traces the contour of the ribbon. The fill
   then arrives underneath and the stroke retires, so the shape is written and
   then becomes solid.

   The word does not fade in beside the mark; it OPENS. Its max-width runs from
   0, and because the row is centred that pushes the mark leftward into its
   lockup position on its own. One property produces both halves of the move.

   Everything is off under prefers-reduced-motion: the lockup is simply there,
   and the overlay still leaves on load. */
.preloader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  /* the hero's own ground colour, so anything the sky box does not cover
     matches what sits beneath and the handoff has no seam */
  background: #46372e;
  opacity: 1;
  overflow: hidden;
  /* the exit is staged: veil darkens (0.55s), THEN the overlay fades over the
     now-identical hero — so the fade itself has nothing visible to show */
  transition: opacity 0.75s ease 0.45s, visibility 0s linear 1.2s;
}
/* A pixel-copy of .hero__sky at scroll 0 — same box, same flip, same blur,
   same veil. The preloader is not a screen before the page: it IS the hero's
   sky with the lockup on it, so when it fades there is nothing to see change
   except the lockup leaving and the hero copy arriving. */
.preloader__sky {
  position: absolute;
  left: 50%;
  top: 413px;
  width: 1758px;
  height: 1162px;
  transform: translate(-50%, -50%);
  filter: blur(5px);
}
.preloader__sky img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scaleX(-1);
}
/* The veil is the hero's, but it arrives LAST: the logo draws over the clean
   bright sky, and only as the preloader hands over does the sky darken to the
   hero's level — the darkening IS the transition the eye reads. */
.preloader__sky::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.5) 14.587%, rgba(0, 0, 0, 0.25) 100%);
  opacity: 0;
  transition: opacity 0.55s ease;
}
.preloader.is-done .preloader__sky::after { opacity: 1; }

.preloader__lockup {
  position: relative;
  /* NO z-index, deliberately: DOM order already paints this above the sky
     (both positioned, sky first), and a z-index would make it a stacking
     context — which compositing treats as an ISOLATED blending group,
     cutting the letters' difference-blend off from the sky it inverts. */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.preloader__row { display: flex; align-items: center; }

.preloader__mark {
  width: 88px;
  height: 71px;
  flex: none;
  overflow: visible;
  /* the mark draws LARGE and centre-stage (the word is still closed, so the
     row centres on it alone), then settles down to its place in the lockup
     just before the word opens — see pre-settle below */
  transform: scale(1.8);
}
/* The line IS the logo: the design file's own single-stroke path, drawn by
   dashoffset — one line completing, like a loader. No mask, no patch. */
.preloader__line {
  /* born transparent: the fade below runs while only the line's first pixels
     exist, so the stroke materialises from nothing instead of popping in */
  opacity: 0;
  fill: none;
  stroke: #fff;
  /* the artwork's own ribbon width, from the design file (47.15 over a
     1080-unit viewBox) */
  stroke-width: 47.15;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* the script measures the path and sets --len; 1 is a harmless placeholder */
  stroke-dasharray: var(--len, 1);
  stroke-dashoffset: var(--len, 1);
}

/* overflow hidden is what lets max-width crop the word while it opens */
.preloader__word {
  overflow: hidden;
  max-width: 0;
  white-space: nowrap;
  font-family: var(--font-display);
  font-size: 82px;
  line-height: 1;
  letter-spacing: -0.03em;
  color: #fff;
}
.preloader__word span:first-child { font-weight: 500; }
.preloader__word span:last-child { font-weight: 300; }

/* Letter-by-letter blur-in, riding the crop: each .preloader__ch (split by
   the script, --i is its index) condenses on its own clock, the stagger
   pacing the letters to land roughly where the opening crop reveals them.
   The 2.15s base is the word's own pre-open delay — keep them in step.
   `backwards` holds a letter transparent until its turn; the keyframes'
   end state equals the natural state, so nothing snaps at handover and
   no property stays pinned by a forwards fill. */
.preloader__ch { display: inline-block; }
.preloader.is-running .preloader__ch {
  animation: pre-letter 0.55s cubic-bezier(0.22, 1, 0.36, 1)
    calc(2.15s + var(--i, 0) * 0.05s) backwards;
}
@keyframes pre-letter {
  from { opacity: 0; filter: blur(9px); transform: translateY(0.07em); }
  to { opacity: 1; filter: none; transform: none; }
}

/* NEGATIVE on touch: white type + difference blending IS the photographic
   negative — a hovered letter becomes the exact inverse of the sky pixels
   behind it. The flip is instant BY CHOICE: blend modes cannot interpolate
   (an eased invert() was tried and kept for the footer tagline instead),
   and here the hard flip is the flashbulb the effect wants. */
.preloader.is-running .preloader__ch:hover { mix-blend-mode: difference; }

.preloader__tag {
  font-size: 12px;
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: capitalize;
  color: rgba(255, 255, 255, 0.72);
  opacity: 0;
}

/* the sequence: draw, open, then the tagline. The draw takes 1.6s — 1.2s
   outran the eye on the long single-stroke path, 2s dwelt. Everything after
   is chained off the draw's end: settle at 1.6, word at 2.15 (as the mark
   lands), tagline at 2.9. */
.preloader.is-running .preloader__line {
  animation:
    pre-birth 0.28s ease-out forwards,
    pre-draw 1.6s cubic-bezier(0.65, 0, 0.35, 1) forwards;
}
/* once drawn, the big mark glides down to its real size; the word opens the
   moment it lands (1.6 + 0.55 ≈ the word's 2.15s delay) */
.preloader.is-running .preloader__mark {
  animation: pre-settle 0.55s cubic-bezier(0.22, 1, 0.36, 1) 1.6s forwards;
}
@keyframes pre-settle { to { transform: scale(1); } }
.preloader.is-running .preloader__word {
  animation: pre-open 1.28s cubic-bezier(0.22, 1, 0.36, 1) 2.15s forwards;
}
.preloader.is-running .preloader__tag {
  animation: pre-tag 0.9s ease 2.9s forwards;
}

@keyframes pre-birth { to { opacity: 1; } }
@keyframes pre-draw { to { stroke-dashoffset: 0; } }
@keyframes pre-open { to { max-width: 420px; } }
@keyframes pre-tag  { to { opacity: 1; } }

/* leaving: the overlay lifts a little as it goes, so the page arrives rather
   than being uncovered */
.preloader.is-done {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.preloader.is-done .preloader__lockup {
  transform: translateY(-14px);
  opacity: 0;
  transition: transform 0.75s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.5s ease;
}

/* nothing scrolls behind it — and the lock outlives the preloader itself:
   .is-intro holds the page still through the handoff and the hero copy's
   entrance, so the animation always plays over the top of the page */
html.is-preloading, html.is-preloading body,
html.is-intro, html.is-intro body { overflow: hidden; }

/* The handoff: while the preloader holds the page the hero copy and the nav
   wait below their resting places, and the moment it lifts (the script removes
   html.is-preloading) each piece rides up in turn over the same sky. With no
   JS the class is never set and everything simply shows. */
html.is-preloading .nav,
html.is-preloading .hero__content .pill,
html.is-preloading .hero__title,
html.is-preloading .hero__aside {
  opacity: 0;
}
html.is-preloading .hero__content .pill,
html.is-preloading .hero__title,
html.is-preloading .hero__aside {
  transform: translateY(26px);
}
/* delays sit past the veil's 0.55s darkening, so the copy rises into the
   already-darkened sky rather than fighting it */
.hero__content .pill {
  transition: opacity 0.7s ease 0.5s, transform 0.7s cubic-bezier(0.22, 1, 0.36, 1) 0.5s;
}
.hero__title {
  transition: opacity 0.7s ease 0.63s, transform 0.7s cubic-bezier(0.22, 1, 0.36, 1) 0.63s;
}
.hero__aside {
  transition: opacity 0.7s ease 0.79s, transform 0.7s cubic-bezier(0.22, 1, 0.36, 1) 0.79s;
}
@media (prefers-reduced-motion: reduce) {
  .hero__content .pill, .hero__title, .hero__aside { transition: none; }
}

@media (max-width: 640px) {
  .preloader__word { font-size: 48px; }
  .preloader__mark { width: 54px; height: 44px; }
  @keyframes pre-open { to { max-width: 250px; } }
}

@media (prefers-reduced-motion: reduce) {
  .preloader.is-running .preloader__line,
  .preloader.is-running .preloader__mark,
  .preloader.is-running .preloader__word,
  .preloader.is-running .preloader__tag { animation: none; }
  .preloader__mark { transform: none; }
  .preloader__line { stroke-dasharray: none; stroke-dashoffset: 0; opacity: 1; }
  .preloader__word { max-width: 420px; }
  .preloader__tag { opacity: 1; }
  .preloader.is-done .preloader__lockup { transition: none; }
}


/* ── headline colour ─────────────────────────────────────────────────────────
   The display headlines carry a ramp across the block — deep navy into charcoal
   into a warm gold — sampled off the Figma frame rather than picked: #033867 at
   the start of a headline, #2c2d2a through its middle, #a98655 by the end.

   background-size is ~100%, so the WHOLE ramp always spans the headline. An
   earlier pass set it to 220% and slid it, which meant the text only ever
   showed a slice of the gradient — at one end of the loop a headline sat
   entirely inside the navy and read as flat, which is not what the frame shows.

   The animation is therefore in the COLOURS, not the position: the three stops
   are registered custom properties, so they interpolate. Without @property a
   colour in a custom property is an unanimatable token and the keyframes are
   simply ignored — the static ramp still renders, which is the right fallback.
   A little position drift rides along on the 8% of overscan.

   The gradient sits on the h2 and not on the lines inside it, so a two-line
   headline is one continuous ramp rather than the same ramp twice. Nothing here
   touches a background: only the fill of the type. The white headlines on the
   dark sections are left out — this is for type on cream. */
@property --hg1 { syntax: '<color>'; initial-value: #033867; inherits: false; }
@property --hg2 { syntax: '<color>'; initial-value: #2c2d2a; inherits: false; }
@property --hg3 { syntax: '<color>'; initial-value: #a98655; inherits: false; }
/* the position of the middle stop — moving THIS is what you actually see */
@property --hgp { syntax: '<percentage>'; initial-value: 40%; inherits: false; }

.argument__block h2,
.who__title,
.collage__text h2,
.close__title,
.step__name {
  --hg1: #033867;
  --hg2: #2c2d2a;
  --hg3: #a98655;
  /* 175deg, and the angle is the whole thing. At the near-horizontal 100deg
     this started at, BOTH lines of a headline span the same 0.01-0.98 of the
     ramp, so line one runs to gold as well and the reference's blue first line
     is lost. At 175deg the vertical component dominates by about eleven to one:
     line one takes 0.16-0.50 of the ramp and line two takes 0.50-0.83, so each
     line gets half of it and the block reads blue, then dark, then gold —
     which is what the frame shows. Solved against the built block, 479 x 83.

     background-size stays 100%: the whole ramp must span the headline, and any
     oversize slides part of it out of view. */
  --hgp: 40%;
  background-image: linear-gradient(175deg,
    var(--hg1) 0%,
    var(--hg2) var(--hgp),
    var(--hg3) 100%);
  background-size: 100% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: headline-ramp 8s ease-in-out infinite alternate;
}

/* What moves is the MIDDLE STOP, not the endpoints. Sliding it from 34% to 74%
   drags the whole dark-to-gold transition up and back through the words, so the
   gold visibly climbs into the first line and retreats — the endpoints stay
   anchored, so the headline still starts navy and ends gold at every moment.

   40% to 80% is as far as it can usefully go. Line one occupies 0.16-0.50 of
   the ramp: at 80% that whole line sits in the navy-to-charcoal half and reads
   blue, and at 40% only its last quarter has begun to warm. Past 40% the gold
   reaches line one properly and the frame's blue first line is lost.

   The first version only nudged the three colours a few values each over 13s,
   which is animation you can prove and cannot see. This is the difference
   between the property changing and something happening. */
@keyframes headline-ramp {
  to {
    --hgp: 80%;
    --hg1: #0d5590;
    --hg2: #3f3a28;
    --hg3: #d7a862;
  }
}

@media (prefers-reduced-motion: reduce) {
  .argument__block h2,
  .who__title,
  .collage__text h2,
  .close__title,
  .step__name { animation: none; }
}

/* the collage copy — and the system intro's one-liner — are split into lines
   and words by the script so each can arrive on its own; inline-block is what
   lets a word be moved at all */
.collage__text h2 .line { display: block; }
.collage__text .cw,
.system__body .cw,
.who__aside .cw,
.close__aside .cw { display: inline-block; will-change: opacity, transform; }

/* the section headlines share the system title's entrance — condensing out
   of blur as they climb into view, --tIn written from each one's own screen
   position. Defaults keep them whole without JS; the gradient ramp composes
   fine with a filter, which blurs the rendered text, spans untouched. */
.who__title,
.close__title,
.collage__text h2 {
  opacity: var(--tIn, 1);
  transform: translateY(calc((1 - var(--tIn, 1)) * 26px));
  filter: blur(calc((1 - var(--tIn, 1)) * 14px));
}
