/* Jo — waitlist site
   Ported from the "Jo Waitlist Light" design. Values are 1:1 with the source. */

/* ── Fonts (self-hosted) ─────────────────────────────────────────────────
   Served from assets/fonts/ rather than Google Fonts and Fontshare, so the
   page has no third-party runtime dependency: nothing to go down, nothing to
   rate-limit, no request telling another host who is reading this page.
   See assets/fonts/README.md for sources and licences. */

/* Geist and Geist Mono are variable fonts — one file covers the whole weight
   axis, which is why the CDN served the same URL for 400, 500 and 600. */
@font-face {
  font-family: 'Geist';
  src: url('assets/fonts/geist-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Geist Mono';
  src: url('assets/fonts/geist-mono-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* Clash Display ships as static instances, so one file per weight. Only 500 is
   used today; 400 and 600 are here so a future weight change doesn't silently
   fall back to a synthesised one. */
@font-face {
  font-family: 'Clash Display';
  src: url('assets/fonts/clash-display-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Clash Display';
  src: url('assets/fonts/clash-display-500.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Clash Display';
  src: url('assets/fonts/clash-display-600.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

:root {
  --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
  --font-mono: 'Geist Mono', ui-monospace, Menlo, monospace;
  /* Wide, rounded geometric display face — display sizes only, never body.
     Modern luxury rather than the classic-luxury serif this replaced: the
     reference points (Open and similar) all pair a characterful geometric
     display with a neutral sans underneath, which is what this does with
     Geist. Loaded from Fontshare, free for commercial use. */
  --font-display: 'Clash Display', 'Geist', ui-sans-serif, system-ui, sans-serif;

  /* Warm bone and soft ink rather than #fff/#000. Pure black on pure white is
     the most common palette on the web and reads utilitarian; a warm ground
     with an ink that never quite reaches black is what print luxury does. */
  --background: oklch(0.983 0.005 84);
  --foreground: oklch(0.20 0.011 58);
  --muted-foreground: oklch(0.53 0.012 66);
  --input: oklch(0.885 0.008 84);

  --surface: oklch(0.966 0.007 84);
  --surface-strong: oklch(0.945 0.009 84);

  /* Muted brass, used only on the mono eyebrows and a few rules. Any more
     than a trace of it and it stops reading as restraint. Lightness is pinned
     at 0.53 because the eyebrows are 11px: a prettier 0.60 measures 3.8:1 on
     the bone ground and fails WCAG AA, where this measures 5.11:1. */
  --accent: oklch(0.53 0.075 72);

  /* Derived from the ink, so hairlines stay warm instead of dropping to grey. */
  --hair-10: color-mix(in oklch, var(--foreground) 10%, transparent);
  --hair-12: color-mix(in oklch, var(--foreground) 13%, transparent);
  --hair-14: color-mix(in oklch, var(--foreground) 16%, transparent);
  --hair-16: color-mix(in oklch, var(--foreground) 19%, transparent);
  --hair-18: color-mix(in oklch, var(--foreground) 22%, transparent);

  --radius-md: 6px;
  --radius-xl: 12px;
}

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

html { scroll-behavior: smooth; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--background);
  color: var(--foreground);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
}

/* Film grain over the whole page. The bone ground is perfectly flat, and flat
   large areas read as unfinished rather than restrained — print luxury always
   has tooth. This is a single inline SVG (no request) at very low opacity,
   above everything but inert to the pointer. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 200;
  pointer-events: none;
  opacity: 0.042;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 140px 140px;
}

a { color: var(--foreground); text-decoration: none; }
a:hover { color: var(--muted-foreground); }

:focus-visible {
  outline: 2px solid var(--muted-foreground);
  outline-offset: 2px;
  border-radius: 4px;
}

@keyframes joLineIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* ── Shared type ─────────────────────────────────────────────────────── */

.eyebrow,
.mono-label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted-foreground);
}
.mono-label { letter-spacing: 0.18em; white-space: nowrap; }

/* The section markers are the one place the brass appears. */
.eyebrow { color: var(--accent); }

.mono-num {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--muted-foreground);
}

.lede {
  margin: 0;
  max-width: 56ch;
  font-size: 17px;
  line-height: 1.6;
  color: var(--muted-foreground);
  text-wrap: pretty;
}

.strong { color: var(--foreground); }
.rule { height: 1px; background: var(--hair-10); }
.self-start { align-self: start; }

/* ── Buttons ─────────────────────────────────────────────────────────── */

.btn {
  appearance: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  font-family: var(--font-sans);
  font-weight: 500;
  white-space: nowrap;
}

.btn-primary {
  height: 44px;
  padding: 0 20px;
  border: 0;
  background: var(--foreground);
  color: var(--background);
  font-size: 15px;
  transition: opacity 0.15s ease;
}
.btn-primary:hover { opacity: 0.9; }

.btn-ghost {
  height: 40px;
  padding: 0 18px;
  border: 1px solid var(--hair-16);
  background: transparent;
  color: var(--foreground);
  font-size: 14px;
  transition: background 0.15s ease;
}
.btn-ghost:hover { background: var(--surface-strong); }

.btn-tall { height: 44px; font-size: 15px; border-color: var(--hair-18); }

.linkish {
  appearance: none;
  cursor: pointer;
  background: transparent;
  border: 0;
  padding: 0;
  color: var(--muted-foreground);
  font-family: var(--font-sans);
  font-size: 14px;
}
.linkish.underline { text-decoration: underline; text-underline-offset: 4px; }

/* ── Header ──────────────────────────────────────────────────────────── */

.site-header {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  height: 64px;
  padding: 0 clamp(20px, 4vw, 48px);
  background: var(--background);
  border-bottom: 1px solid var(--hair-10);
}

.brand { display: flex; align-items: center; gap: 11px; }

/* ── The mark ────────────────────────────────────────────────────────────
   Jo's own face. The blob in the app is a chrome sphere with a hard horizon
   — bright silver above, gunmetal below — so the mark is that object, not a
   logotype. Built from gradients rather than an image: it stays crisp at any
   density, costs no request, and re-colours with the palette.
   The horizon is a 1% gradient stop, which is what makes it read as a
   reflective surface rather than a shaded ball. */
.mark {
  position: relative;
  flex: 0 0 auto;
  width: 21px;
  height: 21px;
  border-radius: 50%;
  background:
    radial-gradient(ellipse 58% 42% at 40% 24%,
      oklch(1 0 0) 0%,
      oklch(1 0 0 / 0.72) 30%,
      transparent 62%),
    linear-gradient(to bottom,
      oklch(0.96 0.004 84) 0%,
      oklch(0.87 0.006 84) 45%,
      oklch(0.40 0.008 70) 46%,
      oklch(0.55 0.008 70) 74%,
      oklch(0.74 0.006 80) 100%);
  box-shadow:
    inset 0 0 0 0.5px color-mix(in oklch, var(--foreground) 42%, transparent),
    inset 0 -2px 3px -2px oklch(1 0 0 / 0.5),
    0 1px 3px color-mix(in oklch, var(--foreground) 20%, transparent);
}

/* The soft halo the app gives it. */
.mark::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in oklch, var(--accent) 22%, transparent) 0%, transparent 70%);
  z-index: -1;
}

/* Alive, but only just — the same slow breath the app's blob has. */
.js-motion .mark { animation: markBreath 4.4s ease-in-out infinite; }
@keyframes markBreath {
  0%, 100% { transform: scale(1) translateY(0); }
  50%      { transform: scale(1.05) translateY(-0.5px); }
}

.overlay-bar .brand-name { font-size: 19px; letter-spacing: 0.14em; }
/* Open-style wordmark: modest size, wide tracking. The tracking is the brand
   device — at normal spacing this just reads as a heading. */
.brand-name {
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 500;
  letter-spacing: 0.14em;
}

/* ── Sound toggle ────────────────────────────────────────────────────────
   Sits between the wordmark and the nav. Deliberately quiet: four hairline
   bars and a word. It animates only while actually playing, so it doubles as
   the indicator that something is speaking. */
.sound-toggle {
  appearance: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin-right: auto;
  margin-left: clamp(16px, 3vw, 34px);
  padding: 7px 13px 7px 11px;
  border: 1px solid var(--hair-14);
  border-radius: 999px;
  background: transparent;
  color: var(--muted-foreground);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, transform 0.15s ease;
}
.sound-toggle:hover { color: var(--foreground); border-color: var(--hair-18); }
.sound-toggle:active { transform: scale(0.97); }
.sound-toggle[aria-pressed="true"] { color: var(--foreground); border-color: var(--accent); }

.sound-bars {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 11px;
}
.sound-bars i {
  display: block;
  width: 2px;
  height: 3px;
  background: currentColor;
  border-radius: 1px;
  transform-origin: center;
}
/* Bars only move while audio is actually playing. */
.sound-toggle[aria-pressed="true"] .sound-bars i {
  animation: soundBar 1s ease-in-out infinite;
}
.sound-toggle[aria-pressed="true"] .sound-bars i:nth-child(1) { animation-delay: 0s; }
.sound-toggle[aria-pressed="true"] .sound-bars i:nth-child(2) { animation-delay: 0.18s; }
.sound-toggle[aria-pressed="true"] .sound-bars i:nth-child(3) { animation-delay: 0.36s; }
.sound-toggle[aria-pressed="true"] .sound-bars i:nth-child(4) { animation-delay: 0.12s; }

@keyframes soundBar {
  0%, 100% { transform: scaleY(1); }
  50%      { transform: scaleY(3.4); }
}

/* Three things now compete for a 64px bar on a phone. Nothing may wrap: the
   wordmark tightens, the toggle drops to bars only, and the CTA holds one
   line. */
@media (max-width: 560px) {
  .sound-toggle { padding: 6px 9px; margin-left: 10px; gap: 0; }
  .sound-label { display: none; }
  .site-header { gap: 8px; padding: 0 16px; }
  .brand { flex: 0 0 auto; gap: 8px; }
  .brand-name { font-size: 15px; letter-spacing: 0.09em; white-space: nowrap; }
  .mark { width: 17px; height: 17px; }
  .site-nav { flex: 0 0 auto; }
  .site-nav .nav-cta { font-size: 13px; white-space: nowrap; }
}

.site-nav {
  display: flex;
  align-items: center;
  gap: clamp(16px, 3vw, 32px);
  font-size: 14px;
}
.site-nav a { color: var(--muted-foreground); }
.site-nav a:hover { color: var(--foreground); }
.site-nav .nav-cta { font-weight: 500; color: var(--foreground); }

/* ── Hero ────────────────────────────────────────────────────────────── */

.hero {
  display: flex;
  flex-wrap: wrap-reverse;
  align-items: stretch;
  border-bottom: 1px solid var(--hair-10);
}

.hero-copy {
  flex: 1 1 420px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(20px, 2.6vh, 32px);
  padding: clamp(40px, 6vh, 72px) clamp(24px, 4vw, 56px) clamp(40px, 6vh, 72px) clamp(20px, 4vw, 48px);
  min-height: clamp(420px, calc(100svh - 64px), 900px);
}

/* Clash Display runs wider than the serif it replaced, so the ceiling comes
   down to keep the two-line headline off the form below it. */
.hero-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(34px, 4.1vw, 70px);
  line-height: 1.04;
  letter-spacing: -0.028em;
  font-weight: 500;
  text-wrap: balance;
}

.hero-sub {
  margin: 0;
  max-width: 46ch;
  font-size: clamp(16px, 1.25vw, 19px);
  line-height: 1.55;
  color: var(--muted-foreground);
  text-wrap: pretty;
}

.hero-media {
  flex: 1 1 400px;
  position: relative;
  min-height: clamp(300px, 52vh, 780px);
  background: var(--surface-strong);
  /* On a narrow crop the earbud can sit near the edge; keep its waveform
     inside the photo rather than spilling onto the copy. */
  overflow: hidden;
}

/* Sound coming out of the earbud. Positioned from JS against the photo's
   cover-fit rect, so it tracks the AirPod as the hero resizes. Starts
   invisible so it can't flash at 0,0 before the image reports its size. */
.ear-wave {
  position: absolute;
  display: block;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.6s ease;
}

.hero-fade {
  position: absolute;
  inset: auto 0 0 0;
  height: 55%;
  background: linear-gradient(
    to top,
    var(--background) 2%,
    color-mix(in oklch, var(--background) 60%, transparent) 40%,
    transparent 100%
  );
  pointer-events: none;
}

.hero-wave {
  position: absolute;
  left: clamp(20px, 3vw, 36px);
  right: clamp(20px, 3vw, 36px);
  bottom: clamp(20px, 3vw, 34px);
  display: flex;
  align-items: center;
  gap: 16px;
  pointer-events: none;
}
.hero-wave canvas { flex: 1 1 auto; height: 34px; display: block; }

/* ── Image slots ─────────────────────────────────────────────────────── */

.slot {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--surface-strong);
}
/* Hidden with opacity, NOT display:none. A display:none image never loads
   under loading="lazy" — lazy loading works off intersection with the
   viewport, and an element with no box never intersects, so the fetch is
   deferred forever and the placeholder stays up permanently. */
.slot img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.5s ease;
}
.slot.is-loaded img { opacity: 1; }
.slot.is-loaded .slot-placeholder { display: none; }

/* Focal points. Both photos put the face high in the frame, so a centred cover
   crop beheads them the moment the box gets shorter than the image — which is
   exactly what happens on a phone. Biasing the crop upward keeps the face and
   the earbud in shot. placeEarWave() reads these values back, so the waveforms
   follow the crop instead of being left behind. */
.hero-media [data-slot-img] { object-position: 50% 30%; }
.who-figure [data-slot-img] { object-position: 50% 18%; }

/* The stacked layout crops hardest of all — the figure goes wide and short
   against a tall portrait, so the focal point has to ride higher still. */
@media (max-width: 860px) {
  .who-figure [data-slot-img] { object-position: 50% 12%; }
}

/* On a phone the hero box is barely taller than the photo, so there is only
   ~46px of crop slack to play with — and the parallax scale eats into it from
   both edges. Show more of the top so she sits lower in frame with her head
   clear of the edge. */
@media (max-width: 700px) {
  .hero-media [data-slot-img] { object-position: 50% 8%; }
}

/* Portrait media inside a landscape frame — sit it in the middle at full
   height rather than cropping the sides off. */
.slot-contain img { object-fit: contain; }

.slot-placeholder {
  max-width: 32ch;
  padding: 24px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.6;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted-foreground);
}

/* ── Sections ────────────────────────────────────────────────────────── */

.section {
  padding: clamp(80px, 11vh, 152px) clamp(24px, 5vw, 64px);
  border-bottom: 1px solid var(--hair-10);
}

/* The header is sticky and 64px tall, so an un-offset anchor jump parks the
   section heading underneath it. */
[id] { scroll-margin-top: 88px; }

.wrap { max-width: 1240px; margin: 0 auto; display: flex; flex-direction: column; }
.stack-sm { display: flex; flex-direction: column; gap: 14px; }
.stack-md { gap: clamp(28px, 4vh, 44px); }
.stack-lg { gap: clamp(36px, 5vh, 60px); }

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  justify-content: space-between;
  gap: 24px;
}

.section-head-copy {
  max-width: 720px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.section h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(32px, 3.7vw, 56px);
  line-height: 1.08;
  letter-spacing: -0.022em;
  font-weight: 500;
  text-wrap: balance;
}

/* ── Capture form ────────────────────────────────────────────────────── */

.capture { display: flex; flex-direction: column; gap: 10px; max-width: 520px; }

.capture-form { display: flex; flex-wrap: wrap; gap: 8px; }
.capture-form-center { justify-content: center; width: 100%; max-width: 520px; }
.capture-field { flex: 1 1 240px; min-width: 0; }

.capture-form input[type="email"],
.overlay textarea {
  display: block;
  width: 100%;
  font-family: var(--font-sans);
  font-size: 15px;
  color: var(--foreground);
  background: var(--background);
  border: 1px solid var(--input);
  border-radius: var(--radius-md);
  outline: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.capture-form input[type="email"] { height: 44px; padding: 0 14px; }
.overlay textarea { min-height: 96px; padding: 12px 14px; line-height: 1.5; resize: vertical; }

.capture-form input[type="email"]:focus,
.overlay textarea:focus {
  border-color: var(--muted-foreground);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent) 28%, transparent);
}

.capture-note, .hint {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted-foreground);
}

.form-error { margin: 0; font-size: 13px; line-height: 1.5; color: oklch(0.45 0.19 27); }

/* ── 01 Interaction ──────────────────────────────────────────────────── */

.interaction-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 24px;
  align-items: start;
}

/* A whisper of elevation. Everything else on the page is borders and fills;
   lifting the cards a fraction off the ground is what stops a flat palette
   reading as a wireframe. */
.card {
  border: 1px solid var(--hair-12);
  border-radius: var(--radius-xl);
  background: var(--surface);
  overflow: hidden;
  box-shadow:
    0 1px 2px color-mix(in oklch, var(--foreground) 3%, transparent),
    0 18px 48px -22px color-mix(in oklch, var(--foreground) 12%, transparent);
}

.who-figure {
  box-shadow:
    0 1px 2px color-mix(in oklch, var(--foreground) 4%, transparent),
    0 26px 64px -26px color-mix(in oklch, var(--foreground) 18%, transparent);
}
.card-pad { padding: clamp(20px, 2.4vw, 32px); }

.card-head {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--hair-10);
}
.card-head canvas { flex: 1 1 auto; height: 22px; display: block; }

/* Fixed height, not min-height. The transcript types itself in line by line,
   and a growing box would shove the rest of the page down while you're
   scrolling past it — the single worst source of scroll jank on this page.
   It scrolls internally instead, pinned to the newest line. */
.transcript {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: clamp(18px, 2vw, 26px);
  height: 340px;
  overflow-y: auto;
  scrollbar-width: none;
  overscroll-behavior: contain;
}
.transcript::-webkit-scrollbar { display: none; }

.line {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr);
  gap: 12px;
  align-items: baseline;
  animation: joLineIn 0.4s ease both;
}
.line-who {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  color: var(--muted-foreground);
  padding-top: 4px;
}
.line-text {
  margin: 0;
  font-size: clamp(15px, 1.3vw, 18px);
  line-height: 1.42;
  letter-spacing: -0.015em;
  color: var(--foreground);
  text-wrap: pretty;
}

.stats { display: flex; flex-direction: column; gap: 24px; }
.stat { display: flex; flex-direction: column; gap: 6px; }
.stat-value {
  font-size: clamp(72px, 9vw, 128px);
  line-height: 0.86;
  letter-spacing: -0.05em;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Three action rows land one at a time; reserve their space up front so the
   card doesn't grow underneath the reader. */
.acts { display: flex; flex-direction: column; gap: 12px; min-height: 84px; }
.act {
  display: flex;
  align-items: baseline;
  gap: 12px;
  font-size: 14px;
  animation: joLineIn 0.4s ease both;
}
.act-time { font-family: var(--font-mono); font-size: 11px; color: var(--muted-foreground); }
.act-text { color: var(--foreground); }

/* ── 02 Demo video ───────────────────────────────────────────────────── */

/* The footage is portrait inside a landscape frame, so most of this box is
   empty surface. Rather than leave it flat, light it: a soft pool behind
   where the phone sits, so the video reads as an object photographed on a
   surface instead of a letterboxed video. */
.video {
  position: relative;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--hair-12);
  border-radius: var(--radius-xl);
  overflow: hidden;
  background:
    radial-gradient(
      ellipse 34% 78% at 50% 52%,
      color-mix(in oklch, var(--foreground) 13%, transparent) 0%,
      color-mix(in oklch, var(--foreground) 5%, transparent) 55%,
      transparent 78%
    ),
    var(--surface);
  box-shadow:
    0 1px 2px color-mix(in oklch, var(--foreground) 4%, transparent),
    0 24px 60px -24px color-mix(in oklch, var(--foreground) 14%, transparent);
}

/* Chrome only in the margins. The mask is transparent through the middle, so
   the metal never sits behind the footage — it fills the dead surface either
   side and stops there. Deliberately much fainter than the closing plate:
   this is a hint that the finale pays off, not a second finale. */
.video-plate {
  position: absolute;
  inset: 0;
  background: url('assets/chrome.jpg') center / cover no-repeat;
  filter: saturate(0.18) sepia(0.28) brightness(0.99) contrast(1.08);
  opacity: 0.8;
  -webkit-mask-image: radial-gradient(ellipse 34% 78% at 50% 50%, transparent 0%, transparent 38%, #000 88%);
  mask-image: radial-gradient(ellipse 34% 78% at 50% 50%, transparent 0%, transparent 38%, #000 88%);
  pointer-events: none;
}

.video-stage { position: absolute; inset: 0; z-index: 1; }
.video-stage video,
.video-stage iframe {
  width: 100%;
  height: 100%;
  display: block;
  border: 0;
  object-fit: contain;
}

/* Portrait footage letterboxes onto the frame's own surface, so playing the
   video doesn't flip the surround from light to black mid-click. */
.video-stage video { background: transparent; }
.video-stage iframe { background: #000; }

.video-cover {
  position: absolute;
  inset: 0;
  appearance: none;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  font-family: inherit;
  color: var(--foreground);
  transition: opacity 0.3s ease;
}
.video.is-playing .video-cover { opacity: 0; pointer-events: none; }

.video-pill {
  position: relative;
  z-index: 1;
  margin: clamp(16px, 2.4vw, 28px);
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 22px 14px 18px;
  border-radius: 999px;
  border: 1px solid var(--hair-14);
  background: color-mix(in oklch, var(--background) 94%, transparent);
  backdrop-filter: blur(6px);
  transition: background 0.25s ease;
}
.video-cover:hover .video-pill { background: var(--background); }
.video-pill-label { font-size: 14px; font-weight: 500; }
.video-pill-time { font-family: var(--font-mono); font-size: 12px; color: var(--muted-foreground); }

/* ── Hairline tile grid (post-signup overlay) ────────────────────────── */

.grid-hairline {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1px;
  background: var(--hair-12);
  border: 1px solid var(--hair-12);
  border-radius: var(--radius-xl);
  overflow: hidden;
}
.grid-hairline-sm { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }

.tile {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: clamp(24px, 3vw, 40px);
  background: var(--background);
}
.tile-tight { gap: 6px; padding: 24px; background: var(--surface); }
.tile-value {
  font-size: 44px;
  line-height: 1;
  letter-spacing: -0.04em;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* ── 04 Who it's for ─────────────────────────────────────────────────── */

/* Portrait photograph beside the copy. The section is deliberately sparse
   since the prose came out, so the image carries the weight the words used to. */
.who-layout {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(32px, 5vw, 72px);
  align-items: center;
}

.who-figure {
  margin: 0;
  position: relative;
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--surface-strong);
}
/* .slot is absolute; the figure's aspect-ratio gives it something to fill. */

.who-copy {
  display: flex;
  flex-direction: column;
  gap: clamp(32px, 4.5vh, 52px);
}

/* Beside the photo the column is too narrow for four across — auto-fit lands
   on an unbalanced 3 + 1, so pin it to a 2x2. */
.who-copy .moments {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(20px, 2.5vw, 36px);
}

@media (max-width: 860px) {
  .who-layout { grid-template-columns: minmax(0, 1fr); }
  .who-figure { aspect-ratio: 3 / 2; }
}

/* Four moments, reduced to a timestamp and a name. The prose that used to sit
   under each one said what the heading already implies. */
.moments {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: clamp(16px, 2.5vw, 32px);
}
.moment {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: 16px;
}
/* The rule is a pseudo-element rather than a border so it can draw itself in
   left-to-right as the section arrives. */
.moment::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--hair-14);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.moment h3 { margin: 0; font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }

/* ── Closing ─────────────────────────────────────────────────────────── */

.closing {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: clamp(96px, 14vh, 180px) clamp(20px, 4vw, 48px);
  border-bottom: 0;
}

/* ── Liquid chrome finale ────────────────────────────────────────────────
   The page ends on a moving metal panel. The plate is inset beyond the
   section on every side so it can drift without ever exposing an edge, and
   the animation is transform-only so it runs on the compositor rather than
   forcing paint on a section the reader is actively scrolling into. */
/* Explicit z-index rather than -1: a negative child sits behind its parent's
   own background box, which is fragile the moment .closing gains one. */
.closing-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  /* Feather the plate into the page at both ends. Without this the metal
     starts and stops on a hard horizontal line, which reads as a pasted-in
     band rather than the page dissolving into it. Masking the whole layer
     (not just the image) takes the scrim with it, so the fade is even. */
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent 0%,
    rgba(0, 0, 0, 0.35) 6%,
    #000 22%,
    #000 82%,
    rgba(0, 0, 0, 0.5) 94%,
    transparent 100%
  );
  mask-image: linear-gradient(
    to bottom,
    transparent 0%,
    rgba(0, 0, 0, 0.35) 6%,
    #000 22%,
    #000 82%,
    rgba(0, 0, 0, 0.5) 94%,
    transparent 100%
  );
}

/* The hairline above the closing section defeats the point of the fade —
   it draws the exact hard edge the mask is dissolving. */
.section:has(+ .closing) { border-bottom: 0; }
.closing-inner { position: relative; z-index: 1; }

.closing-bg video {
  position: absolute;
  inset: -4%;
  width: 108%;
  height: 108%;
  object-fit: cover;
  /* The plate is a cold blue; the page is warm bone. Pulling the saturation
     down and warming it is what keeps it from reading as a foreign asset
     pasted onto the design. */
  filter: saturate(0.3) sepia(0.24) contrast(1.04) brightness(1.03);
}

/* Scrim. Without it the metal's own highlights run straight through the
   headline and the copy stops being readable. Heavier at the edges than the
   middle, so the centre of the plate still shows through. */
.closing-bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse 80% 66% at 50% 42%,
    color-mix(in oklch, var(--background) 62%, transparent) 0%,
    color-mix(in oklch, var(--background) 74%, transparent) 50%,
    color-mix(in oklch, var(--background) 93%, transparent) 100%
  );
}

/* The footage supplies its own motion, so the slow CSS drift that used to
   animate the still is gone — running both read as two things moving at once. */
.closing-inner {
  max-width: 760px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  text-align: center;
}
.closing h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(34px, 4.3vw, 64px);
  line-height: 1.04;
  letter-spacing: -0.025em;
  font-weight: 500;
  text-wrap: balance;
}
.closing .lede { max-width: 48ch; }

/* ── Footer ──────────────────────────────────────────────────────────── */

.site-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 28px clamp(20px, 4vw, 48px);
  border-top: 1px solid var(--hair-10);
  font-size: 13px;
  color: var(--muted-foreground);
}
.footer-links { display: flex; gap: 24px; }
.site-footer a { color: var(--muted-foreground); }
.site-footer a:hover { color: var(--foreground); }

/* ── Overlay ─────────────────────────────────────────────────────────── */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--background);
  overflow-y: auto;
  animation: joLineIn 0.35s ease both;
}
.overlay-inner { min-height: 100%; display: flex; flex-direction: column; }

.overlay-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 64px;
  padding: 0 clamp(20px, 4vw, 48px);
  border-bottom: 1px solid var(--hair-10);
}

.overlay-body {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(32px, 7vh, 88px) clamp(20px, 4vw, 48px);
}
.overlay-col { width: 100%; max-width: 620px; }

.step { display: flex; flex-direction: column; gap: 28px; }
.step[hidden] { display: none; }

.overlay-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(28px, 3.3vw, 44px);
  line-height: 1.1;
  letter-spacing: -0.022em;
  font-weight: 500;
}
.overlay .lede { font-size: 16px; }

.field { display: flex; flex-direction: column; gap: 12px; }
.field label { font-size: 15px; font-weight: 500; }

.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  appearance: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  height: 34px;
  padding: 0 14px;
  border-radius: var(--radius-md);
  border: 1px solid var(--hair-18);
  background: transparent;
  color: var(--foreground);
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 500;
  transition: background 0.15s ease, color 0.15s ease;
}
.chip[aria-pressed="true"] { background: var(--foreground); color: var(--background); }

.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; }

.referral { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.referral-link {
  flex: 1 1 260px;
  min-width: 0;
  display: flex;
  align-items: center;
  height: 44px;
  padding: 0 14px;
  border: 1px solid var(--hair-18);
  border-radius: var(--radius-md);
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--muted-foreground);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* ── Motion ──────────────────────────────────────────────────────────────
   Everything here animates transform and opacity only, so it stays on the
   compositor and doesn't undo the scroll work. Nothing animates layout. */

@keyframes riseIn {
  from { opacity: 0; transform: translate3d(0, 18px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Hero arrives in sequence on load rather than all at once. The hero sits
   above the fold, so the scroll-reveal system never touches it — without this
   it was the one static part of the page. */
.js-motion .hero-copy .eyebrow,
.js-motion .hero-title,
.js-motion .hero-sub,
.js-motion .capture {
  animation: riseIn 0.85s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.js-motion .hero-copy .eyebrow { animation-delay: 0.05s; }
.js-motion .hero-title         { animation-delay: 0.14s; }
.js-motion .hero-sub           { animation-delay: 0.26s; }
.js-motion .capture            { animation-delay: 0.38s; }

/* Opacity only — the earbud waveform is positioned against this box, and any
   transform here would slide the photo out from under it. */
.js-motion .hero-media { animation: fadeIn 1.2s ease 0.1s both; }

/* Children of a revealed group come in one after another instead of as a
   block. This is what makes the single-column phone layout feel alive. */
.moments .moment,
.grid-hairline .tile {
  opacity: 0;
  transform: translate3d(0, 12px, 0);
  transition: opacity 0.5s ease, transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}
[data-reveal]:not(.is-hidden) .moment,
[data-reveal]:not(.is-hidden) .tile { opacity: 1; transform: none; }

.moment:nth-child(1), .tile:nth-child(1) { transition-delay: 0.06s; }
.moment:nth-child(2), .tile:nth-child(2) { transition-delay: 0.14s; }
.moment:nth-child(3), .tile:nth-child(3) { transition-delay: 0.22s; }
.moment:nth-child(4), .tile:nth-child(4) { transition-delay: 0.30s; }

[data-reveal]:not(.is-hidden) .moment::before { transform: scaleX(1); }
.moment:nth-child(1)::before { transition-delay: 0.10s; }
.moment:nth-child(2)::before { transition-delay: 0.18s; }
.moment:nth-child(3)::before { transition-delay: 0.26s; }
.moment:nth-child(4)::before { transition-delay: 0.34s; }

/* Hero parallax. The photo drifts slower than the page, which is the cheapest
   way to make a static image feel like it has depth. Scaled a little first so
   there is slack to move into — at 1:1 the photo exactly fills its box on
   desktop and shifting it would expose an edge. Both the photo and its
   waveform live inside this wrapper, so they move as one and stay aligned. */
.media-inner {
  position: absolute;
  inset: 0;
}
.js-motion .media-inner {
  transform: scale(1.09) translate3d(0, var(--parallax, 0px), 0);
  will-change: transform;
}
/* Gentler zoom on a phone — 9% crops her head against the top edge where the
   box is only ~46px taller than the photo. */
@media (max-width: 700px) {
  .js-motion .media-inner { transform: scale(1.035) translate3d(0, var(--parallax, 0px), 0); }
}

/* Reading progress along the bottom edge of the header. A brass hairline is
   about as quiet as a progress indicator gets, and it gives the page a sense
   of length without a scrollbar. */
.scroll-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: var(--accent);
  transform: scaleX(var(--progress, 0));
  transform-origin: left;
  opacity: 0.55;
  pointer-events: none;
}

/* Section headings wipe up rather than simply fading. The bottom inset ends
   negative so descenders are never clipped. */
.js-motion .section h2,
.js-motion .closing h2 {
  clip-path: inset(0 0 110% 0);
  transition: clip-path 0.95s cubic-bezier(0.16, 1, 0.3, 1);
}
.js-motion [data-reveal]:not(.is-hidden) h2 { clip-path: inset(0 0 -12% 0); }

/* Photographs lean in slightly on hover. The waveform lives inside the same
   box, so it scales with the picture and stays on the earbud.
   Specificity matters here: .who-figure is also a [data-reveal] element, and
   the reveal rule sets `transform: none` at equal weight further down the
   file, so a plain .who-figure:hover loses. The .js-motion prefix outranks it. */
@media (hover: hover) {
  .js-motion .who-figure { transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1); }
  .js-motion .who-figure:hover { transform: scale(1.012); }
}

/* Slow breath on the play affordance so the eye finds it. */
@media (hover: hover) {
  .video:not(.is-playing) .video-pill { animation: pillBreath 3.6s ease-in-out infinite; }
}
@keyframes pillBreath {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.028); }
}

/* Header lifts off the page once you start scrolling. */
.site-header {
  transition: border-color 0.4s ease, box-shadow 0.4s ease, backdrop-filter 0.4s ease;
  border-bottom-color: transparent;
}
.site-header.is-scrolled {
  border-bottom-color: var(--hair-10);
  background: color-mix(in oklch, var(--background) 82%, transparent);
  backdrop-filter: saturate(1.4) blur(14px);
  box-shadow: 0 1px 24px color-mix(in oklch, var(--foreground) 5%, transparent);
}

/* Interaction feedback. The :active states matter most on a phone, where
   there is no hover and a press with no response reads as a dead tap. */
.btn-primary { transition: opacity 0.15s ease, transform 0.15s ease; }
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
.btn-primary:active { transform: translateY(0) scale(0.985); }
.btn-ghost:active { transform: scale(0.985); }
.chip { transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease; }
.chip:active { transform: scale(0.96); }
.video-cover:active .video-pill { transform: scale(0.985); }
.video-pill { transition: background 0.25s ease, transform 0.2s ease; }

/* Nav underline draws in from the left. */
.site-nav a { position: relative; }
.site-nav a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -5px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.site-nav a:hover::after { transform: scaleX(1); }

/* ── Reveal on scroll ────────────────────────────────────────────────── */

/* Shorter travel and a quicker curve than the design's 0.7s/18px: at full
   length the block is still visibly sliding when it reaches reading position,
   which is what makes a scroll feel laggy rather than smooth. */
[data-reveal].is-hidden { opacity: 0; transform: translate3d(0, 14px, 0); }
[data-reveal] {
  transition: opacity 0.45s ease, transform 0.55s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: opacity, transform;
}
/* Drop the compositor hint once revealed — leaving it on every block keeps a
   layer alive for the life of the page. */
[data-reveal]:not(.is-hidden) { will-change: auto; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-delay: 0s !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0s !important;
  }
  [data-reveal].is-hidden { opacity: 1; transform: none; }
  /* Staggered children and drawn rules must not depend on a class the reveal
     system never applies under reduced motion. */
  .moments .moment,
  .grid-hairline .tile { opacity: 1; transform: none; }
  .moment::before { transform: scaleX(1); }
}

/* ── Small screens ───────────────────────────────────────────────────── */

@media (max-width: 640px) {
  .site-nav a:not(.nav-cta) { display: none; }
  /* `height`, not `min-height` — the base rule sets a fixed height, so a
     min-height here is silently ignored and phones keep the full-size card. */
  .transcript { height: 268px; }
}

/* The demo footage is shot portrait. A 16:9 frame is right on a wide screen —
   the phone sits centred in it — but on a narrow one it would squeeze the
   video down to a sliver, so let the frame stand up and the video fill it. */
@media (max-width: 760px) {
  .video { aspect-ratio: 3 / 4; }
}
@media (max-width: 560px) {
  .video { aspect-ratio: 9 / 16; }
}
