/* ============================================================
   Interact — V2 (dark)
   Mirrors Figma 574:2 (1440 x 8096). V1's styles.css is untouched;
   nothing here is shared with it except css/fonts.css.

   TOKENS ARE AN INVERSION, NOT A RECOLOUR. V1's page cream becomes
   this page's ink, and V1's ink pills become cream pills. Deep purple
   #42256B is retired outright: it vanishes on near-black, so lilac is
   the only accent on this ground.
   ============================================================ */
:root{
  --page:      #0A0A0A;   /* the ground. every image is faded to exactly this */
  --ink:       #F4F3EF;   /* primary type — V1's page colour, inverted        */
  --body:      #A8A79E;
  --muted:     #9A9A92;   /* unchanged: reads on cream AND on black          */
  --hairline:  #2A2A26;
  --numeral:   #C4B9D0;
  --lilac:     #D6C2F0;   /* the only accent                                 */
  --wordmark:  #171715;   /* dark-on-dark: the relationship inverts too      */

  --glass-ink:  30,29,26;
  --glass-link: #BDBCB3;

  --frnd:      #FF3E84;
  --playchat:  #784BD0;

  --serif: "Instrument Serif", Georgia, "Times New Roman", serif;
  --sans:  "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --mono:  "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* One design pixel as a length — same contract as V1. Unitless numbers:
     calc(90 * var(--s)). calc(90px * var(--s)) is px x px and silently
     kills the declaration. Written as 100vw/1440 so the division is exact. */
  --s: clamp(0.7222222px, calc(100vw / 1440), 1.1666667px);

  --ease: cubic-bezier(.16,1,.3,1);        /* long tail, no overshoot */
  --ease-io: cubic-bezier(.4,0,.2,1);

  /* INTRO TIMING — one place, because feel is iterative.
     ORDER (brief, 2026-09-04): bloom bed -> header text -> body text ->
     CTA -> headphone -> nav. The bed first is the point now: the ground
     exists before anything is placed on it, and the object the whole page
     is about arrives LAST, settling down into flowers that are already
     alive. The nav is furniture and still comes after everything.
     Each element also RESOLVES OUT OF BLUR rather than just fading — that
     soft-to-sharp arrival is what the Pyko reference actually does, and its
     absence is why the first pass read as a plain fade. */
  --in-bloom-d: .10s;  --in-bloom-t: 1.05s;   /* 1 bloom bed */
  --in-type-d:  .50s;  --in-type-t:  .80s;    /* 2-4 type, staggered by --d */
  --in-hp-d:    1.45s; --in-hp-t:    1.20s;   /* 5 headphone, the object last */
  --in-nav-d:   2.45s; --in-nav-t:   .70s;    /* 6 nav */
}

*,*::before,*::after{ box-sizing:border-box; }
html{ scroll-behavior:smooth; }
body{
  margin:0; background:var(--page); color:var(--ink);
  font-family:var(--sans);
  -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
  /* `clip`, NOT `hidden`: overflow-x:hidden makes body a scroll container,
     which silently kills position:sticky on the Vision pin. */
  overflow-x:clip;
}
a{ color:inherit; text-decoration:none; }
img{ max-width:100%; display:block; }
:focus-visible{ outline:2px solid var(--lilac); outline-offset:3px; border-radius:2px; }

.skip{
  position:absolute; left:50%; top:0; transform:translate(-50%,-120%); z-index:200;
  padding:10px 18px; border-radius:0 0 8px 8px;
  background:var(--ink); color:#0A0A0A; font-size:13px;
}
.skip:focus{ transform:translate(-50%,0); }

/* ── shared type ─────────────────────────────────────────── */
.eyebrow{
  margin:0; font-family:var(--mono); font-weight:500;
  font-size:calc(11 * var(--s)); letter-spacing:.14em; text-transform:uppercase;
  color:var(--muted); display:flex; align-items:center; justify-content:center;
  gap:calc(10 * var(--s));
}
.eyebrow--bright{ color:#CFCEC6; }
.eyebrow--lilac{ color:var(--lilac); }
.eyebrow .dot{ width:calc(5 * var(--s)); height:calc(5 * var(--s)); border-radius:50%; background:var(--lilac); flex:none; }

.h1{
  margin:0; font-family:var(--serif); font-weight:400;
  font-size:calc(80 * var(--s)); line-height:.98; letter-spacing:-.03em;
  text-align:center;
}
.h1__line{ display:block; }
.h1__line--it{ font-style:italic; color:var(--lilac); }

.h2{
  margin:0; font-family:var(--serif); font-weight:400;
  font-size:calc(54 * var(--s)); line-height:1.06; letter-spacing:-.025em;
  text-align:center; text-wrap:balance;
}
.h2--wide{ font-size:calc(68 * var(--s)); }
.h2 em{ font-style:italic; color:var(--lilac); }

.sub,.lede{
  margin:0; color:var(--body); text-align:center;
  font-size:calc(17 * var(--s)); line-height:1.5; letter-spacing:-.01em;
}
.lede{ font-size:calc(16 * var(--s)); }
.rule{ border:0; height:1px; background:var(--hairline); margin:0; }

/* ── buttons ─────────────────────────────────────────────── */
.btn{
  display:inline-flex; align-items:center; gap:calc(9 * var(--s));
  height:calc(49 * var(--s)); padding:0 calc(22 * var(--s));
  border-radius:calc(26 * var(--s));
  font-size:calc(14 * var(--s)); letter-spacing:-.01em; font-weight:500;
  transition:transform .5s var(--ease), box-shadow .5s var(--ease), background-color .3s var(--ease-io);
  will-change:transform;
}
.btn--cream{ background:var(--ink); color:#0A0A0A; }
.btn--cream:hover{ transform:translateY(-2px); box-shadow:0 calc(14 * var(--s)) calc(30 * var(--s)) rgba(0,0,0,.5); }
.btn--ghost{ background:#121210; color:var(--ink); border:1px solid var(--hairline); }
.btn--ghost:hover{ transform:translateY(-2px); border-color:#3A3A34; }
.btn--lilac{ background:var(--lilac); color:#121210; }
.btn--lilac:hover{ transform:translateY(-2px); box-shadow:0 calc(14 * var(--s)) calc(34 * var(--s)) rgba(214,194,240,.24); }
.btn--static{ cursor:default; color:var(--body); }
.btn--static:hover{ transform:none; border-color:var(--hairline); }
.btn__knob{ width:calc(7 * var(--s)); height:calc(7 * var(--s)); border-radius:50%; background:var(--lilac); }
.btn__arrow{ font-family:var(--sans); }

/* ============================================================
   NAV — the 70:2 glass pill
   Opacity only on .nav itself: a transform or will-change would give it
   a stacking context and kill the pill's backdrop-filter.
   ============================================================ */
.nav{
  position:fixed; inset:0 0 auto 0; z-index:100; padding:calc(16 * var(--s)) calc(24 * var(--s));
  transition:transform .38s var(--ease);
}
/* AUTO-HIDE, ported from the FRND page. Reading runs downward, so down is
   the only direction in which a floating bar is ever in the way; coming
   back on the first upward intent means it is never more than a flick from
   reach. -200% of its own height clears the pill and its shadow at every
   viewport without hard-coding a number that drifts when the pill resizes.
   🔑 The transform goes HERE and the load-in is an OPACITY (see .is-ready
   below), so the entry animation and the hide never share a property. */
.nav.is-away{ transform:translateY(-200%); }
.nav__pill{
  display:flex; align-items:center; gap:calc(24 * var(--s));
  height:calc(60 * var(--s)); padding:0 calc(10 * var(--s)) 0 calc(22 * var(--s));
  border-radius:calc(30 * var(--s));
  background:linear-gradient(180deg, rgba(var(--glass-ink),.88), rgba(var(--glass-ink),.80));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.10), inset 0 0 0 1px rgba(255,255,255,.16),
             0 calc(8 * var(--s)) calc(24 * var(--s)) rgba(18,18,16,.16);
  -webkit-backdrop-filter:blur(18px); backdrop-filter:blur(18px);
}
.brand{ display:inline-flex; align-items:center; gap:calc(11 * var(--s)); flex:none; }
/* the supplied Interact wordmark (Figma MVP 1004:7, white). Height-led so it
   scales with --s; 600×112 source = 5.35:1, ~4× the rendered size. */
.brand__logo{ display:block; height:calc(22 * var(--s)); width:auto; }
.nav .brand__logo{ height:calc(19 * var(--s)); }   /* nav sits 3px shorter than the footer brand (2026-09-30) */
.nav__links{ display:flex; gap:calc(34 * var(--s)); margin:0 auto; }
.nav__links a{
  font-size:calc(14 * var(--s)); color:var(--glass-link); letter-spacing:-.01em;
  transition:color .3s var(--ease-io);
}
.nav__links a:hover{ color:var(--ink); }
.nav__cta{ flex:none; height:calc(41 * var(--s)); }
.nav__toggle{ display:none; }

/* ── AMBIENT SOUND TOGGLE ────────────────────────────────────
   Bottom right, floating, same glass as the nav pill so it reads as part
   of the same chrome rather than a widget dropped on the page. State is
   carried by the BARS, not by swapping an icon: they stand and move when
   sound is on, and lie down flat and dim when it is off — so there is
   never a moment where the control says one thing and the page does
   another. z-index is under the nav (100) and over everything else. */
.sound{
  position:fixed; right:calc(24 * var(--s)); bottom:calc(24 * var(--s)); z-index:90;
  width:calc(52 * var(--s)); height:calc(52 * var(--s)); border-radius:50%;
  display:grid; place-items:center; cursor:pointer; border:0; padding:0;
  background:linear-gradient(180deg, rgba(var(--glass-ink),.88), rgba(var(--glass-ink),.80));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.10), inset 0 0 0 1px rgba(255,255,255,.16),
             0 calc(8 * var(--s)) calc(24 * var(--s)) rgba(18,18,16,.22);
  -webkit-backdrop-filter:blur(18px); backdrop-filter:blur(18px);
  transition:transform .3s var(--ease), opacity .4s var(--ease-io);
}
.sound:hover{ transform:scale(1.06); }
.sound:focus-visible{ outline:2px solid var(--lilac); outline-offset:3px; }
.js .sound{ opacity:0; pointer-events:none; }
html.is-ready .sound{ opacity:1; pointer-events:auto; transition-delay:0s, var(--in-nav-d); }

.sound__bars{ display:flex; align-items:center; gap:calc(3 * var(--s)); height:calc(16 * var(--s)); }
.sound__bars i{
  display:block; width:calc(2.5 * var(--s)); border-radius:calc(2 * var(--s));
  background:var(--ink); opacity:.45;
  transition:height .3s var(--ease), opacity .3s var(--ease);
}
/* 🔴 THE IDLE BARS ARE STAGGERED, NOT EQUAL. Four bars of the same short
   height read as "⋯" — which in a bottom-right corner means "more options",
   not sound. Uneven heights make the silhouette a waveform even when
   nothing is moving, so the control says what it is while switched off. */
.sound__bars i:nth-child(1){ height:calc(5  * var(--s)); }
.sound__bars i:nth-child(2){ height:calc(10 * var(--s)); }
.sound__bars i:nth-child(3){ height:calc(6  * var(--s)); }
.sound__bars i:nth-child(4){ height:calc(12 * var(--s)); }
/* ── IDLE: THE NUDGE ─────────────────────────────────────────
   2026-10-07: the nudge used to retire for good once you had answered the
   control (`:not(.is-engaged)`), on the theory that a button which keeps
   pulsing after you have met it is nagging. Overruled by the user — the
   point of the control is to be noticed, and a returning visitor with sound
   OFF was looking at a dead icon. It now runs whenever the sound is off.
   The control has to ask to be noticed without lying about its state, so
   the idle gesture is deliberately NOT the playing one: a single ripple
   travels left-to-right across the bars and then rests for most of the
   cycle, under a halo that expands once and fades. Periodic and dim reads
   as "press me"; continuous and lit would read as "already playing".

   🔑 scaleY, not height. Each bar has its OWN idle height (5/10/6/12), so
   animating height here would need four keyframe sets and would fight the
   playing animation for the same property. A scale is relative to whatever
   the bar already is, and it composites. */
.sound:not(.is-on) .sound__bars i{
  transform-origin:50% 50%;
  animation:sndnudge 3.6s var(--ease-io) infinite;
}
.sound:not(.is-on) .sound__bars i:nth-child(1){ animation-delay:0s; }
.sound:not(.is-on) .sound__bars i:nth-child(2){ animation-delay:.07s; }
.sound:not(.is-on) .sound__bars i:nth-child(3){ animation-delay:.14s; }
.sound:not(.is-on) .sound__bars i:nth-child(4){ animation-delay:.21s; }
@keyframes sndnudge{
  0%, 50%, 100%{ transform:scaleY(1);    opacity:.45; }
  60%          { transform:scaleY(1.95); opacity:1;   }
  72%          { transform:scaleY(.70);  opacity:.75; }
  84%          { transform:scaleY(1.12); opacity:.55; }
}
/* the halo rides the same 4.2s clock, so the ring leaves as the wave runs */
.sound:not(.is-on)::after{
  content:''; position:absolute; inset:0; border-radius:50%; pointer-events:none;
  border:1px solid rgba(255,255,255,.30);
  animation:sndhalo 3.6s var(--ease-io) infinite;
}
@keyframes sndhalo{
  0%, 44%{ transform:scale(1);    opacity:0;   }
  54%    { transform:scale(1.14); opacity:.55; }
  100%   { transform:scale(1.62); opacity:0;   }
}

/* playing: the bars stand up and breathe, each on its own clock so the
   group never pulses as one block */
.sound.is-on .sound__bars i{ opacity:1; animation:sndbar 1.1s var(--ease-io) infinite; }
.sound.is-on .sound__bars i:nth-child(1){ animation-duration:.9s;  animation-delay:-.2s; }
.sound.is-on .sound__bars i:nth-child(2){ animation-duration:1.3s; animation-delay:-.6s; }
.sound.is-on .sound__bars i:nth-child(3){ animation-duration:1.0s; animation-delay:-.35s; }
.sound.is-on .sound__bars i:nth-child(4){ animation-duration:1.5s; animation-delay:-.8s; }
@keyframes sndbar{
  0%,100%{ height:calc(4 * var(--s)); }
  50%    { height:calc(15 * var(--s)); }
}

/* ============================================================
   LOAD CHOREOGRAPHY
   `.is-ready` (fonts-ready, or the 2.2s watchdog) is the single starting
   gun. Every start-state below is scoped to `.js` so a dead script leaves
   a complete page rather than a blank one.

     100ms   bloom bed rises, and starts breathing on the wind clock
     500ms+  type, element by element via --d
     1450ms  headphone descends out of blur and settles into the bed
     2450ms  nav
   ============================================================ */
/* opacity only — a transform or will-change here would give .nav a stacking
   context and kill the pill's backdrop-filter */
.js .nav{ opacity:0; }
html.is-ready .nav{
  opacity:1;
  /* transform is re-listed because this rule replaces the shorthand above;
     drop it and the bar would TELEPORT away instead of sliding */
  transition:opacity var(--in-nav-t) var(--ease-io) var(--in-nav-d), transform .38s var(--ease);
}

.js .hero__glow{ opacity:0; transform:scale(.86); }
/* the headphone now arrives LAST and from ABOVE — it descends the last
   50px out of blur, so it reads as being lowered into a bed that is
   already there rather than materialising in place */
.js .hero__phones{ opacity:0; transform:translateX(-50%) translateY(calc(-50 * var(--s))) scale(1.045); filter:blur(20px); }
.js .hero__blooms img{ opacity:0; transform:translateY(calc(40 * var(--s))) scale(1.04); filter:blur(12px); }
.js .hero__air{ opacity:0; }
.js .hero__type > *{ opacity:0; transform:translateY(calc(20 * var(--s))); filter:blur(8px); }
.js .h1__line{ opacity:0; transform:translateY(calc(24 * var(--s))); filter:blur(9px); display:block; }

html.is-ready .hero__glow{
  opacity:1; transform:scale(1);
  transition:opacity var(--in-hp-t) var(--ease-io) var(--in-hp-d),
             transform calc(var(--in-hp-t) * 1.5) var(--ease) var(--in-hp-d);
}
html.is-ready .hero__phones{
  opacity:1; transform:translateX(-50%) translateY(0) scale(1); filter:blur(0);
  transition:opacity var(--in-hp-t) var(--ease-io) var(--in-hp-d),
             transform calc(var(--in-hp-t) * 1.35) var(--ease) var(--in-hp-d),
             filter calc(var(--in-hp-t) * 1.1) var(--ease-io) var(--in-hp-d);
}
/* the air fades up WITH the bed: the pollen belongs to the flowers, so it
   must not arrive as a separate event */
html.is-ready .hero__air{
  opacity:1;
  transition:opacity calc(var(--in-bloom-t) * 1.4) var(--ease-io) var(--in-bloom-d);
}
/* Once JS owns these transforms the intro transitions must GO. Leaving a
   1.2s transition attached to a node that scroll writes every frame turns
   the dive into treacle — the lerp in js/v2.js is the only smoothing that
   should be in play. */
html.intro-done .hero__phones,
html.intro-done .hero__glow,
html.intro-done .hero__air{ transition:none; }
html.is-ready .hero__blooms img{
  opacity:1; transform:translateY(0) scale(1); filter:blur(0);
  transition:opacity var(--in-bloom-t) var(--ease-io) var(--in-bloom-d),
             transform calc(var(--in-bloom-t) * 1.25) var(--ease) var(--in-bloom-d),
             filter var(--in-bloom-t) var(--ease-io) var(--in-bloom-d);
}
html.is-ready .hero__type > *{
  opacity:1; transform:translateY(0); filter:blur(0);
  transition:opacity var(--in-type-t) var(--ease-io) calc(var(--in-type-d) + var(--d,0ms)),
             transform var(--in-type-t) var(--ease) calc(var(--in-type-d) + var(--d,0ms)),
             filter calc(var(--in-type-t) * .9) var(--ease-io) calc(var(--in-type-d) + var(--d,0ms));
}
html.is-ready .h1__line{
  opacity:1; transform:translateY(0); filter:blur(0);
  transition:opacity var(--in-type-t) var(--ease-io) calc(var(--in-type-d) + var(--d,0ms)),
             transform calc(var(--in-type-t) * 1.15) var(--ease) calc(var(--in-type-d) + var(--d,0ms)),
             filter calc(var(--in-type-t) * .9) var(--ease-io) calc(var(--in-type-d) + var(--d,0ms));
}

/* ── scroll reveals (below the fold) ─────────────────────── */
.js [data-reveal]{ opacity:0; transform:translateY(calc(26 * var(--s))); }
.js [data-reveal].is-in{
  opacity:1; transform:translateY(0);
  transition:opacity .85s var(--ease-io) calc(var(--g,0ms) + var(--d,0ms)), transform .95s var(--ease) calc(var(--g,0ms) + var(--d,0ms));
}
/* images in a reveal group resolve out of a soft focus, the same
   soft-to-sharp language the hero and the Vision statement use */
.js [data-rgroup] picture[data-reveal]{ filter:blur(calc(10 * var(--s))); }
.js [data-rgroup] picture[data-reveal].is-in{
  filter:blur(0);
  transition:opacity .9s var(--ease-io) calc(var(--g,0ms) + var(--d,0ms)),
             transform 1.1s var(--ease) calc(var(--g,0ms) + var(--d,0ms)),
             filter 1s var(--ease-io) calc(var(--g,0ms) + var(--d,0ms));
}

/* ============================================================
   1 · HERO
   Comp fidelity: the stage is 1080 design px tall and every layer keeps
   its designed relationship to the others. The headphone deliberately
   overshoots the bottom edge by 204 so the earcups sit IN the blooms.
   ============================================================ */
/* The hero is a 1440x1080 composition CONTAIN-FITTED to the viewport.
   It used to be 1080 design px tall, which on a 1512x750 window put the
   bloom bed 380px below the fold — the arc read as floating on nothing.
   Overriding --s locally to the FITTED BOX width means every existing
   calc(N * var(--s)) inside the hero keeps working and scales together. */
.hero{
  position:relative; height:100svh; min-height:600px; overflow:hidden; isolation:isolate;
  display:grid; place-items:center;
  /* MUST be opaque. `isolation:isolate` makes .hero its own blending group, so
     the bloom loop's `mix-blend-mode:screen` composites against THIS backdrop,
     not the body. Left transparent, the video's pure-black areas rendered as
     #000 — darker than the page's #0A0A0A — and drew a visible rectangle.
     With the ground painted here, screen(black, page) == page, i.e. nothing. */
  background:var(--page);
  --s: max(0.62px, calc(min(100vw, 100svh * 1.33333) / 1440));
}
/* ── THE BED HAS TO SINK, NOT STOP (2026-09-22) ──────────────
   `.hero` is overflow:hidden, and the bloom band hangs 112s BELOW it, so the
   hero's own bottom edge was cutting the plate mid-flower: scroll the hero
   away and a hard horizontal line crossed the whole viewport with black
   under it.

   🔴 IT CANNOT BE FIXED ON THE BED. A mask — like a filter or a transform —
   makes `.hero__blooms` a blending group, which is precisely what severs
   `.hero__loop`'s `mix-blend-mode:screen` from the hero's opaque ground (the
   note on .hero says what that looked like: the video's blacks drawing a
   visible rectangle). So the fade is painted OVER the bed instead, as its
   own layer, and nothing about the blend changes.

   z-index 2 puts it above the bed (auto) and the air (2, and this paints
   later), below the type (3) — which ends 318px higher anyway.
   The stops are weighted LOW on purpose: fully page-coloured at the edge so
   the cut cannot survive, but still transparent by the time it reaches the
   flowers you are meant to be looking at. */
.hero::after{
  content:''; position:absolute; left:0; right:0; bottom:0; z-index:2;
  height:calc(240 * var(--s)); pointer-events:none;
  background:linear-gradient(to top,
    var(--page) 0%,
    rgba(10,10,10,.94) 16%,
    rgba(10,10,10,.62) 42%,
    rgba(10,10,10,.24) 70%,
    rgba(10,10,10,0) 100%);
}
.hero__inner{
  position:relative;
  width:calc(1440 * var(--s)); height:calc(1080 * var(--s));
}
.hero__stage{ position:absolute; inset:0; }
.hero__glow{
  position:absolute; left:calc(340 * var(--s)); top:calc(90 * var(--s));
  width:calc(760 * var(--s)); height:calc(520 * var(--s));
  background:radial-gradient(closest-side, rgba(214,194,240,.16), rgba(214,194,240,0) 72%);
  filter:blur(calc(60 * var(--s)));
  animation:breathe 9s var(--ease-io) infinite;
}
@keyframes breathe{ 0%,100%{ opacity:.85 } 50%{ opacity:1 } }

/* <picture> is INLINE by default, so a width on it left the inner <img> at its
   intrinsic 1070x1219 — the arc rendered 774 wide but still 1219 tall and blew
   past the fold. Both layers must be block-level with the img driven to 100%. */
.hero__phones{
  position:absolute; left:50%; bottom:calc(-204 * var(--s));
  width:calc(1100 * var(--s)); transform:translateX(-50%);
  display:block; line-height:0;
}
.hero__phones img{ width:100%; height:auto; }
.hero__blooms{
  position:absolute; left:0; right:0; bottom:calc(-112 * var(--s));
  height:calc(430 * var(--s));
  display:block; line-height:0; overflow:hidden;
}
.hero__blooms picture,
.hero__blooms img,
.hero__blooms video{ position:absolute; left:0; bottom:0; width:100%; height:auto; }
/* Motion is driven through a CUSTOM PROPERTY on the container and applied to
   its CHILDREN. Writing transform on .hero__blooms itself would make it a
   blending group (as would `filter`, even `blur(0)`), which is exactly what
   stopped the loop's screen blend from reaching the hero's backdrop.
   --bedT carries WIND (a lean + drift on a gust clock) and the SCROLL DIVE
   (a scale about the bed's bottom edge) composed into one transform list,
   because two owners writing `transform` on one node fight. The base 1.075
   is overscan: a gust shifts the plate up to 17px sideways and leans it a
   degree, and the plate must never pull its own faded edge into frame. */
.hero__blooms picture,
.hero__blooms video{
  transform-origin:50% 100%;
  transform:var(--bedT, translate3d(0,0,0) scale(1.075));
  /* on the CHILD, never the band: a filter on .hero__blooms would isolate
     the video's screen blend from the hero ground. See js/v2.js §3c. */
  filter:var(--bedF, none);
  will-change:transform;
}
/* the bed grows past the band on the way out — the clip has to let it */
.hero__blooms.is-diving{ overflow:visible; }

/* ── the air ─────────────────────────────────────────────────
   Pollen, blown petals and a few soft fireflies, drawn in canvas rather
   than filmed: the generated plate turns out to move 0.3% frame-to-frame,
   so there is no wind or sparkle in the footage to grade up. Sits above
   the headphone and the bed, below the type. */
.hero__air{
  position:absolute; inset:0; width:100%; height:100%;
  pointer-events:none; z-index:2;
  transform-origin:50% 88%;
}

/* The loop is already cropped to the band's exact ratio (1280x382 = 1440:430),
   so width:100% lands it perfectly over the still.
   `screen` is what makes it composite: the plate is keyed on luminance
   (alpha = L/knee) and against a black ground screen blending is the same
   maths — so the chrome earcups still read through the dark gaps, and no
   alpha-channel video is needed. */
.hero__loop{
  mix-blend-mode:screen;
  opacity:0; transition:opacity 1s var(--ease-io);
  pointer-events:none;
}
.hero__loop.is-live{ opacity:1; }
.hero__blooms.is-live picture{ opacity:0; transition:opacity 1s var(--ease-io); }

/* centred WITHOUT a transform: the scroll-exit writes style.transform, and a
   translateX(-50%) here was being wiped by it — the whole block snapped right
   by half its width. Never centre an animated element with a transform. */
.hero__type{
  position:absolute; left:0; right:0; top:calc(250 * var(--s));
  margin-inline:auto; width:calc(1200 * var(--s));
  display:flex; flex-direction:column; align-items:center;
  z-index:3;
}
.hero__type .h1{ margin-top:calc(26 * var(--s)); }
.hero__type .sub{ margin-top:calc(30 * var(--s)); width:calc(580 * var(--s)); text-wrap:balance; }
/* the user's Figma 878:2 (2026-09-26): ONE line on desktop, 522 wide at 1440.
   --s scales the type with the width, so nowrap never outgrows the stage. */
@media (min-width:1041px){ .hero__type .sub{ width:auto; white-space:nowrap; } }
.hero__actions{ margin:calc(56 * var(--s)) 0 0; display:flex; gap:calc(14 * var(--s)); }
/* the hero pair reads larger on desktop (user, 2026-09-26: "the cta text
   sizes is too small") — 14 → 19 type, 49 → 64 tall; the rest of the site's
   buttons keep the base size */
@media (min-width:1041px){
  /* 🔴 the hero runs its OWN --s (height-led, ≈0.83 at 1440×900), so these
     are design px of THAT scale: 19 → ≈15.8px, 12 → ~11.7 before */
  .hero__actions .btn{ height:calc(64 * var(--s)); padding:0 calc(32 * var(--s)); border-radius:calc(32 * var(--s));
    font-size:calc(19 * var(--s)); gap:calc(11 * var(--s)); }
  .hero__actions .btn__knob{ width:calc(9 * var(--s)); height:calc(9 * var(--s)); }
}

/* ============================================================
   2 · VISION — the pinned window
   The statement and the carriage share ONE pinned viewport. The track
   height IS the scroll budget for the three moments.
   ============================================================ */
.vision{ position:relative; padding-top:calc(150 * var(--s)); }
.vision__type{ display:flex; flex-direction:column; align-items:center; gap:calc(30 * var(--s)); padding:0 calc(120 * var(--s)); }

/* ── ONE PIN, TWO LAYERS ─────────────────────────────────────
   .vstage is a tall track and .vstage__stick pins one full viewport
   inside it; the extra height IS the whole section's scroll budget. Both
   layers are absolutely positioned in that same viewport so the
   statement can cross-dissolve into the carriage — two separate sticky
   tracks are sequential by construction and could never overlap.
   Nothing here may be transformed by JS: moving the containing block of
   a `position:sticky` child kills the pin, so every animation is written
   on the LAYERS and their children instead. */
.vstage{ position:relative; height:300vh; }   /* 480 -> 620 (2026-09-21): the section
   now arrives in three steps — header, then the carriage over ~840px on its own,
   then the rail — and the pan that follows dwells on each of the three moments.
   The extra 140vh is what pays for all of that without shortening the pan. */
.vstage__stick{ position:sticky; top:0; height:100svh; }
.vlayer{
  position:absolute; inset:0;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
}
/* ── VW1 · CAPTION BAR ───────────────────────────────────────
   THE WHOLE POINT OF THIS LAYOUT: the section pins, so all of it has to
   live inside one 100svh stage. The Figma comp is 1300 design px and the
   stage is ~770 at 1512x805, so something has to give — and it must not
   be the window, because the window is the only place the argument is
   legible. So the header and the caption rail are fixed height and the
   carriage takes what is left, CROPPING instead of scaling. The window
   stays 516 design px wide at every viewport height; you simply see more
   or less carriage above and below it. */
.vlayer--win{
  justify-content:center;
  /* 104 of top padding is the nav's own height: everything else on this
     page scrolls UNDER the glass pill by design, but a header that does it
     is just unreadable. */
  padding:calc(104 * var(--s)) 0 calc(28 * var(--s));
  gap:calc(26 * var(--s));
}
.vwin__head{
  flex:none;
  display:flex; flex-direction:column; align-items:center;
  gap:calc(18 * var(--s));
  padding:0 calc(120 * var(--s));
}
/* 46, not the system 54: the Vision statement above owns this section's
   headline voice at 68, so this one has to read as its subordinate — and
   the height it gives back goes straight into the window. */
.vwin__head .h2{ font-size:calc(46 * var(--s)); text-wrap:pretty; }
/* the headline builds line by line, so each line is its own block */
.vline{ display:block; }
.js .vline,
.js .vision__type .eyebrow{ opacity:0; }
/* the carriage dissolves in halfway through the pin (js/v2.js §5), so the
   whole layer starts hidden — script-scoped, so a dead script still shows
   the section complete */
.js .vlayer--win{ opacity:0; }
/* …and each band inside it owns its own start state, because they arrive
   in order: HEADER first (2026-09-21 — the words name the thing before the
   thing appears), then the carriage, then the rail. The LAYER's opacity is
   now only the section's fade to black. */
.js .vwin__frame,
.js .vwin__head .eyebrow,
.js .vwin__head .h2,
.js .vrail{ opacity:0; }

/* the flexible band: takes the leftover height and clips the plate */
.vwin__frame{
  position:relative; width:100%;
  flex:1 1 auto;
  min-height:calc(330 * var(--s));      /* below this the seats stop reading */
  max-height:calc(602 * var(--s));      /* never taller than the plate can fill */
  overflow:hidden;
}
/* A SHORT LAPTOP has to degrade on purpose, not by overflowing the pin.
   Below ~760px the fixed bands no longer leave the band its minimum, and
   `justify-content:center` would silently start clipping the header into
   the nav. So the type gives up a few px first. */
@media (min-width:1041px) and (max-height:790px){
  .vlayer--win{ padding-top:calc(92 * var(--s)); gap:calc(22 * var(--s)); }
  .vwin__head{ gap:calc(14 * var(--s)); }
  .vwin__head .h2{ font-size:calc(40 * var(--s)); }
  .vwin__frame{ min-height:calc(300 * var(--s)); }
  .vcap__b{ margin-top:calc(9 * var(--s)); font-size:calc(14 * var(--s)); }
}
/* The plate holds its own 1440:660 ratio and is anchored from the TOP by a
   fixed 58 design px, which is the only crop the composition can afford:
   above the window is empty upper wall, below it are the two seats — the
   whole reason this image is here — so the surplus is always taken off the
   top and the bottom, never off the middle. Keeping the plate a fixed
   ratio is also what keeps the glass percentages below valid. */
.vwin__plate{
  position:absolute; left:0; top:calc(-58 * var(--s));
  width:100%; aspect-ratio:1440 / 660;
}
/* `height:auto` is NOT optional here. The img carries width=1440 height=660
   attributes, and `aspect-ratio` on the plate is only a PREFERRED size —
   content taller than it wins. Without this the plate stayed 660px tall at
   every width, so on a 402px phone the glass computed to 284 of a 184-tall
   plate and the window hung out of its own frame. */
.vwin__carriage img{ width:100%; height:auto; }
/* faded into the page ground on both edges — "every image is faded to
   exactly this", and the crop would otherwise end on a hard line.
   🔴 z-index:1, BELOW the glass. The fades exist to hide the plate's own
   CROP EDGES, and the window is not a crop edge. At 1512x805 the bottom
   fade is 126px tall while only 76px of frame sits under the glass, so it
   was climbing 49px into the window and taking 14% of its brightness at
   90% down, 25% at 95% and 33% at the last row — the image reading as cut
   off at the bottom. The top fade was doing the same for 27px (34% on the
   top row). The gap shrinks as the viewport gets shorter, so capping the
   fade heights would only move the bug; the window has to paint above. */
.vwin__fade{ position:absolute; left:0; right:0; pointer-events:none; z-index:1; }
.vwin__fade--t{
  top:0; height:calc(56 * var(--s));
  background:linear-gradient(to bottom, var(--page), rgba(10,10,10,0));
}
.vwin__fade--b{
  bottom:0; height:calc(120 * var(--s));
  background:linear-gradient(to top, var(--page), rgba(10,10,10,.5) 55%, rgba(10,10,10,0));
}

/* the glass, measured off the carriage plate (x470 y88, 516x284 of 1440x660)
   and expressed as percentages so it tracks the image at any width */
/* z-index:2 lifts the window clear of the fades above. Safe because nothing
   between here and .vstage__stick opens a stacking context — .vwin__plate
   and .vwin__frame are both positioned at z-index:auto, and `overflow:hidden`
   and `border-radius` do not make one — so the glass and the fades resolve
   against the same ancestor and simply stack. */
.vwin__glass{
  position:absolute; left:32.6389%; top:13.3333%; width:35.8333%; height:43.0303%;
  overflow:hidden; border-radius:calc(14 * var(--s)); z-index:2;
}
.vwin__track{
  position:absolute; inset:0 auto 0 0; width:300%;      /* 1548 / 516 */
  transform:translate3d(0,0,0); will-change:transform;
}
.vwin__track img{ width:100%; height:100%; object-fit:cover; }

/* ── THE RAIL ────────────────────────────────────────────────
   Three captions on ONE track that travels with the landscape. The row of
   three columns it replaces could not say which of three a CENTRED window
   belonged to — the lit column sat up to 600px away and off the window's
   axis, and a colour change will not carry that distance while a picture is
   moving. Two things that move together, on the other hand, are read as one
   object without being told, which is the mechanism the rest of this page
   already runs on.
   🔑 EVERY WIDTH HERE IS THE GLASS'S OWN PERCENTAGE (35.8333%), so a caption
   is exactly as wide as the window above it at any viewport. */
.vrail{ flex:none; position:relative; width:100%; }
/* the gates: two ticks marking the window's column, so the rail reads as a
   slot things pass through rather than a row that happens to move */
.vrail__gate{
  position:absolute; top:calc(-22 * var(--s)); width:1px; height:calc(20 * var(--s));
  background:var(--lilac); opacity:.45;
}
.vrail__gate--l{ left:32.6389%; }
.vrail__gate--r{ left:68.4722%; }        /* 32.6389 + 35.8333 */
.vrail__clip{ width:100%; overflow:hidden; }
.vrail__track{ position:relative; width:100%; height:calc(140 * var(--s)); will-change:transform; }
.vcap{
  position:absolute; top:0; width:35.8333%;
  display:flex; flex-direction:column; align-items:center; text-align:center;
}
/* pitch = one caption (35.8333%) + a 108 design px gap (7.5%) */
.vcap:nth-child(1){ left:32.6389%; }
.vcap:nth-child(2){ left:75.9722%; }
.vcap:nth-child(3){ left:119.3055%; }
.vcap__r{
  display:block; width:100%; height:2px; background:#232320;
  transition:background .55s var(--ease-io);
}
.vcap__n{
  margin-top:calc(15 * var(--s));
  font-family:var(--mono); font-size:calc(11 * var(--s)); letter-spacing:.14em;
  color:#55554F; transition:color .55s var(--ease-io);
}
.vcap__l{
  margin-top:calc(8 * var(--s));
  font-family:var(--mono); font-weight:500; text-transform:uppercase;
  font-size:calc(16 * var(--s)); letter-spacing:.06em;
  color:#55554F; transition:color .55s var(--ease-io);
}
.vcap__b{
  margin:calc(11 * var(--s)) 0 0;
  font-size:calc(15 * var(--s)); line-height:1.45; letter-spacing:-.01em;
  color:#6E6E68; transition:color .55s var(--ease-io);
}
.vcap.is-active .vcap__r{ background:var(--lilac); }
.vcap.is-active .vcap__n{ color:var(--lilac); }
.vcap.is-active .vcap__l{ color:var(--ink); }
.vcap.is-active .vcap__b{ color:var(--body); }

/* ============================================================
   3 · WHY — ONE PIN, A FIXED BODY OF WATER, A MOVING FOREGROUND
   .wtrack is the scroll budget; .wtrack__stick pins one viewport inside
   it. The water is the only thing that does not move — it fades up out of
   the train's darkness, holds, and fades back to black before Products.
   Everything animated is written by js/v2.js §4 on the LAYERS, never on
   the sticky element itself.
   ============================================================ */
/* THE HAND-OFF. A sticky track spends its last viewport releasing, so the
   train's (already black) stage scrolls away over a full 805px — a dead
   flick of nothing. Pulling Why up by 30vh lets its water start coming up
   inside that release, which keeps the brief's black beat (~560px, still
   unmistakably total black) without the dead scroll. Safe because by then
   the train is at opacity 0, and .wtrack__stick is opaque so it simply
   paints over the spent stage. */
.why{ position:relative; margin-top:-30vh; }
.wtrack{ position:relative; height:400vh; }  /* 460 → 400 when the truths went (2026-09-26) */
.wtrack__stick{
  position:sticky; top:0; height:100svh; overflow:hidden;
  background:var(--page);
}

/* ── the water ─────────────────────────────────────────────── */
.why__bg{ position:absolute; inset:0; }
.why__poster,.why__loop{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; display:block;
}
/* 🔴 `<picture>` is NOT a replaced element — object-fit on it does nothing,
   and the <img> inside inherits no sizing, so it sat at its intrinsic 1440
   and left a black strip at any viewport wider than that. Same class of bug
   as the carriage img needing `height:auto`: the wrapper gets the CSS, the
   image doesn't. */
.why__poster img{ width:100%; height:100%; object-fit:cover; display:block; }
.why__loop{ opacity:0; transition:opacity 1.1s var(--ease-io); }
.why__loop.is-live{ opacity:1; }
.js .why__bg{ opacity:0; }
/* legibility over moving water: a top scrim for the eyebrow band plus a
   soft centre pool the type sits in. Both are cheap and neither animates. */
.why__vig{
  position:absolute; inset:0; pointer-events:none;
  background:
    radial-gradient(64% 58% at 50% 46%, rgba(5,8,8,.62), rgba(5,8,8,0) 74%),
    linear-gradient(180deg, rgba(5,8,8,.58) 0%, rgba(5,8,8,.10) 46%, rgba(5,8,8,.55) 100%);
}

/* ── the hands ─────────────────────────────────────────────── */
.whands{ position:absolute; inset:0; }
/* Both halves keep the FULL 1440 width so they stay in register with each
   other; only their transform differs. The meeting point is at 50.69% of
   the width, which is the plate's own axis of symmetry. */
/* The MEETING LINE — the fingertips — is 258 design px down the 652-tall
   crop. Anchoring `top` as `62% - 258s` keeps that line at a constant 62%
   of the stage at every viewport height, which is what stops the arms
   drifting into the headline on a short laptop. No translateY here: JS
   owns this transform and two owners would fight. */
.whand{
  position:absolute; left:0; width:100%;
  top:calc(61% - 258 * var(--s));
  z-index:3; will-change:transform, opacity;
}
.whand img{ width:100%; height:auto; }
.js .whand{ opacity:0; }
/* the light between them — it belongs to the MEETING, so it is its own
   element and only exists once they are close */
.whand__glow{
  /* on the brightest row of the tip zone (240 of 652), not the arms' centre */
  position:absolute; left:50.69%; top:61%; z-index:4;
  width:calc(520 * var(--s)); height:calc(520 * var(--s));
  margin:calc(-278 * var(--s)) 0 0 calc(-260 * var(--s));
  border-radius:50%; pointer-events:none;
  background:radial-gradient(closest-side,
    rgba(255,246,226,.60), rgba(255,232,190,.20) 38%, rgba(214,194,240,0) 72%);
  opacity:0;
}

/* 🔴 A SHORT LAPTOP CANNOT HOLD ALL THREE. Below 800px there is not enough
   height for thesis + hands + truths, so the thesis is what gives way (JS
   fades and lifts it on the same `lift` phase) while the hands stay put and
   the truths still arrive below them. The hands never leave at any size —
   that is the part of the brief that must not bend. */
@media (min-width:1041px) and (max-height:799px){
  .whand{ top:calc(56% - 258 * var(--s)); }
  .whand__glow{ top:56%; }
  .why__thesis{ top:calc(96 * var(--s)); gap:calc(20 * var(--s)); }
}

/* ── the foreground ───────────────────────────────────────── */
.why__thesis{
  position:absolute; left:0; right:0; z-index:5;
  will-change:transform, opacity;
}
/* Anchored to the TOP, clearing the nav, so the gap to the hands below is
   the same on a 700px laptop as on a 1000px one.
   THE WHOLE SECTION NOW HAS TO HOLD THREE THINGS AT ONCE — thesis, the met
   hands, and the three truths under them — inside one pinned viewport. At
   1512x805 that is 725px of usable height for 300 + 130 + 183 of content,
   i.e. two 30px gaps and nothing spare. Every number below is that budget. */
.why__thesis{
  top:calc(91 * var(--s));
  display:flex; flex-direction:column; align-items:center; gap:calc(26 * var(--s));
  padding:0 calc(120 * var(--s));
}
.why__thesis .lede{ width:calc(560 * var(--s)); }
/* THE USER'S FIGMA (919:2, 2026-09-26) — "the desktop looks very crowded":
   the statement drops from 54 to 42 and wraps in a ~700 measure, the need
   gets its OWN last line at the old 54, and the lede opens to 940 so it
   sits in two lines. Desktop only: the phone keeps one run-on paragraph. */
@media (min-width:1041px){
  /* one step down (user, 2026-09-28: "the text is looking crowded") — 42/54 → 36/44 */
  .why__thesis .h2{ font-size:calc(36 * var(--s)); line-height:calc(48 * var(--s)); }
  .why__lead{ display:block; max-width:calc(700 * var(--s)); margin:0 auto; }
  /* the two lines are written out, not wrapped, so each can arrive on its own */
  .why__ln{ display:block; }
  .why__thesis .h2 em{ display:block; font-size:calc(44 * var(--s)); }
  .why__thesis .lede{ width:calc(940 * var(--s)); }
}
@media (min-width:1041px) and (max-height:799px){
  .why__thesis .h2{ font-size:calc(33 * var(--s)); line-height:calc(44 * var(--s)); }
  .why__thesis .h2 em{ font-size:calc(40 * var(--s)); }
}
.js .why__thesis [data-wstep]{ opacity:0; }

/* ============================================================
   4 · PRODUCTS — angled hero phone on chrome ribbon (PV4)
   The phone is 956 tall in a 760 band and starts at y-60: that overshoot
   is what makes it read as bigger than the frame.
   ============================================================ */
/* same hand-off trim as Why: the water is already black by the time the
   why track starts releasing, so Products arrives inside that release
   instead of after a full viewport of dead scroll. Products paints over
   the spent stage because it is later in the DOM. */
.products{ position:relative; margin-top:-30vh; }
/* THE FOURTH PIN (2026-09-22). 260vh of track: ~160vh of assembly, then the
   composed stage holds and leaves in one piece. */
/* 🔴 AND IT STILL NEEDS ITS CLOSING FADE. I retired this on the theory that
   an opaque black stick already "ends on darkness" — it does not. When the
   stick unpins it scrolls up and its own bottom EDGE becomes the hard
   horizontal line this gradient was written to kill in the first place, with
   the CTA's dawn starting directly under it. Sitting at the bottom of the
   TRACK, it is out of the viewport for the whole assembly and the whole
   hold, and rises through the composition only as the stage leaves.
   z-index 3 clears the sticky element, which is its own stacking context. */
.products::after{
  content:''; position:absolute; left:0; right:0; bottom:0;
  height:calc(300 * var(--s)); z-index:3; pointer-events:none;
  background:linear-gradient(to top, var(--page), rgba(10,10,10,.62) 46%, rgba(10,10,10,0));
}
.ptrack{ position:relative; height:390vh; }   /* +100vh hold for the CTA (2026-10-01) */
.ptrack__stick{
  position:sticky; top:0; height:100svh; overflow:hidden;
  background:var(--page);
}
.ptrack__stick .prow{ height:100%; }
/* the header now shares the pinned viewport with the band instead of sitting
   in its own block above it — so it loses the 120/96 padding it could afford
   when it had the screen to itself */
.products__header{
  position:absolute; top:0; left:0; right:0; z-index:4;
  display:flex; flex-direction:column; align-items:center;
  /* 🔴 112, not 56: the nav is a FLOATING PILL ~100px deep, and a
     top-anchored header in a pinned viewport is the one header on this page
     that can collide with it. The other three sections centre in their
     stage and never come near it. */
  gap:calc(18 * var(--s)); padding:calc(112 * var(--s)) calc(120 * var(--s)) 0;
}
.js [data-phead]{ opacity:0; }

.prow{ position:relative; height:calc(760 * var(--s)); overflow:hidden; }
/* 🔴 HEADROOM ON ALL FOUR SIDES. The ribbon now travels HORIZONTALLY as well
   as vertically, and a plate sized exactly to its band opens a black gap at
   the edge the moment it moves — the same trap the hero bed and the CTA dawn
   both taught. 80 design px of overscan against a travel bounded to ±55. */
.prow__bg{
  display:block; position:absolute; inset:calc(-80 * var(--s)); will-change:transform;
  /* THE RIBBON DRAWS ITSELF IN along its own flow: a soft-edged wipe driven
     by --wipe (js/v2.js §3c). The default is past 100% so a dead script — or
     reduced motion — simply shows the whole ribbon. */
  /* 🔴 270deg, NOT 90: "the ribbon fading in from right to left". This was
     the flip band's direction and is now the only one — Playchat is out. */
  -webkit-mask-image:linear-gradient(270deg, #000 var(--wipe, 130%), transparent calc(var(--wipe, 130%) + 16%));
          mask-image:linear-gradient(270deg, #000 var(--wipe, 130%), transparent calc(var(--wipe, 130%) + 16%));
}
/* the second band's ribbon flows the other way, so it draws the other way */
.prow--flip .prow__bg{
  -webkit-mask-image:linear-gradient(270deg, #000 var(--wipe, 130%), transparent calc(var(--wipe, 130%) + 16%));
          mask-image:linear-gradient(270deg, #000 var(--wipe, 130%), transparent calc(var(--wipe, 130%) + 16%));
}
/* the copy arrives on the band's timeline now, after the ribbon and the
   device — not on the generic time-delayed reveal, which could not know
   whether the ribbon had even appeared yet */
.js [data-pstep]{ opacity:0; }
.prow__bg img{ width:100%; height:100%; object-fit:cover; }
.prow__scrim{ position:absolute; inset:0; }
.prow__scrim--left{ background:linear-gradient(90deg, rgba(10,10,10,.95) 0%, rgba(10,10,10,.82) 38%, rgba(10,10,10,0) 66%); }
.prow__scrim--right{ background:linear-gradient(90deg, rgba(10,10,10,0) 34%, rgba(10,10,10,.82) 62%, rgba(10,10,10,.95) 100%); }

/* THE DEVICE — Figma 688:2.
   The plate is the 910x2022 chrome body plus a symmetric 26px pad for the
   side buttons, so the box is 454.57 wide (430 * 962/910) and sits 12.29
   left of and above the body: the BODY still occupies exactly the 430 design
   px at left 830 / 180 that the old cut-out did.

   🔴 TOP IS 66, NOT -60. The first port reused the old -60 and the device
   still read as a slab: at 955.5 tall in a 760 band the crop takes BOTH ends,
   hiding the corner radius and the punch-hole camera, which are the two
   things that say "phone". Solve instead for the top that puts the ROTATED
   bounding box 30px inside the band and let only the bottom bleed —
   halfH = 215*sin13 + 477.75*cos13 = 513.9, so centre 544 and body top 66.
   Cropping a product shot at the bottom is normal; cropping both ends is
   what made it look like a coloured rectangle. */
.phone{
  position:absolute; top:calc(53.71 * var(--s)); width:calc(454.57 * var(--s));
  transform:rotate(-13deg); transform-origin:50% 50%;
}
.phone--right{ left:calc(817.71 * var(--s)); }
/* inside the pin the header takes the top ~190px, so the device drops to
   clear it. Only the bottom crops — cropping both ends is what made it read
   as a coloured rectangle the first time. */
/* 🔴 SIZED OFF THE ROTATED EDGE, NOT THE BOUNDING BOX. At the band's old
   454x981 the device's BOX cleared the headline by 2px — but it is rotated
   -13deg, so its top edge runs from its left corner UP to the right and
   crossed y=216 at x=903, straight through "a very human truth." The pin
   now holds four things instead of two, so the device gives up some scale:
   at 380x820 from top 250 the same edge crosses at y=281, clear of the
   headline's 258, and the screen's centre still lands at 660 — inside the
   729 viewport, so the frnd wordmark is still on screen. */
.ptrack__stick .phone{ top:calc(250 * var(--s)); width:calc(380 * var(--s)); }
.ptrack__stick .phone--right{ left:calc(892 * var(--s)); }
.phone--left{ left:calc(167.71 * var(--s)); transform:rotate(13deg); }
/* the alpha edge is the device silhouette, so there is no wrapper to bevel —
   the chrome, the bezel and the glass are all in the plate */
.phone__device{ display:block; width:100%; height:auto; }
.phone__glow{
  position:absolute; left:50%; top:50%; width:240%; height:110%;
  transform:translate(-50%,-50%); border-radius:50%;
  background:radial-gradient(closest-side, rgba(255,62,132,.30), rgba(255,62,132,0) 70%);
  filter:blur(calc(70 * var(--s)));
}
.phone__glow--violet{ background:radial-gradient(closest-side, rgba(120,75,208,.32), rgba(120,75,208,0) 70%); }

/* same rule as .hero__type — no transform centring on an animated element */
.prow__copy{
  position:absolute; top:0; bottom:0;
  display:flex; flex-direction:column; justify-content:center;
  width:calc(500 * var(--s)); left:calc(120 * var(--s)); z-index:3;
  /* the device now sits lower in the band (centre 544 against the band's
     380), so a band-centred column floated high with dead space under it.
     Padding on a centred flex column moves the content by HALF the value,
     so 91 buys the 45.5 that splits the difference. Done this way and not
     with `top` because the no-transform-centring rule still applies here. */
  padding-top:calc(91 * var(--s));
}
.prow--flip .prow__copy{ left:auto; right:calc(120 * var(--s)); }
/* the copy centres in what is LEFT of the viewport under the header, not in
   the whole of it. Padding on a centred flex column moves content by half
   the value, so 200 buys the 100 that clears the header. */
.ptrack__stick .prow__copy{ padding-top:calc(200 * var(--s)); }
/* the quiet line that replaced the Playchat band. Deliberately the lowest-
   contrast type on the page: it is a footnote, not a second product. */
.pstealth{
  margin:calc(26 * var(--s)) 0 0; font-family:var(--mono);
  font-size:calc(12 * var(--s)); letter-spacing:.09em; line-height:1.5;
  /* 🔴 THE SUBTLETY IS IN THE COLOUR, NOT AN OPACITY. The timeline writes an
     inline opacity on every [data-pstep] as it arrives, so an `opacity` here
     is overwritten the moment the line lands — it would only have "worked"
     on the compact path, where nothing animates. #6E6E68 is --muted taken
     down to roughly half its contrast against the page. */
  color:#6E6E68;
}
.tag{ margin:0; font-family:var(--mono); font-size:calc(11 * var(--s)); letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
.pname{ margin:calc(18 * var(--s)) 0 calc(16 * var(--s)); font-size:calc(36 * var(--s)); font-weight:500; letter-spacing:-.03em; display:flex; align-items:center; gap:calc(12 * var(--s)); }
/* the frnd script wordmark (Figma MVP 1017:10) in place of the typed name */
.pname__logo{ display:block; height:calc(44 * var(--s)); width:auto; }
.pdesc{ margin:0 0 calc(24 * var(--s)); font-size:calc(17 * var(--s)); color:var(--body); }
.pill{
  font-family:var(--mono); font-size:calc(10 * var(--s)); letter-spacing:.12em; text-transform:uppercase;
  color:var(--muted); border:1px solid var(--hairline); border-radius:calc(11 * var(--s));
  padding:calc(5 * var(--s)) calc(10 * var(--s)); font-weight:500;
}
/* .plink / .pill--soon are retired: the product CTA is now a real .btn */

/* ============================================================
   4b · USER STORIES — the heart of faces (2026-09-25)
   One pin. Everything inside .sstage is laid out in DESIGN PX on the
   Figma's 1440×1000 stage (390×844 on a phone) and the stage is scaled
   to fit by js/v2.js §5b — so type sizes here are px, not --s. Tiles are
   positioned only by transform, written by the timeline.
   ============================================================ */
.stories{ position:relative; background:var(--page); }
.strack{ position:relative; height:560vh; }
.strack__stick{ position:sticky; top:0; height:100svh; overflow:hidden; }
.sglow{
  position:absolute; left:50%; top:46%; width:78vw; height:78vh; transform:translate(-50%,-50%);
  background:radial-gradient(closest-side, rgba(158,90,242,.30), rgba(242,76,115,.10) 55%, rgba(10,10,10,0));
  filter:blur(40px); pointer-events:none; opacity:0;
}
.sstage{ position:absolute; left:0; top:0; width:1440px; height:1000px; transform-origin:0 0; }
.sstage__eb{ position:absolute; left:0; right:0; top:64px; font-size:11px; gap:10px; }
.sstage__eb .dot{ width:5px; height:5px; }
.sfield{ position:absolute; inset:0; }
.stile{
  position:absolute; left:0; top:0; padding:0; border:0; margin:0; cursor:pointer;
  border-radius:3px; overflow:hidden; background:#151513; opacity:0;
  transform-origin:0 0; will-change:transform, opacity;
}
.stile img{ width:100%; height:100%; object-fit:cover; object-position:50% 28%; display:block; pointer-events:none; }
.stile:focus-visible{ outline:2px solid var(--lilac); outline-offset:3px; }
.stile.is-far img{ filter:blur(3px); }
.stile:hover{ z-index:5; }
/* "Tap any face" as a CTA (2026-09-28): a cream pill, centred under the
   nucleus; JS writes only its `top` and its pointer-events */
.snuc__hint.snuc__cta{
  left:50%; right:auto; transform:translateX(-50%); display:inline-flex; align-items:center; gap:10px;
  padding:12px 20px; border:0; border-radius:999px; cursor:pointer; white-space:nowrap;
  background:var(--ink); color:#0A0A0A; text-shadow:none;
  font-family:var(--mono); font-weight:700; font-size:12px; letter-spacing:.16em; text-transform:uppercase;
  box-shadow:0 0 0 6px rgba(214,194,240,.12), 0 10px 34px rgba(214,194,240,.35);
  transition:transform .35s var(--ease), box-shadow .35s var(--ease);
}
.snuc__hint.snuc__cta .btn__arrow{ font-family:var(--sans); letter-spacing:0; }
.snuc__hint.snuc__cta:hover,.snuc__hint.snuc__cta:focus-visible{ transform:translateX(-50%) scale(1.05); box-shadow:0 0 0 8px rgba(214,194,240,.18), 0 12px 40px rgba(214,194,240,.45); outline:none; }
.snuc{ position:absolute; left:0; top:0; opacity:0; pointer-events:none; transform-origin:50% 50%; }
.snuc span{ position:absolute; border-radius:1px; background-size:cover; background-position:50% 28%; }
.snuc__hint{
  position:absolute; left:0; right:0; margin:0; text-align:center; opacity:0; pointer-events:none;
  /* bolder + brighter (user, 2026-09-26: "so that my eyes falls on it") —
     700 on the variable mono, full ink, a lilac glow; it is the one call to
     action once the heart has opened */
  font-family:var(--mono); font-weight:700; font-size:13px; letter-spacing:.16em; text-transform:uppercase;
  color:var(--ink); text-shadow:0 0 18px rgba(214,194,240,.55);
}
.shl{
  position:absolute; top:772px; margin:0; font-family:var(--serif); font-size:64px; line-height:1.03;
  letter-spacing:-.03em; opacity:0; will-change:transform, opacity;
}
.shl span,.shl em{ display:block; }
/* the headlines sit over the faces: taps must fall through to the field (audit 2026-10-02) */
.shl, .sstage__eb{ pointer-events:none; }
.shl--l{ left:112px; text-align:left; }
.shl--r{ right:112px; text-align:right; }
.shl__dim{ color:#55554F; }
.shl__lit{ color:var(--ink); }
.shl em{ font-style:italic; color:var(--lilac); }
.scenter{ position:absolute; left:0; right:0; top:800px; display:flex; flex-direction:column; align-items:center; gap:18px; opacity:0; }
.slede{ margin:0; text-align:center; color:var(--muted); font-size:15px; line-height:1.5; }
.sbtn{ height:42px; padding:0 18px; gap:8px; border-radius:21px; font-size:14px; }

/* the phone stage: 390×844 design px, the same beats re-set for a column */
.sstage--m{ width:390px; height:844px; }
.sstage--m .sstage__eb{ top:44px; }
.sstage--m .shl{ top:458px; font-size:34px; left:0; right:0; text-align:center; }
.sstage--m .shl--r{ top:530px; }
.sstage--m .scenter{ top:622px; gap:16px; }
.sstage--m .slede{ font-size:14px; line-height:1.5; max-width:316px; margin:0 auto; text-wrap:balance; }
.sstage--m .slede br{ display:none; }   /* the desktop break is too wide for 360 (2026-09-28) */

/* ── the open story ── */
.sopen{ position:fixed; inset:0; z-index:1000; }
.sopen[hidden]{ display:none; }
.sopen__scrim{
  position:absolute; inset:0; background:radial-gradient(ellipse at 50% 50%, rgba(0,0,0,.30), rgba(0,0,0,.82));
  -webkit-backdrop-filter:blur(14px); backdrop-filter:blur(14px); opacity:0; transition:opacity .5s var(--ease-io);
}
.sopen.is-open .sopen__scrim{ opacity:1; }
.sopen__card{
  position:absolute; left:calc(50% - min(33vw, 480px)); top:50%; width:min(29vw, 420px); aspect-ratio:3 / 4;
  margin-top:calc(min(29vw, 420px) * -0.6667); border-radius:6px; overflow:hidden; background:#151513;
  box-shadow:0 40px 90px rgba(0,0,0,.6); transform-origin:0 0;
}
.sopen__card img{ width:100%; height:100%; object-fit:cover; object-position:50% 28%; display:block; }
.sopen__fade{ position:absolute; left:0; right:0; bottom:0; height:44%; background:linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,.82)); }
.sopen__name{ position:absolute; left:26px; bottom:52px; margin:0; font-family:var(--serif); font-size:40px; line-height:1; letter-spacing:-.02em; color:var(--ink); }
.sopen__meta{ position:absolute; left:28px; bottom:26px; margin:0; font-family:var(--mono); font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
.sopen__x{
  position:absolute; right:16px; top:16px; width:36px; height:36px; border-radius:18px; border:1px solid rgba(244,243,239,.25);
  background:rgba(0,0,0,.45); color:var(--ink); font-size:20px; line-height:1; cursor:pointer;
}
.sopen__col{
  position:absolute; left:calc(50% - min(33vw, 480px) + min(29vw, 420px) + min(5vw, 70px)); top:50%;
  width:min(34vw, 480px); transform:translateY(-50%); opacity:0;
}
.sopen__count{ margin:0 0 18px; font-family:var(--mono); font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--lilac); }
.sopen__q{ margin:0; font-family:var(--serif); font-style:italic; font-size:clamp(22px, 2.2vw, 32px); line-height:1.18; letter-spacing:-.01em; color:var(--ink); }
.sopen__q .w{ opacity:.16; transition:opacity .35s var(--ease-io); }
.sopen__q .w.on{ opacity:1; }
/* the story itself (2026-09-26): real accounts are told in brief, under the
   headline — the headline no longer reads along, since there is no voice yet */
.splayer[hidden]{ display:none !important; }   /* voice off until real recordings */
.sopen__b{ margin:16px 0 0; font-size:15px; line-height:1.6; color:var(--body); max-width:34em; }
.splayer{
  margin-top:40px; height:64px; display:flex; align-items:center; gap:14px; padding:0 20px 0 10px;
  border-radius:32px; background:rgba(244,243,239,.06); box-shadow:inset 0 0 0 1px rgba(244,243,239,.14);
}
.splayer__btn{ flex:none; width:44px; height:44px; border-radius:22px; border:0; background:var(--lilac); cursor:pointer; position:relative; }
.splayer__icon{ position:absolute; left:50%; top:50%; width:12px; height:14px; transform:translate(-40%,-50%); background:#0A0A0A; clip-path:polygon(0 0, 100% 50%, 0 100%); }
.splayer.is-playing .splayer__icon{ transform:translate(-50%,-50%); clip-path:polygon(0 0,35% 0,35% 100%,0 100%,0 0,65% 0,100% 0,100% 100%,65% 100%,65% 0); }
.splayer__bars{ flex:1; height:32px; display:flex; align-items:center; gap:5px; overflow:hidden; }
.splayer__bars i{ flex:none; width:3px; border-radius:2px; background:rgba(244,243,239,.35); transition:background .3s; }
.splayer__bars i.on{ background:var(--lilac); }
.splayer__t{ flex:none; font-family:var(--mono); font-size:11px; letter-spacing:.06em; color:var(--muted); }
.sopen__label{ margin:14px 0 0; font-family:var(--mono); font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
.sopen__nav{
  position:absolute; top:50%; margin-top:-24px; width:48px; height:48px; border-radius:24px; cursor:pointer;
  border:1px solid rgba(244,243,239,.25); background:transparent; color:var(--ink); font-family:var(--serif); font-size:30px; line-height:1;
  opacity:0; transition:opacity .4s var(--ease-io) .3s;
}
.sopen__nav--prev{ left:max(24px, calc(50% - min(33vw, 480px) - 80px)); }
.sopen__nav--next{ right:max(24px, calc(50% - min(33vw, 480px) - 80px)); }
.sopen__hint{ position:absolute; left:0; right:0; bottom:28px; margin:0; text-align:center; font-family:var(--mono); font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:#55554F; opacity:0; transition:opacity .4s .4s; }
.sopen.is-open .sopen__nav,.sopen.is-open .sopen__hint{ opacity:1; }

/* ============================================================
   5 · LEADERSHIP  +  6 · ANNOUNCEMENTS   (2026-09-25, rev 2)
   Both are PINS now (.seqtrack), like every section around them: out of
   the dark, header → subheader → images → text, hold, back to dark.
   Everything must fit ONE viewport, so sizes ride --k: the usual --s,
   but capped by the viewport HEIGHT (760 design px of stage).
   Beats: [data-seq="from,to"], driven by js/v2.js §5c.
   ============================================================ */
.lead,.news{ position:relative; background:var(--page); }
.seqtrack{ position:relative; height:300vh; }
.seqtrack__stick{ position:sticky; top:0; height:100svh; overflow:hidden; display:flex; align-items:center; background:var(--page); }
/* 2026-10-01: no headroom reserved for the nav any more. The stage uses the
   whole viewport and is centred in it; the auto-hiding nav simply comes in
   OVER it when the user scrolls up. --k is sized against 720 design px of
   stage (was 780 incl. 64 of top padding), so the content grows to fill. */
.seq__inner{ --k:min(var(--s), calc(100svh / 720)); width:100%; padding:0 calc(120 * var(--s)); will-change:opacity; }
.js [data-seq]{ opacity:0; }

.lead__head{ display:flex; flex-direction:column; align-items:center; gap:calc(16 * var(--k)); }
.lead__inner .h2,.news__inner .h2{ font-size:calc(50 * var(--k)); }
.lead__lede{ max-width:none; font-size:calc(16 * var(--k)); line-height:1.5; }
.lead__row{ display:grid; grid-template-columns:1fr 1fr; gap:calc(40 * var(--s)); margin-top:calc(46 * var(--k)); }
.lcard{ display:grid; grid-template-columns:calc(236 * var(--k)) 1fr; gap:calc(28 * var(--k)); align-items:center; min-width:0; }
.lcard__plate{ display:block; border-radius:calc(14 * var(--k)); overflow:hidden; background:#121210; aspect-ratio:236 / 300; }
.lcard__plate img{ width:100%; height:100%; object-fit:cover; object-position:50% 22%; display:block; }
.lcard__txt{ min-width:0; }
.lcard__tag{ margin:0 0 calc(10 * var(--k)); font-family:var(--mono); font-size:calc(11 * var(--k)); letter-spacing:.10em; color:var(--lilac); }
.lcard__name{ margin:0 0 calc(10 * var(--k)); font-family:var(--serif); font-weight:400; font-size:calc(34 * var(--k)); line-height:1.05; letter-spacing:-.02em; color:var(--ink); }
.lcard__role{ margin:0 0 calc(12 * var(--k)); font-family:var(--mono); font-size:calc(11 * var(--k)); letter-spacing:.08em; text-transform:uppercase; color:var(--muted); }
.lcard__bio{ margin:0; color:var(--body); font-size:calc(14.5 * var(--k)); line-height:1.6; }
.lcard__link{ margin:calc(14 * var(--k)) 0 0; }
.lcard__link a,.nlink{
  font-family:var(--mono); font-weight:500; font-size:calc(11 * var(--k));
  letter-spacing:.10em; text-transform:uppercase; color:var(--ink); transition:color .3s var(--ease-io);
}
.lcard__link a:hover{ color:var(--lilac); }
.lead__more{ margin:calc(46 * var(--k)) 0 0; display:flex; justify-content:center; }

.news__head{ display:flex; justify-content:space-between; align-items:flex-end; gap:calc(40 * var(--s)); }
.news__title{ display:flex; flex-direction:column; gap:calc(16 * var(--k)); }
.news__title .eyebrow{ justify-content:flex-start; }
.news__title .h2{ text-align:left; }
.news__all{ flex:none; }
.news__row{ display:grid; grid-template-columns:calc(560 * var(--s)) 1fr; gap:calc(40 * var(--s)); margin-top:calc(34 * var(--k)); align-items:start; }
.nfeat{ display:flex; flex-direction:column; color:inherit; }
.nfeat__img,.nitem__img{ display:block; overflow:hidden; background:#121210; }
.nfeat__img{ border-radius:calc(14 * var(--k)); aspect-ratio:16 / 9; margin-bottom:calc(18 * var(--k)); }
.nfeat__img img,.nitem__img img{ width:100%; height:100%; object-fit:cover; display:block; transition:transform 1.2s var(--ease); }
.nfeat:hover .nfeat__img img,.nitem:hover .nitem__img img{ transform:scale(1.03); }
.nmeta{ display:block; font-family:var(--mono); font-weight:500; font-size:calc(10.5 * var(--k)); letter-spacing:.10em; text-transform:uppercase; color:var(--lilac); }
.nfeat__h{ display:block; margin:calc(10 * var(--k)) 0 calc(12 * var(--k)); font-family:var(--serif); font-size:calc(30 * var(--k)); line-height:1.1; letter-spacing:-.02em; color:var(--ink); }
.nlist{ list-style:none; margin:0; padding:0; }
/* desktop/phone pairs in the news row — see the markup comment */
.nfeat--m,.nlist__li--m{ display:none; }
.nlist__li + .nlist__li{ border-top:1px solid var(--hairline); }
.nitem{ display:grid; grid-template-columns:calc(128 * var(--k)) 1fr; gap:calc(18 * var(--k)); align-items:center; padding:calc(14 * var(--k)) 0; color:inherit; }
.nlist__li:first-child .nitem{ padding-top:0; }
.nitem__img{ border-radius:calc(9 * var(--k)); aspect-ratio:16 / 10; }
.nitem__txt{ display:flex; flex-direction:column; gap:calc(7 * var(--k)); min-width:0; }
.nitem .nmeta{ font-size:calc(10 * var(--k)); }
.nitem__h{ font-family:var(--serif); font-size:calc(20 * var(--k)); line-height:1.12; letter-spacing:-.01em; color:var(--ink); }
.nitem .nlink{ font-size:calc(10 * var(--k)); color:var(--muted); }

/* ── 5 · FOUNDERS' PERSPECTIVE — quote cards (2026-09-28, Figma 974:35) ──
   Four tall cards: the post image (or a lilac quote panel for a text-only
   post), the founder's portrait sitting on the image edge, name · role,
   date, the post as a serif pull quote, "Read on LinkedIn ↗". */
.fp{ position:relative; background:var(--page); }
.fp__row{ display:grid; grid-template-columns:repeat(4, 1fr); gap:calc(24 * var(--s)); margin-top:calc(40 * var(--k)); }
.fcard{ position:relative; display:flex; flex-direction:column; min-width:0; padding:calc(10 * var(--k)) calc(10 * var(--k)) calc(22 * var(--k));
  border-radius:calc(18 * var(--k)); background:#121210; box-shadow:inset 0 0 0 1px var(--hairline); color:inherit;
  transition:box-shadow .45s var(--ease), background-color .45s var(--ease-io); }
.fcard__img{ display:block; height:calc(176 * var(--k)); border-radius:calc(12 * var(--k)); overflow:hidden; background:#1A1A18; }
.fcard__img img{ width:100%; height:100%; object-fit:cover; display:block; transition:transform 1.2s var(--ease); }
.fcard__img--q{ position:relative; background:linear-gradient(135deg, #43335C, #1F1A29); }
.fcard__qm{ position:absolute; left:calc(18 * var(--k)); top:calc(-14 * var(--k)); font-family:var(--serif); font-size:calc(150 * var(--k)); line-height:1; color:var(--lilac); }
.fcard__qt{ position:absolute; right:calc(18 * var(--k)); bottom:calc(18 * var(--k)); text-align:right; font-family:var(--mono); font-weight:500; font-size:calc(11 * var(--k)); line-height:1.5; letter-spacing:.16em; text-transform:uppercase; color:var(--lilac); }
.fcard__av{ position:absolute; left:calc(20 * var(--k)); top:calc(160 * var(--k)); width:calc(52 * var(--k)); height:calc(52 * var(--k)); border-radius:50%; overflow:hidden; box-shadow:0 0 0 calc(4 * var(--k)) #121210; background:#121210; }
.fcard__av img{ width:100%; height:100%; object-fit:cover; display:block; }
.fcard__who{ display:flex; flex-direction:column; gap:calc(4 * var(--k)); margin:calc(10 * var(--k)) 0 0 calc(74 * var(--k)); min-height:calc(34 * var(--k)); }
.fcard__n{ font-size:calc(13 * var(--k)); font-weight:500; color:var(--ink); }
.fcard__d{ font-family:var(--mono); font-weight:500; font-size:calc(9.5 * var(--k)); letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
.fcard__t{ margin:calc(22 * var(--k)) calc(10 * var(--k)) 0; font-family:var(--serif); font-size:calc(22 * var(--k)); line-height:1.18; letter-spacing:-.01em; color:var(--ink); min-height:calc(104 * var(--k)); }
.fcard__go{ margin:calc(18 * var(--k)) calc(10 * var(--k)) 0; font-family:var(--mono); font-weight:500; font-size:calc(10 * var(--k)); letter-spacing:.14em; text-transform:uppercase; color:var(--lilac); }
.fp__dots{ display:none; }
@media (hover:hover){
  .fcard:hover{ background:#161614; box-shadow:inset 0 0 0 1px rgba(214,194,240,.35); }
  .fcard:hover .fcard__img img{ transform:scale(1.04); }
}

/* ── 5c · FROM THE TEAM — the writing index (2026-09-26, Figma 952:2) ──
   One row per article: No. + topic │ title + dek │ author │ read time │ ↗.
   On hover the cover slides in ahead of the title and the title takes the
   accent. Rows are not links yet (sample content). */
.wr{ position:relative; background:var(--page); }
.wr__list{ list-style:none; margin:calc(40 * var(--k)) 0 0; padding:0; border-bottom:1px solid var(--hairline); }
.wrow{
  display:grid; grid-template-columns:calc(180 * var(--s)) 1fr calc(216 * var(--s)) calc(96 * var(--s)) calc(24 * var(--s));
  align-items:center; min-height:calc(112 * var(--k)); padding:calc(14 * var(--k)) 0;
  border-top:1px solid var(--hairline); cursor:default; outline:none;
}
.wrow__meta{ display:flex; flex-direction:column; align-items:flex-start; gap:calc(10 * var(--k)); }
.wrow__no{ font-family:var(--mono); font-weight:500; font-size:calc(11 * var(--k)); letter-spacing:.14em; color:var(--muted); }
.wrow__chip{ font-family:var(--mono); font-weight:500; font-size:calc(10 * var(--k)); letter-spacing:.14em; text-transform:uppercase; color:var(--lilac);
  padding:calc(5 * var(--k)) calc(10 * var(--k)); border:1px solid rgba(214,194,240,.45); border-radius:99px; line-height:1; }
.wrow__mm{ display:none; }
.wrow__main{ display:flex; align-items:center; min-width:0; }
.wrow__img{ flex:none; width:0; height:calc(88 * var(--k)); margin-right:0; border-radius:calc(10 * var(--k)); overflow:hidden; opacity:0;
  transition:width .55s var(--ease), margin-right .55s var(--ease), opacity .35s var(--ease-io); }
.wrow__img img{ width:calc(132 * var(--k)); height:100%; object-fit:cover; display:block; }
.wrow__txt{ display:flex; flex-direction:column; gap:calc(8 * var(--k)); min-width:0; padding-right:calc(32 * var(--s)); }
.wrow__t{ font-family:var(--serif); font-size:calc(30 * var(--k)); line-height:1.12; letter-spacing:-.015em; color:var(--ink); transition:color .35s var(--ease-io); }
.wrow__d{ font-size:calc(14 * var(--k)); line-height:1.5; color:var(--body); }
.wrow__by{ display:flex; align-items:center; gap:calc(12 * var(--k)); }
.wrow__av{ flex:none; display:grid; place-items:center; width:calc(34 * var(--k)); height:calc(34 * var(--k)); border-radius:50%;
  font-family:var(--serif); font-size:calc(17 * var(--k)); color:var(--muted); background:#232321; box-shadow:inset 0 0 0 1px #5A5A54; }
.wrow__av.is-team{ color:var(--lilac); background:#332B40; box-shadow:inset 0 0 0 1px var(--lilac); }
.wrow__who{ display:flex; flex-direction:column; gap:calc(5 * var(--k)); }
.wrow__n{ font-size:calc(14 * var(--k)); font-weight:500; color:var(--ink); }
.wrow__r,.wrow__m{ font-family:var(--mono); font-weight:500; font-size:calc(10 * var(--k)); letter-spacing:.12em; color:var(--muted); }
.wrow__r{ text-transform:none; letter-spacing:.06em; }
.wrow__go{ justify-self:end; font-size:calc(18 * var(--k)); color:var(--ink); transition:color .35s var(--ease-io), transform .45s var(--ease); }
.wr__all{ cursor:default; }
@media (hover:hover){
  .wrow:hover .wrow__img,.wrow:focus-visible .wrow__img{ width:calc(132 * var(--k)); margin-right:calc(24 * var(--k)); opacity:1; }
  .wrow:hover .wrow__t,.wrow:focus-visible .wrow__t{ color:var(--lilac); }
  .wrow:hover .wrow__go,.wrow:focus-visible .wrow__go{ color:var(--lilac); transform:translate(2px,-2px); }
}
.nfeat:hover .nlink,.nitem:hover .nlink{ color:var(--lilac); }
.nfeat:hover .nfeat__h,.nitem:hover .nitem__h{ color:#FFFFFF; }

/* ============================================================
   5 · CTA — first light
   ============================================================ */
/* THE THIRD... fourth pin. 240vh of track: the stage holds for 140vh while
   the section assembles, then scrolls away over the last 100vh with the CTA
   already composed — which for a CTA is not wasted travel, it is the beat
   where the button stays on screen while you decide. No -30vh hand-off
   here: the footer is next, not another pin. */
.cta{ position:relative; }
.ctrack{ position:relative; height:240vh; }
.ctrack__stick{
  position:sticky; top:0; height:100svh; overflow:hidden;
  background:var(--page);           /* the black beat has to BE black */
}
/* 🔑 THE TWO PLATES MUST SHARE ONE GEOMETRY. The still and the loop are the
   same 1440:860 composition, both `inset:-70s 0` and both `object-fit:cover`,
   so they cover-crop identically at every viewport and the loop lands exactly
   on the still it replaces. Any difference here shows as a jump the moment
   the video goes live. */
.cta__bloom,
.cta__bed{
  position:absolute; inset:calc(-70 * var(--s)) 0;
  will-change:transform, opacity;
}
/* <picture> is not a replaced element — object-fit on the wrapper does
   nothing and the inner img must be sized itself. Same trap as the
   carriage and the Why poster. */
.cta__bloom img{ width:100%; height:100%; object-fit:cover; display:block; }

/* ── THE MOVING BED ──────────────────────────────────────────
   The loop lies exactly on top of its own still. Its TOP IS MASKED AWAY, so
   only the flowers move: the sky, the horizon glow and the mist above the
   bloom line stay perfectly still, which is what stops the whole frame
   swimming. The mask edge lands in mist — the one texture that hides a
   feathered cut. */
.cta__bed{
  overflow:hidden;
  -webkit-mask-image:linear-gradient(to bottom, transparent 34%, #000 58%);
          mask-image:linear-gradient(to bottom, transparent 34%, #000 58%);
  /* the wind is written here as a custom property, never as `transform` on
     this node — two owners writing transform on one element fight, which is
     the trap the hero's bed already taught us */
  transform-origin:50% 100%;
}
.cta__loop{
  width:100%; height:100%; object-fit:cover; display:block;
  opacity:0; transition:opacity .6s var(--ease);
  transform:var(--cbedT, none);
  transform-origin:50% 100%;
}
/* only on `playing`, so a stalled video never shows a frozen first frame */
.cta__loop.is-live{ opacity:1; }
.cta__inner{
  position:absolute; inset:0; z-index:2;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  /* the block is lifted off centre so the button lands ABOVE the horizon
     glow rather than on top of it — the brightest part of the plate is the
     one thing the CTA should not sit in */
  gap:calc(28 * var(--s)); padding:0 calc(120 * var(--s)) calc(190 * var(--s));
}
.cta__inner .lede{ width:calc(520 * var(--s)); }
/* 🔴 This block has to live AFTER .cta__inner, not up with the other
   short-laptop rules: a media query adds no specificity, so the base
   rule's `padding` shorthand further down the file would simply win.
   On a short laptop the lift pushes the eyebrow up under the nav glass
   (72px against a nav that ends at 97) — give the height back. */
@media (min-width:1041px) and (max-height:790px){
  .cta__inner{ padding-bottom:calc(90 * var(--s)); }
}
/* the four beats start hidden and are driven by the pin, not by the
   generic reveal observer — see js/v2.js §8 */
.js [data-cstep]{ opacity:0; }

/* ============================================================
   FOOTER
   ============================================================ */
.foot{ position:relative; padding:calc(120 * var(--s)) calc(120 * var(--s)) 0; overflow:hidden; }
.foot__grid{ display:grid; grid-template-columns:calc(420 * var(--s)) 1fr 1fr 1fr; gap:calc(60 * var(--s)); }
.foot__blurb{ margin:calc(24 * var(--s)) 0 calc(28 * var(--s)); font-size:calc(14 * var(--s)); line-height:1.6; color:var(--body); max-width:calc(320 * var(--s)); }
.foot__social{ margin:0; display:flex; gap:calc(10 * var(--s)); }
.foot__social a{
  width:calc(30 * var(--s)); height:calc(30 * var(--s)); border-radius:50%;
  border:1px solid var(--hairline); display:grid; place-items:center;
  font-family:var(--mono); font-size:calc(10 * var(--s)); color:#C4B9D0;
  transition:border-color .3s var(--ease-io), color .3s var(--ease-io);
}
.foot__social a:hover{ border-color:var(--lilac); color:var(--lilac); }
.foot__col{ display:flex; flex-direction:column; gap:calc(12 * var(--s)); }
.foot__h{ margin:0 0 calc(10 * var(--s)); font-family:var(--mono); font-size:calc(10 * var(--s)); letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
.foot__col a{ font-size:calc(13 * var(--s)); transition:color .3s var(--ease-io); }
.foot__col a:hover{ color:var(--lilac); }
.foot__soon{ font-size:calc(13 * var(--s)); color:#5A5A54; }
.foot__legal{
  display:flex; justify-content:space-between; align-items:center;
  margin-top:calc(90 * var(--s)); padding-top:calc(26 * var(--s)); border-top:1px solid var(--hairline);
  font-family:var(--mono); font-size:calc(10 * var(--s)); letter-spacing:.12em; text-transform:uppercase; color:var(--muted);
}
.foot__legal p{ margin:0; }
.foot__legal-links{ display:flex; gap:calc(22 * var(--s)); }
.foot__legal-links a{ color:var(--muted); transition:color .3s var(--ease-io); }
.foot__legal-links a:hover{ color:var(--lilac); }
/* the supplied wordmark (Figma MVP 1003:5, black) re-inked to --wordmark
   #171715 — dark-on-dark, the same tone the serif wordmark had. Spans the
   content width, so its edges line up with the grid and the legal rule. */
.foot__wordmark{ margin:calc(72 * var(--s)) 0 0; padding-bottom:calc(56 * var(--s)); }
.foot__wordmark img{ display:block; width:100%; height:auto; }

/* ============================================================
   COMPACT (< 1040) — a graceful fallback, NOT the designed mobile.
   The 390 comp has not been signed off yet, so this unstacks the
   desktop composition into one legible column and drops the pin.
   ============================================================ */
@media (max-width:1039px){
  .nav{ padding:12px 14px; }
  .sound{ right:16px; bottom:16px; width:46px; height:46px; }
  .sound__bars{ height:14px; gap:3px; }
  .sound__bars i{ width:2.5px; }
  .sound__bars i:nth-child(1){ height:5px; }
  .sound__bars i:nth-child(2){ height:9px; }
  .sound__bars i:nth-child(3){ height:6px; }
  .sound__bars i:nth-child(4){ height:11px; }
  @keyframes sndbar{ 0%,100%{ height:4px; } 50%{ height:13px; } }
  .nav__pill{ height:54px; padding:0 8px 0 16px; border-radius:27px; gap:12px; }
  .nav__links{ display:none; }
  .brand__logo{ height:20px; }
  .nav .brand__logo{ height:17px; }
  .nav__cta{ height:38px; padding:0 16px; font-size:13px; margin-left:auto; }

  .hero{ height:auto; min-height:100svh; padding:104px 0 0; }
  /* --s has a 0.62px floor, so the desktop `width:calc(1440 * var(--s))`
     resolves to 893px — a fixed box 2.2x wider than the phone, which
     pushed the stage off the left edge and the type off the right. The
     fit box is a DESKTOP device: on mobile the hero flows. */
  .hero__inner{ width:100%; height:auto; }
  .hero__type{ position:relative; left:auto; top:auto; transform:none; width:100%; padding:0 22px; }
  .h1{ font-size:40px; }
  .sub,.hero__type .sub{ font-size:15px; width:auto; max-width:34em; margin-top:20px; }
  .hero__actions{ margin-top:30px; flex-wrap:wrap; justify-content:center; }
  .btn{ height:44px; padding:0 18px; font-size:13px; border-radius:22px; }
  /* the plate is 1070x1219, so at 100% width it is ~1.14x the viewport width
     tall — the stage has to be at least that or the arc gets cropped off */
  .hero__stage{ position:relative; height:calc(100vw * 1.16); max-height:56svh; margin-top:20px; }
  .hero__phones{ width:100%; bottom:6px; }
  .hero__blooms{ bottom:-18px; display:block; }
  .hero__glow{ left:10%; top:10%; width:80%; height:52%; }

  .vision{ padding-top:70px; }
  .vision__type{ padding:0 22px 60px; gap:18px; }
  /* NO PIN and NO STACK on mobile: the layers un-absolute and flow one
     after the other, statement then carriage. 4+ screens of sticky
     cross-dissolve is not a phone gesture. */
  .vstage{ height:auto; }
  .vstage__stick{ position:static; height:auto; }
  .vlayer{ position:static; }
  /* the flex budget only exists to fit a pinned viewport; with no pin the
     band goes back to its natural ratio and the columns stack */
  .vlayer--win{ gap:26px; padding:0; }
  .vwin__head{ gap:14px; padding:0 22px; }
  /* no pin, no height budget: show the whole carriage, uncropped */
  .vwin__frame{ min-height:0; max-height:none; aspect-ratio:1440 / 660; }
  .vwin__plate{ top:0; }
  .vwin__fade--t{ height:34px; }
  .vwin__fade--b{ height:70px; }
  .h2,.h2--wide{ font-size:30px; }
  .js .vwin__frame,
  .js .vwin__head .eyebrow,
  .js .vwin__head .h2,
  .js .vrail{ opacity:1; }
  .js [data-pstep]{ opacity:1; }
  .prow__bg{ inset:0; -webkit-mask-image:none; mask-image:none; }
  /* no pin, no pan, so no rail: the three simply stack. `position:static` on
     the caps is the whole fix — absolutely-positioned children of a track
     that no longer travels would pile up on each other. */
  .vrail__clip{ overflow:visible; }
  .vrail__track{ height:auto; display:grid; gap:26px; padding:4px 22px 60px; }
  .vrail__gate{ display:none; }
  .vcap{ position:static; width:auto; align-items:flex-start; text-align:left; }
  .vcap__b{ max-width:none; font-size:15px; }
  .vcap__l{ font-size:12px; }
  .vcap__n{ font-size:10px; }

  /* NO PIN on mobile: the water becomes a tall background behind a normal
     column, the hands sit closed in the middle of it, and the type flows.
     A 4.6-screen sticky choreography is not a phone gesture. */
  .wtrack{ height:auto; }
  .wtrack__stick{ position:static; height:auto; overflow:hidden; }
  .wtrack__stick{ display:flex; flex-direction:column; }
  .why__bg{ position:absolute; inset:0; }
  .whands{ position:relative; order:2; width:100%; aspect-ratio:1440 / 652; }
  .whand{ top:0; }
  .whand__glow{ display:none; }
  .why__thesis,.truths{ position:relative; top:auto; margin-top:0; }
  .why__thesis{ order:1; padding:70px 22px 24px; gap:18px; }
  .why__thesis .lede{ width:auto; }
  .lede{ font-size:15px; }
  .truths{ order:3; grid-template-columns:1fr; gap:34px; padding:24px 22px 90px; }
  .truth__t{ font-size:17px; }
  .truth__b{ font-size:14px; }

  /* 🔴 -30vh exists ONLY to eat a sticky track's dead release viewport. With
     no pins on mobile there is nothing to release, so the margins just pull
     Products up INTO Why and collide the two sections' type. */
  .why,.products{ margin-top:0; }
  /* no pin on a phone: the track collapses and the stage flows, header
     first and the band under it — the same order the timeline plays. */
  .ptrack{ height:auto; }
  .ptrack__stick{ position:static; height:auto; overflow:visible; display:flex; flex-direction:column; }
  .products__header{ position:static; order:1; padding:80px 22px 44px; gap:18px; }
  .ptrack__stick .prow{ height:auto; order:2; }
  /* 🔴 THE PIN'S DEVICE RULES ARE 2-CLASS AND BEAT THE 1-CLASS COMPACT ONES.
     `.ptrack__stick .phone{width:380*--s}` survived into mobile as a 364px
     device on a 402px screen — nine tenths of the width — and the copy,
     which un-absolutes here, landed straight on top of it. Every desktop
     pin value has to be answered, not just `top`. */
  .ptrack__stick .phone{ top:8%; width:47%; }
  .ptrack__stick .prow__copy{ padding-top:30px; }
  .js [data-phead]{ opacity:1; }
  .pstealth{ font-size:12px; margin-top:22px; }
  .prow{ height:auto; padding:0 0 44px; }
  .prow__bg{ position:relative; }
  .prow__scrim{ display:none; }
  /* 🔴 left:26.5% = 50% - 47%/2. NOT `left:50% + translateX(-50%)`: the
     phone channel writes `transform` on this node every frame, so a
     transform-based centring is wiped the moment the row scrolls into view
     and the device slides half its own width off to the right. Same rule
     that bit .hero__type and .prow__copy. */
  .prow__bg{ height:46svh; }
  .phone{ position:absolute; top:8%; width:47%; left:26.5% !important; right:auto; transform:rotate(-9deg); }
  .prow--flip .phone{ left:26.5% !important; transform:rotate(9deg); }
  .prow__copy{ position:relative; top:auto; transform:none; width:auto; left:auto !important; right:auto !important; padding:30px 22px 0; }
  .pname{ font-size:28px; }
  .pname__logo{ height:38px; }
  .pdesc{ font-size:15px; }

  /* no pin on a phone: the track collapses and the stage flows, exactly
     the stacked order it had before — type first, dawn under it */
  .ctrack{ height:auto; }
  .ctrack__stick{ position:static; height:auto; overflow:visible; }
  .cta{ padding:90px 0 0; }
  .ctrack__stick{ display:flex; flex-direction:column; }
  .cta__inner{ position:static; order:1; padding:0 22px; gap:18px; }
  .cta__bloom{ order:2; position:relative; inset:auto; margin-top:40px; }
  .cta__bloom img{ height:auto; }
  /* no pin, no wind, no loop on a phone: the still carries the section */
  .cta__bed{ display:none; }
  .js [data-cstep]{ opacity:1; }
  .products::after{ height:150px; }

  .foot{ padding:70px 22px 0; }
  .foot__grid{ grid-template-columns:1fr 1fr; gap:34px; }
  .foot__brand{ grid-column:1 / -1; }
  .foot__legal{ flex-direction:column; gap:14px; align-items:flex-start; margin-top:50px; }
  .foot__wordmark{ margin:44px 0 0; padding-bottom:32px; }
}

/* ============================================================
   REDUCED MOTION — kill the choreography, keep the page.
   Everything lands in its final state; nothing is hidden.
   ============================================================ */
@media (prefers-reduced-motion:reduce){
  html{ scroll-behavior:auto; }
  .js .hero__glow,.js .hero__phones,.js .hero__blooms,
  .js .hero__type > *,.js .h1__line,.js [data-reveal],.js [data-step]{
    opacity:1 !important; transform:none !important; transition:none !important; filter:none !important;
  }
  /* no pin, no stack, no dissolve — a plain black statement */
  .vstage{ height:auto !important; }
  .seqtrack{ height:auto !important; }
  .seqtrack__stick{ position:relative !important; height:auto !important; padding:calc(40 * var(--s)) 0 calc(80 * var(--s)); }
  .js [data-seq]{ opacity:1 !important; }
  /* the stories: no pin, no burst — the composed heart, tappable */
  .strack{ height:auto !important; }
  .strack__stick{ position:relative !important; }
  .vstage__stick{ position:static !important; height:auto !important; }
  .vlayer{ position:static !important; }
  .vlayer--type{ padding:calc(90 * var(--s)) 0; }
  .js [data-pstep]{ opacity:1 !important; }
  .prow__bg{ -webkit-mask-image:none !important; mask-image:none !important; }
  /* Products: no pin either — the header sits above a still band. Without
     the timeline nothing would ever raise these from opacity 0. */
  .ptrack{ height:auto !important; }
  .ptrack__stick{ position:static !important; height:auto !important; overflow:visible !important; display:flex !important; flex-direction:column; }
  .products__header{ position:static !important; order:1; padding:calc(110 * var(--s)) calc(120 * var(--s)) calc(70 * var(--s)); }
  .ptrack__stick .prow{ height:calc(760 * var(--s)) !important; order:2; }
  .ptrack__stick .phone{ top:calc(53.71 * var(--s)) !important; }
  .ptrack__stick .prow__copy{ padding-top:calc(91 * var(--s)) !important; }
  .js [data-phead]{ opacity:1 !important; transform:none !important; filter:none !important; }
  .js [data-pstep]{ transform:none !important; filter:none !important; }
  .prow__bg{ transform:none !important; filter:none !important; }
  .js .vlayer--win,
  .js .vwin__frame,
  .js .vwin__head .eyebrow,
  .js .vwin__head .h2,
  .js .vrail{ opacity:1 !important; }
  /* 🔴 THE RAIL HAS TO FLATTEN HERE TOO, and not only on mobile. Its three
     caps are absolutely positioned at 32.6% / 76% / 119% and only the
     scroll-driven translate brings 02 and 03 into view — with the script
     early-returned, `.vrail__clip{overflow:hidden}` would simply eat two
     thirds of the section's copy. Without motion the mechanism does not
     exist, so it falls back to what it replaced: three columns, side by
     side, all lit. */
  .vrail__clip{ overflow:visible !important; }
  .vrail__track{
    height:auto !important; transform:none !important;
    display:grid; grid-template-columns:repeat(3, 1fr);
    gap:0 calc(60 * var(--s)); width:calc(1200 * var(--s)); margin:0 auto;
  }
  .vrail__gate{ display:none; }
  .vcap{
    position:static !important; width:auto; opacity:1 !important;
    align-items:flex-start; text-align:left;
  }
  .vcap__r{ background:var(--lilac); }
  .vcap__n{ color:var(--lilac); }
  .vcap__l{ color:var(--ink); }
  .vcap__b{ color:var(--body); }
  /* Why: no pin either — one still body of water behind flowing type, the
     hands already closed. Nothing is hidden and nothing moves. */
  .wtrack{ height:auto !important; }
  .wtrack__stick{ position:static !important; height:auto !important; }
  .why__bg{ position:absolute !important; inset:0; opacity:1 !important; transform:none !important; }
  .why__loop{ display:none; }
  .wtrack__stick{ display:flex !important; flex-direction:column; }
  .whands{ position:relative !important; order:2; width:100%; aspect-ratio:1440 / 652; }
  .whand{ top:0 !important; transform:none !important; opacity:1 !important; }
  .whand__glow{ display:none; }
  .js .why__thesis > *,.js .truths{ opacity:1 !important; transform:none !important; filter:none !important; }
  .why__thesis,.truths{ position:relative !important; top:auto !important; margin-top:0 !important; }
  /* CTA: no pin either — the dawn simply sits behind a centred block */
  /* the nav's auto-hide is FUNCTIONAL — it reclaims the page — so reduced
     motion keeps it and drops only the slide. Same for the sound button:
     the bars stop dancing but still stand up to show state. */
  .nav{ transition:none !important; }
  .sound.is-on .sound__bars i{ animation:none !important; height:calc(13 * var(--s)); }
  /* the nudge is pure decoration — unlike the nav's auto-hide, nothing is
     lost by removing it */
  .sound:not(.is-on) .sound__bars i{ animation:none !important; transform:none !important; }
  .sound::after{ display:none !important; }
  .ctrack{ height:auto !important; }
  .ctrack__stick{ position:static !important; height:calc(860 * var(--s)); }
  .js [data-cstep]{ opacity:1 !important; transform:none !important; filter:none !important; }
  .cta__bloom{ transform:none !important; opacity:1 !important; }
  .cta__bed{ display:none !important; }
  .why__thesis{ order:1; padding:calc(120 * var(--s)) calc(120 * var(--s)) calc(30 * var(--s)); }
  .truths{ order:3; padding:calc(30 * var(--s)) calc(120 * var(--s)) calc(120 * var(--s)); }
  .js .hero__phones,.js .hero__blooms img{ filter:none !important; transform:none !important; }
  .js .hero__phones{ transform:translateX(-50%) !important; }
  .hero__glow{ animation:none; }
  .hero__loop{ display:none; }
  .hero__air{ display:none; }
  .btn{ transition:none; }
}
