/* ═══════════ LANDING / PUBLIC ENTRANCE ═══════════
   Single-viewport pre-app surface with two motion systems:
   (1) ambient vector-field drift for sophisticated dynamism,
   (2) a domain trajectory path that evolves toward better outcomes. */

.landing {
  --lp-nav-h: 60px;
  position: relative;
  height: 100%;
  width: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scroll-behavior: smooth;
  background: var(--bg-page);
  color: var(--text-primary);
  /* Hide the native scrollbar across the whole cinematic landing — it
     reads as chrome over the full-screen hero video, and the page drives
     its own "↓ scroll" affordances. (The scrolljack already hid it via
     .lp-mortality-locked; this just extends that to the hero + rest.) */
  scrollbar-width: none;
}
.landing::-webkit-scrollbar { width: 0; height: 0; display: none; }
/* While the user is inside the mortality scrollytelling, hide the
   scrollbar. The dwell-clamp and state-3 entry snap legitimately move
   scrollTop during beat transitions; without this, the user sees the
   scrollbar twitch and reads it as losing their place. Class is
   toggled by `applyState` in landing.js and removed when state resets. */
.landing.lp-mortality-locked {
  scrollbar-width: none;
}
.landing.lp-mortality-locked::-webkit-scrollbar {
  width: 0;
  height: 0;
}
/* Hard scroll lock for the forced-dwell window (engaged by engageDwellLock
   in landing.js, only while a beat's dwell is counting down). overflow:hidden
   makes the scroller physically unscrollable, so macOS momentum that leaks
   past the onWheel preventDefault has nothing to move — killing the "scroll a
   bit, jerk back" bounce at its source. Programmatic scrollTo (the pre-position
   snaps) still works under overflow:hidden, and the scrollbar is already
   hidden, so toggling overflow is layout-neutral. */
.landing.lp-dwell-locked { overflow-y: hidden; }

/* Hero section — the cinematic top-of-page. Owns the particle swarm,
   ambient haze, trajectory layer, and the original landing-frame copy.
   Fills the viewport so the experience opens with the cinematic, then
   scrolls into the rest of the marketing page below. */
.landing-hero-section {
  position: relative;
  /* Sticky nav above sits in normal flow and pushes the section down,
     so subtract its height to keep the cinematic flush with the fold —
     otherwise .landing-scrollcue (pinned to the section's bottom) lands
     below the viewport on first paint. */
  min-height: calc(100vh - var(--lp-nav-h, 60px));
  min-height: calc(100dvh - var(--lp-nav-h, 60px));
  width: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background:
    radial-gradient(ellipse 98% 64% at 44% -16%, rgba(8,145,178,0.018), transparent 74%),
    radial-gradient(ellipse 96% 68% at 84% 102%, rgba(22,163,74,0.008), transparent 78%),
    /* Bottom-of-hero gradient deepens toward the dark mortality
       backdrop below, so the seam reads as dusk rather than a cut.
       The lower wash stays under ~14% alpha — enough that a slow
       scroller sees something darkening below without breaking the
       airy cinematic above the fold. */
    linear-gradient(180deg, rgba(255,255,255,0.26) 0%, rgba(255,255,255,0) 28%, rgba(7,24,39,0.04) 72%, rgba(11,20,34,0.14) 100%),
    var(--bg-page);
}

/* ── Shared ambient scene (onboarding) ── */
.welcome-model-stage,
.landing-ambient {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  overflow: hidden;
}

/* Particle swarm host — full-viewport canvas. Sits BELOW the chrome
   (z-index 0 vs the frame's z-index 2) so headline/pillars/CTA
   occlude the cloud rather than the canvas painting on top of them
   with additive blending. */
.landing-cloud {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.landing-swarm-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

/* ── Hero background video ──
   Full-bleed looping clip behind the frame chrome. The swarm that used
   to live here has moved to the mortality reveal; the video owns the
   hero ambience now. Legibility comes from .landing-hero-scrim plus the
   light-text overrides below — all scoped to the --video modifier so the
   swarm-era light treatment is untouched if the video is ever removed. */
.landing-hero-video {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.landing-hero-video-el {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Subject sits ~1/3 from the left of the frame. On narrow/tall
     viewports cover crops the sides (centered by default), which would
     clip the subject — bias the focal point left so the crop keeps it in
     frame. Near-noop on wide desktop, where cover crops top/bottom. */
  object-position: 33% center;
}
.landing-hero-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* Two layers:
     (1) a feathered dark pocket behind the centered headline/subhead so
         the copy keeps contrast on any video frame WITHOUT dimming the
         footage at the edges. Feathered to transparent → no visible edge.
         The top lockup (white wordmark + stroked tagline + dark-glass
         pill) already carries its own contrast, so it gets the smooth
         base wash below instead of a discrete pocket (a pocket there read
         as a distracting blob). CTA carries its own fill; the scroll cue
         sits in the base's deep bottom.
     (2) a vertical base wash — deeper through the top third (covers the
         lockup uniformly), light through the mids, deep at the bottom
         (toward the dark mortality backdrop, so the seam reads as dusk). */
  background:
    radial-gradient(ellipse 90% 30% at 50% 50%, rgba(6, 13, 23, 0.52), transparent 76%),
    linear-gradient(
      180deg,
      rgba(7, 16, 28, 0.50) 0%,
      rgba(7, 16, 28, 0.30) 18%,
      rgba(7, 16, 28, 0.12) 34%,
      rgba(7, 16, 28, 0.10) 52%,
      rgba(7, 16, 28, 0.40) 78%,
      rgba(11, 20, 34, 0.74) 100%
    );
}

/* Light-text treatment for hero copy sitting directly on the video. The
   nav keeps its own frosted-white bar, so it's deliberately left alone.
   The ambient orb + rising-trajectory line were fillers for the blank
   hero — the footage replaces them, so hide them here. */
.landing-hero-section--video .landing-ambient,
.landing-hero-section--video .landing-trajectory-wrap { display: none; }

/* Full-screen hero: no top chrome. The nav stays hidden until the reveal
   (driven in JS, motion path only), so pull the hero up under the nav's
   reserved row and let the video fill the whole viewport. Compound
   selector so it outranks the base .landing-hero-section sizing
   regardless of source order. */
.landing-hero-section.landing-hero-section--video {
  margin-top: calc(-1 * var(--lp-nav-h, 60px));
  min-height: 100vh;
  min-height: 100dvh;
}

/* Heavier weights over the footage — mono copy needs more body against a
   moving background than it does on the flat light page. JetBrains Mono
   ships up to 700 (see the Google Fonts import); don't exceed it or the
   browser fakes the bold. The dark halo (a soft wide blur) travels with
   the glyphs so the copy holds contrast as the clip loops into brighter
   snow frames, independent of the section scrim. */
.landing-hero-section--video .landing-headline {
  color: #fbfdff;
  font-weight: 700;
  text-shadow:
    0 1px 2px rgba(4, 10, 18, 0.6),
    0 2px 26px rgba(4, 10, 18, 0.6),
    0 4px 60px rgba(4, 10, 18, 0.45);
}
.landing-hero-section--video .landing-sub {
  color: rgba(240, 247, 255, 0.95);
  font-weight: 500;
  text-shadow:
    0 1px 2px rgba(4, 10, 18, 0.6),
    0 2px 22px rgba(4, 10, 18, 0.55);
}
.landing-hero-section--video .landing-mark-word {
  font-weight: 700;
  /* The brand cyan is too low-contrast over dark footage (and washes out
     on bright snow frames). White reads cleanly; the cyan identity still
     lives in the dot + tagline accents below. */
  color: #fbfdff;
  /* Just a soft diffuse drop for separation — the uniform top wash
     already carries the contrast, so no tight dark edge (that read as a
     heavy outline on the large letterforms). */
  text-shadow: 0 2px 18px rgba(4, 10, 18, 0.42);
}
/* Small, letter-spaced labels are the hardest to read over the footage —
   the thin strokes get lost. Give them a tight near-opaque dark edge
   (acts like a 1px stroke) layered under a soft blur, and push the color
   to near-white. */
.landing-hero-section--video .landing-mark-tag {
  color: #eaf4fd;
  font-weight: 700; /* heaviest JetBrains Mono weight loaded */
  font-size: 11px;       /* +1px for a touch more stroke body */
  letter-spacing: 2.8px; /* tightened slightly so the word reads denser */
  /* Soft drop only — the weight + top wash carry it now, so no tight
     dark edge (that read heavy, same as the wordmark). */
  text-shadow: 0 1px 12px rgba(4, 10, 18, 0.45);
}
.landing-hero-section--video .landing-scrollcue {
  color: #eaf4fd;
  /* Tiny and sits over the bright bottom of the frame — thicken + enlarge
     and keep the tight dark edge so it stays legible. */
  font-weight: 700;
  font-size: 10.5px;
  text-shadow:
    0 0 1px rgba(4, 10, 18, 0.95),
    0 1px 3px rgba(4, 10, 18, 0.85),
    0 2px 16px rgba(4, 10, 18, 0.7);
}
/* The base CTA's pale ring is right in spirit but too thick/opaque (9px
   @ .74) over video. Keep the ring, just dial it down to a soft 4px halo,
   over a tight dark drop for grounding. */
.landing-hero-section--video .landing-cta {
  box-shadow:
    0 12px 30px -16px rgba(4, 10, 18, 0.7),
    0 0 0 4px rgba(232, 248, 252, 0.2);
  /* Symmetric padding now that the arrow is gone — the base's extra left
     pad existed to optically balance it, which would leave the label
     off-center here. */
  padding: 19px 40px;
}
/* Hover: keep the blue lift, hold the same restrained ring. */
.landing-hero-section--video .landing-cta:hover {
  box-shadow:
    0 14px 32px -14px rgba(37, 99, 235, 0.5),
    0 0 0 4px rgba(232, 248, 252, 0.28);
}

/* Early-access pill: the base style is built for a light page — a white
   inset top-highlight and a downward cyan drop-glow that, over dark
   video, read as a misaligned blue layer ("offset"), plus a light
   frosted fill that picks up the blurred footage unevenly. Rework it as a
   clean dark-glass chip: translucent dark fill, a single crisp accent
   hairline, no bevel shadows. */
.landing-hero-section--video .landing-recency {
  background: rgba(10, 20, 34, 0.42);
  border-color: color-mix(in srgb, var(--accent) 55%, rgba(255, 255, 255, 0.5));
  color: #dff3fc;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  box-shadow: none;
}

/* Click ripple — subtle expanding ring at the click point. Sits above
   the cloud but below the frame chrome so it never overlaps headline /
   pillars / CTA. Self-removes on animationend (see landing.js). */
.landing-ripple {
  position: absolute;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 1px solid rgba(16, 165, 200, 0.32);
  transform: translate(-50%, -50%) scale(0.3);
  opacity: 0;
  pointer-events: none;
  z-index: 1;
  animation: landing-ripple-out 800ms cubic-bezier(0.16, 0.84, 0.34, 1) forwards;
}

@keyframes landing-ripple-out {
  0% {
    transform: translate(-50%, -50%) scale(0.3);
    opacity: 0;
    border-width: 1px;
  }
  18% {
    opacity: 0.32;
  }
  100% {
    transform: translate(-50%, -50%) scale(2.6);
    opacity: 0;
    border-width: 0.4px;
  }
}

/* When detached as a bridge, the canvas is reparented to <body> with
   fixed positioning so it persists across the landing → welcome pane
   swap. z-index sits below welcome-card (z:2) but above the model
   stage (z:0) so the figure remains visible while the 3D model fades
   in beneath it. */
.landing-swarm-bridge {
  position: fixed !important;
  z-index: 1 !important;
  pointer-events: none;
  opacity: 1;
  transition: opacity 700ms ease;
}

.landing-ambient::before {
  content: "";
  position: absolute;
  z-index: 1;
  inset: -18% -14%;
  background:
    radial-gradient(ellipse 34% 54% at 18% 40%, rgba(8,145,178,0.15), rgba(8,145,178,0) 74%),
    radial-gradient(ellipse 30% 50% at 74% 30%, rgba(8,145,178,0.11), rgba(8,145,178,0) 74%),
    radial-gradient(ellipse 36% 56% at 66% 72%, rgba(8,145,178,0.09), rgba(8,145,178,0) 76%),
    radial-gradient(ellipse 24% 40% at 48% 52%, rgba(255,255,255,0.3), rgba(255,255,255,0) 78%);
  opacity: 0.24;
  filter: blur(12px);
  animation: landing-diffuse-a 14s ease-in-out infinite alternate;
}

.landing-ambient::after {
  content: "";
  position: absolute;
  z-index: 2;
  inset: -24% -20%;
  background:
    radial-gradient(ellipse 28% 46% at 22% 64%, rgba(8,145,178,0.1), rgba(8,145,178,0) 76%),
    radial-gradient(ellipse 30% 52% at 78% 62%, rgba(8,145,178,0.09), rgba(8,145,178,0) 78%),
    radial-gradient(ellipse 20% 34% at 54% 24%, rgba(255,255,255,0.24), rgba(255,255,255,0) 82%);
  opacity: 0.2;
  mix-blend-mode: multiply;
  filter: blur(18px);
  animation: landing-diffuse-b 18s ease-in-out infinite alternate;
}

.model-scene-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* Full-viewport rather than bound to `.welcome-model-stage` — the
   stage canvas itself is constrained by `.welcome`'s `max-width` on
   wide monitors, but the vignette's atmospheric wash needs to cover
   the entire viewport so the body's plain page color doesn't show as
   a darker stripe on either side of the constrained layout. z-index
   sits above the canvas (z:0 within stage) but below the card
   (z:2). pointer-events:none so the wash doesn't block clicks. */
.model-scene-vignette {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(ellipse 76% 54% at 50% 48%, rgba(255,255,255,0) 8%, rgba(255,255,255,0.4) 72%, rgba(255,255,255,0.82) 100%),
    linear-gradient(0deg, rgba(246, 249, 252, 0.96) 0%, rgba(246, 249, 252, 0.7) 16%, rgba(246, 249, 252, 0) 38%),
    linear-gradient(90deg, rgba(246, 249, 252, 0.9) 0%, rgba(246, 249, 252, 0) 16%, rgba(246, 249, 252, 0) 84%, rgba(246, 249, 252, 0.9) 100%),
    radial-gradient(ellipse 90% 64% at 52% 16%, rgba(8,145,178,0.07), rgba(8,145,178,0) 68%);
}

.landing-orb {
  position: absolute;
  width: min(46vw, 560px);
  aspect-ratio: 1;
  left: 70%;
  top: 46%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background:
    radial-gradient(circle at 38% 34%, rgba(255,255,255,0.4), rgba(255,255,255,0.02) 44%, rgba(255,255,255,0) 70%),
    radial-gradient(circle at 62% 64%, rgba(22,163,74,0.016), rgba(8,145,178,0.012) 42%, rgba(8,145,178,0) 74%);
  filter: blur(0.2px);
  opacity: 0.16;
  animation: landing-orb-breathe 14s ease-in-out infinite;
}

/* Domain-specific trajectory layer. Wrapper is sized + positioned;
   the inner svg + marker fill it via `inset: 0`. The wrapper exists so
   the marker (an HTML element with animated pseudo-children) can be
   positioned in the same coordinate space as the svg without using
   `position: fixed` — which would pin it to the viewport even after
   the user scrolls past the hero. */
.landing-trajectory-wrap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: 44vh;
  min-height: 240px;
  pointer-events: none;
  z-index: 1;
}
.landing-trajectory {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0.86;
}

.landing-trajectory-track {
  stroke-dasharray: 2500;
  stroke-dashoffset: 2500;
  animation: landing-trace 2600ms cubic-bezier(.4, 0, .2, 1) 620ms forwards;
}

.landing-trajectory-area {
  opacity: 0;
  animation: landing-fade-soft 1800ms ease 1320ms forwards;
}

.landing-trajectory-marker {
  position: absolute;
  z-index: 2;
  pointer-events: none;
}

.landing-trajectory-dot {
  position: absolute;
  left: 0;
  top: 0;
  width: 11px;
  height: 11px;
  border-radius: 999px;
  background:
    radial-gradient(circle at 34% 30%, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.12) 38%, rgba(255,255,255,0) 62%),
    color-mix(in srgb, var(--accent) 60%, var(--green) 40%);
  transform: translate(-50%, -50%);
  box-shadow:
    0 0 0 2px rgba(8,145,178,0.16),
    0 0 20px rgba(8,145,178,0.36),
    0 0 34px rgba(22,163,74,0.22);
  animation: landing-dot-breathe 3600ms ease-in-out infinite;
  opacity: var(--trajectory-intensity, 0.86);
}

.landing-trajectory-pulse {
  position: absolute;
  left: 0;
  top: 0;
  width: 26px;
  height: 26px;
  border-radius: 999px;
  transform: translate(-50%, -50%) scale(0.2);
  background: radial-gradient(circle, rgba(8,145,178,0.24) 0%, rgba(8,145,178,0.02) 72%);
  opacity: 0;
  animation: landing-dot-pulse 2400ms ease-out infinite;
}

.landing-trajectory-ring {
  position: absolute;
  left: 0;
  top: 0;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  border: 1px solid rgba(8,145,178,0.38);
  opacity: 0.7;
  animation: landing-ring-orbit 5200ms linear infinite;
}

.landing-trajectory-glint {
  position: absolute;
  left: 0;
  top: 0;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: rgba(255,255,255,0.94);
  transform: translate(-50%, -50%);
  box-shadow: 0 0 8px rgba(255,255,255,0.52);
  animation: landing-glint-orbit 5200ms linear infinite;
}

/* Content frame — flex column, generous breathing room, max-width to
   keep line lengths cockpit-tight. */
.landing-frame {
  position: relative;
  z-index: 2;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  max-width: 1040px;
  width: 100%;
  margin: 0 auto;
  padding: clamp(28px, 5vh, 64px) clamp(20px, 4vw, 52px);
  text-align: center;
}

/* ── Brand mark ── */
.landing-mark {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  color: var(--accent);
  opacity: 0;
  animation: landing-rise 1900ms cubic-bezier(.2, .7, .2, 1) 600ms forwards;
}
.landing-mark > svg { display: block; }
.landing-mark-word {
  font-family: var(--font-mono);
  font-size: 32px;
  font-weight: 600;
  letter-spacing: 14px;
  color: var(--accent);
}
.landing-mark-tag {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 3.2px;
  color: color-mix(in srgb, var(--text-secondary) 70%, var(--accent) 30%);
  text-transform: uppercase;
}

/* ── Hero ── */
.landing-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(28px, 4vh, 44px);
  max-width: 760px;
  margin: 0 auto;
}
/* Recency pill — sits as the third line of the AESCLE / tagline / pill
   logo lockup, reading as a status tick on the brand rather than as a
   competing eyebrow above the headline. Inherits .landing-mark's tight
   10px column gap + shared fade-in, so it lands as part of the wordmark
   group rather than as its own moment. */
.landing-recency {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  padding: 6px 14px 6px 12px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 6%, rgba(255, 255, 255, 0.78));
  border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--border));
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 1.8px;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--accent) 72%, var(--text-secondary));
  backdrop-filter: blur(4px);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.7) inset,
    0 6px 18px -10px color-mix(in srgb, var(--accent) 50%, transparent);
}
.landing-recency-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent);
  animation: landing-recency-pulse 2.2s ease-in-out infinite;
}
.landing-recency-label {
  display: inline-block;
  line-height: 1;
}
@keyframes landing-recency-pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent);
  }
  50% {
    box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 0%, transparent);
  }
}
.landing-headline {
  font-family: var(--font-sans);
  font-size: clamp(34px, 5vw, 60px);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: 0.01em;
  color: var(--text-primary);
  /* Layered halo of the page background. Reads as scattered light in the
     fog rather than a card edge, but masks particles behind the glyphs. */
  text-shadow:
    0 0 14px rgba(255,255,255,0.95),
    0 0 28px rgba(255,255,255,0.78),
    0 0 56px rgba(255,255,255,0.55),
    0 10px 36px rgba(8,145,178,0.11);
  opacity: 0;
  animation: landing-rise 2200ms cubic-bezier(.2, .7, .2, 1) 1800ms forwards;
}
.landing-sub {
  font-family: var(--font-sans);
  font-size: clamp(14px, 1.32vw, 19px);
  font-weight: 400;
  line-height: 1.58;
  letter-spacing: -0.01em;
  color: color-mix(in srgb, var(--text-primary) 70%, var(--text-secondary) 30%);
  max-width: 600px;
  text-shadow:
    0 0 10px rgba(255,255,255,0.92),
    0 0 22px rgba(255,255,255,0.7),
    0 0 44px rgba(255,255,255,0.45);
  opacity: 0;
  /* A hair behind the headline (not its own beat) — the subhead
     reinforces the same message, so it follows as part of one "promise"
     beat. The hero reads as three acts: brand → promise → CTA. */
  animation: landing-rise 2200ms cubic-bezier(.2, .7, .2, 1) 1950ms forwards;
}
/* ── Footer / CTA ── */
.landing-foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 28px;
  position: relative;
  margin-top: clamp(32px, 5.4vh, 78px);
  margin-bottom: clamp(36px, 5.4vh, 64px);
  opacity: 0;
  animation: landing-rise 1900ms cubic-bezier(.2, .7, .2, 1) 3000ms forwards;
}
.landing-cta {
  font-family: var(--font-mono);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 4.6px;
  color: #f8fbff;
  background: linear-gradient(90deg, #1f2a44 0%, #111827 100%);
  border: 1px solid #2c3b5d;
  padding: 19px 38px 19px 43px;
  border-radius: 999px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 14px;
  position: relative;
  z-index: 6;
  opacity: 1;
  transition: background 200ms ease, color 200ms ease, border-color 200ms ease, transform 200ms ease, box-shadow 200ms ease;
  box-shadow:
    0 18px 38px -14px rgba(17, 24, 39, 0.5),
    0 0 0 9px rgba(232, 248, 252, 0.74);
}
.landing-cta:hover {
  background: linear-gradient(90deg, #2563eb 0%, #1d4ed8 100%);
  border-color: #2563eb;
  transform: translateY(-1px);
  box-shadow:
    0 18px 38px -12px rgba(37,99,235,0.42),
    0 0 0 8px rgba(232, 248, 252, 0.8);
}
.landing-cta:active { transform: translateY(0); }
.landing-cta-arrow {
  font-size: 16px;
  letter-spacing: 0;
  transition: transform 200ms ease;
}
.landing-cta:hover .landing-cta-arrow { transform: translateX(3px); }

.landing-foot-note {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 1px;
  color: color-mix(in srgb, var(--text-dim) 64%, var(--text-secondary) 36%);
  margin: 0;
  position: relative;
  z-index: 2;
}

/* ── Exit transition (clicked BEGIN) ── */
/* Reduced-motion fallback path: fade the entire landing immediately. */
.landing.is-leaving {
  opacity: 0;
  transition: opacity 360ms ease;
}

/* Cinematic path: only the chrome and decorative layers fade. The
   particle cloud keeps animating and morphs into the silhouette,
   which is then handed off to welcome as the bridge. The whole page
   below the hero fades too — we're leaving the marketing surface, not
   sliding off it. */
.landing.is-handing-off .landing-frame,
.landing.is-handing-off .landing-trajectory,
.landing.is-handing-off .landing-trajectory-marker,
.landing.is-handing-off .landing-ambient,
.landing.is-handing-off .landing-nav,
.landing.is-handing-off .lp-section,
.landing.is-handing-off .lp-footer {
  opacity: 0;
  transition: opacity 360ms ease;
}

/* ── Animations ── */
@keyframes landing-rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes landing-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes landing-fade-soft {
  from { opacity: 0; }
  to   { opacity: 0.72; }
}
@keyframes landing-trace {
  to { stroke-dashoffset: 0; }
}
@keyframes landing-dot-pulse {
  0% {
    opacity: 0;
    transform: scale(0.2);
  }
  26% {
    opacity: 0.48;
  }
  100% {
    opacity: 0;
    transform: scale(1.28);
  }
}
@keyframes landing-dot-breathe {
  0%, 100% { opacity: 0.94; transform: translate(-50%, -50%) scale(1); }
  50%      { opacity: 0.66; transform: translate(-50%, -50%) scale(1.08); }
}
@keyframes landing-orb-breathe {
  0%, 100% { transform: translate(-50%, -50%) scale(0.985); opacity: 0.24; }
  50%      { transform: translate(-50%, -50%) scale(1.02); opacity: 0.36; }
}
@keyframes landing-diffuse-a {
  0% {
    transform: translate3d(-9%, -3%, 0) rotate(-1.4deg) scale(1.05);
  }
  50% {
    transform: translate3d(3%, 4.6%, 0) rotate(0.6deg) scale(1.11);
  }
  100% {
    transform: translate3d(10%, -2.2%, 0) rotate(1.6deg) scale(1.06);
  }
}
@keyframes landing-diffuse-b {
  0% {
    transform: translate3d(8%, -6%, 0) rotate(1.2deg) scale(1.1);
  }
  50% {
    transform: translate3d(-6%, 3.6%, 0) rotate(-0.8deg) scale(1.03);
  }
  100% {
    transform: translate3d(-9%, 6.4%, 0) rotate(-1.8deg) scale(1.12);
  }
}
@keyframes landing-ring-orbit {
  0% { transform: translate(-50%, -50%) rotate(0deg) scale(0.96); opacity: 0.55; }
  50% { transform: translate(-50%, -50%) rotate(180deg) scale(1.04); opacity: 0.84; }
  100% { transform: translate(-50%, -50%) rotate(360deg) scale(0.96); opacity: 0.55; }
}
@keyframes landing-glint-orbit {
  0% {
    transform: translate(calc(-50% + 11px), calc(-50% - 2px));
    opacity: 0.22;
  }
  12% { opacity: 0.96; }
  50% {
    transform: translate(calc(-50% - 11px), calc(-50% + 2px));
    opacity: 0.34;
  }
  62% { opacity: 0.88; }
  100% {
    transform: translate(calc(-50% + 11px), calc(-50% - 2px));
    opacity: 0.22;
  }
}

/* ═══════════ LANDING PAGE — POST-HERO SECTIONS ═══════════
   The hero above is the cinematic. Below it: a conventional
   marketing-page cadence (social proof → value prop → FAQ → CTA →
   footer). Shared rhythm: each section uses `.lp-shell` for max-width
   centering, `.lp-eyebrow` for a small uppercase label, and
   `.lp-section-title` for the section h2. Everything stays on the
   technical-instrument typography of the rest of the app
   (JetBrains Mono, uppercase + tracking for headers). */

.lp-shell {
  max-width: 1080px;
  margin: 0 auto;
  padding: 0 clamp(20px, 4vw, 52px);
  width: 100%;
}

.lp-section {
  position: relative;
  padding: clamp(96px, 14vh, 176px) 0;
  /* Anchor jumps from the sticky nav must clear it. */
  scroll-margin-top: var(--lp-nav-h, 60px);
}

.lp-eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 2.4px;
  color: var(--text-dim);
  margin: 0 0 16px;
  text-transform: uppercase;
}

.lp-section-head {
  max-width: 720px;
  margin: 0 auto clamp(40px, 6vh, 64px);
  text-align: center;
}

.lp-section-title {
  font-family: var(--font-sans);
  font-size: clamp(26px, 3.2vw, 38px);
  font-weight: 700;
  line-height: 1.16;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  margin: 0 0 18px;
}

.lp-section-sub {
  font-family: var(--font-sans);
  font-size: clamp(13.5px, 1.05vw, 16px);
  line-height: 1.55;
  letter-spacing: -0.01em;
  color: var(--text-secondary);
  margin: 0 auto;
  max-width: 480px;
  text-wrap: balance;
}

/* Mobile-only forced break in the "Find your killer. Take it down."
   heading — splits cleanly between the two sentences instead of letting
   the narrow column orphan "Take". Inert (no break) on desktop, where
   the line fits whole. */
.lp-what-break { display: none; }
@media (max-width: 880px) {
  .lp-what-break { display: inline; }
}

/* The "Your personal longevity radar" reveal stacks on two lines at every
   viewport. This is the page's climactic beat; the two-line form gives the
   tagline the vertical mass and statement cadence that a single wide line
   lacks (a plain <br> with no display toggle — it breaks everywhere). */

/* The "You're not average" and "A longer life" beats carry the
   heaviest visuals on the page (bell-curve callout + threat horizon),
   so they get extra vertical room — the visuals breathe and the
   sections feel like distinct chapters rather than stacked panels. */
.lp-section.lp-problem,
.lp-section.lp-what,
.lp-section.lp-perform {
  padding: clamp(128px, 19vh, 232px) 0;
}

/* The "promise" chapter — "A longer life" + "So you can do your
   life's work" — is wrapped in a single `.lp-chapter-promise` div
   so the cyan-tinted atmosphere paints CONTINUOUSLY across both
   beats. Painting each section separately produced a visible seam
   at the boundary because each gradient stopped at its own bottom;
   one gradient spanning the full chapter height has no inflection.
   Gradient runs from white at the top (smooth entry from the white
   `.lp-problem` band above) to a soft cyan-tinted tone at the
   bottom (a hard edge to the white `.lp-value` band below marks
   the WHAT→HOW chapter break). Two radial cyan highlights add
   internal variation: one near the threat horizon's +5y extension,
   one center-low behind the radar's focal bloom. Picks up the same
   accent-glow story used by the hero and the final CTA. */
.lp-chapter-promise {
  background:
    radial-gradient(ellipse 70% 30% at 70% 32%, rgba(8, 145, 178, 0.035), transparent 70%),
    radial-gradient(ellipse 60% 26% at 50% 78%, rgba(8, 145, 178, 0.05), transparent 70%),
    linear-gradient(180deg, #ffffff 0%, #f3f9fb 100%);
}
.lp-chapter-promise .lp-section.lp-what,
.lp-chapter-promise .lp-section.lp-perform {
  background: transparent;
}

/* The two beats are siblings inside one chapter, so the seam between
   them is tighter than the spacing on either chapter edge. Top of
   `.lp-what` and bottom of `.lp-perform` keep the full chapter
   margin (they sit against `.lp-problem` and `.lp-value`); only the
   interior gap collapses. */
.lp-section.lp-what { padding-bottom: clamp(64px, 9vh, 112px); }
.lp-section.lp-perform { padding-top: clamp(64px, 9vh, 112px); }
.lp-section.lp-what .lp-section-head {
  margin-bottom: clamp(16px, 2.5vh, 28px);
}
/* Phone: the tree's canopy chips + the raised coach sit high in the scene box
   and crowd the dek. Open the header-to-visual gap so the copy has air. */
@media (max-width: 720px) {
  .lp-section.lp-what .lp-section-head {
    margin-bottom: clamp(40px, 9vw, 60px);
  }
}

/* Chapter break — entering "The 360° view" we leave the cyan
   "promise" atmosphere and land on flat white for the HOW chapter.
   A hairline top edge makes the break unambiguous even when the
   gradient renders subtly (cheap screens, bright rooms). Mirrors
   the band edges on `.lp-faq` below. */
.lp-section.lp-value {
  border-top: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent);
}

/* ── Top nav ── */
/* Sits over the hero on load (transparent) and gets a subtle backdrop
   once the user scrolls. For a first-pass we keep it sticky with a
   light translucent panel — simple, works on every viewport. */
.landing-nav {
  position: sticky;
  top: 0;
  z-index: 20;
  /* Pin to exactly --lp-nav-h so the video hero's `margin-top: -nav-h`
     pull-up lands flush. Without this the nav renders a few px taller
     than the assumed 60px and a sliver of page background peeks above
     the full-screen hero. */
  height: var(--lp-nav-h, 60px);
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: clamp(16px, 3vw, 32px);
  padding: 14px clamp(20px, 4vw, 40px);
  padding-left: max(var(--safe-left), clamp(20px, 4vw, 40px));
  padding-right: max(var(--safe-right), clamp(20px, 4vw, 40px));
  background: rgba(255, 255, 255, 0.78);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.landing-nav-mark {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 4px;
  color: var(--accent);
  text-decoration: none;
}
.landing-nav-links {
  display: flex;
  gap: clamp(14px, 2.4vw, 28px);
  margin-left: auto;
}
.landing-nav-links a {
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 450;
  letter-spacing: 0.1px;
  /* Secondary gray, not primary: the filled CTA pill is the primary
     action, so the links must read as subordinate nav. Still legible on
     the nav's white/blur backdrop. */
  color: var(--text-secondary);
  text-decoration: none;
  transition: color 160ms ease;
}
.landing-nav-links a:hover { color: var(--accent); }
.landing-nav-cta {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  /* Same airy, deliberate tracking that makes the final CTA's mono read
     well (4.6px @ 15px), scaled to nav size. The cramped 1.8px was the
     legibility problem, not the typeface. */
  letter-spacing: 2.8px;
  color: #f8fbff;
  background: linear-gradient(90deg, #1f2a44 0%, #111827 100%);
  border: 1px solid #2c3b5d;
  padding: 10px 22px 10px 24px;
  border-radius: 999px;
  cursor: pointer;
  transition: background 180ms ease, transform 180ms ease;
}
.landing-nav-cta:hover {
  background: linear-gradient(90deg, #2563eb 0%, #1d4ed8 100%);
  transform: translateY(-1px);
}

/* Scroll cue — small affordance under the hero CTA hinting that
   there's more to read. Bounces gently to draw the eye. */
.landing-scrollcue {
  position: absolute;
  bottom: calc(24px + var(--safe-bottom));
  left: 0;
  right: 0;
  margin-inline: auto;
  width: max-content;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 2.4px;
  color: var(--text-dim);
  text-decoration: none;
  opacity: 0;
  animation: landing-rise 1400ms cubic-bezier(.2, .7, .2, 1) 4300ms forwards;
}
.landing-scrollcue-arrow {
  font-size: 14px;
  letter-spacing: 0;
  animation: landing-scrollcue-bounce 2400ms ease-in-out infinite;
}
@keyframes landing-scrollcue-bounce {
  0%, 100% { transform: translateY(0); opacity: 0.6; }
  50%      { transform: translateY(4px); opacity: 1; }
}

/* ── Social proof / "grounded in" ──
   Now embedded inside the Meet Aescle beat (see `.lp-mortality-
   aescle-logos`) rather than living as its own section after the
   pinned stage. The logo grid and individual mark styles below are
   reused inside that block — only the outer container, eyebrow
   spacing, and tonal weighting change. */
.lp-social-grid {
  /* 5 logos with variable widths wrap to 4+1 in a flex row. Use grid so
     the wrap is always balanced — 5 across when there's room, 3+2 below
     880px (the second row centers), 2+2+1 below 560px, 1-per-row mobile. */
  display: grid;
  grid-template-columns: repeat(5, auto);
  justify-content: center;
  align-items: center;
  column-gap: clamp(28px, 4.5vw, 60px);
  row-gap: clamp(28px, 4vh, 44px);
}
@media (max-width: 880px) {
  .lp-social-grid { grid-template-columns: repeat(3, auto); }
  .lp-social-mark:nth-child(4) { grid-column: 1 / span 2; justify-self: end; padding-right: 6px; }
  .lp-social-mark:nth-child(5) { grid-column: 3 / span 1; justify-self: start; padding-left: 6px; }
}
/* The marquee is the mobile rendering of the proof row; on desktop the
   static grid above is used and this stays hidden (no-op). */
.lp-social-marquee { display: none; }

@media (max-width: 600px) {
  /* On phones the stacked grid grew into a tall logo wall that stole
     focus from the Aescle mark. Swap it for a single-row, auto-scrolling
     marquee so the proof reads as one quiet line. */
  .lp-social-grid { display: none; }
  .lp-social-marquee {
    display: block;
    overflow: hidden;
    width: 100%;
    /* Soft edges so logos fade in/out rather than hard-clipping. */
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
            mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  }
  .lp-social-track {
    display: flex;
    align-items: center;
    width: max-content;
    /* The marquee lives inside the pinned mortality stage and only
       reveals on the `.is-aescle` beat — long after page load. Declare
       the scroll but hold it paused; a paused animation doesn't advance
       through its delay, so the delay clock effectively starts when the
       beat activates (see the `.is-aescle` override below). The delay
       (~3500ms) matches the logos' own fade-in cascade — 2500ms
       transition-delay + 1000ms fade — so the row begins to move just
       as they finish landing. The -50% loop then continues seamlessly. */
    animation: lp-social-scroll 26s linear 3500ms infinite;
    animation-play-state: paused;
  }
  .lp-mortality.is-aescle .lp-social-track {
    animation-play-state: running;
  }
  /* Per-mark margin (not flex gap) so the two cloned copies tile evenly
     and the -50% loop lands seamlessly. */
  .lp-social-track .lp-social-mark { flex: 0 0 auto; margin-right: 44px; }
  .lp-social-track .lp-social-logo { height: 22px; }
}

@keyframes lp-social-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .lp-social-track { animation: none; }
}
/* Each logo carries its own brand color baked into the SVG (CDC navy,
   NEJM black, Lancet red banner, JAMA black, AHA red). A subtle
   opacity lift on hover keeps the row visually unified at rest while
   letting individual marks feel touchable. */
.lp-social-mark {
  display: inline-flex;
  align-items: center;
  opacity: 0.9;
  transition: opacity 200ms ease, transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.lp-social-mark:hover {
  opacity: 1;
  transform: translateY(-4px) scale(1.05);
}
.lp-social-logo {
  display: block;
  height: 28px;
  width: auto;
}

/* ── "You're not average" problem section ──
   Sits between the mortality scrollytelling and the threat-horizon
   payoff. Section head is centered; body is a two-column grid
   (copy left, single bell-curve viz right). The viz is the
   centerpiece — the motion *is* the argument.

   Animation split:
     • One-shot on first reveal (0.0 – 2.1s):
         curve strokes in, area fills, then both dim to ghost gray;
         the mean ghost-dot fades in at the apex. After this the
         chart holds in its dimmed end state forever.
     • Infinite loop (8s cycle, starts ~1.0s after reveal):
         pin pops at the apex with a halo flash → dwells briefly →
         slides smoothly along the curve into the right tail
         (CSS offset-path, not stepped samples) → callout card
         fades in well above the chart with three "your" markers →
         hold while the halo pulses → callout + pin fade out → short
         pause → restart at the apex.

   Only the pin + callout loop. The bell curve only draws once.

   All animations are gated on `.lp-problem-viz.is-in-view` (added
   by the shared reveal observer). Class+class specificity beats
   the base `[data-reveal].is-in-view` rule. */

.lp-problem-grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: clamp(28px, 4vw, 64px);
  align-items: center;
}

.lp-problem-copy {
  display: flex;
  flex-direction: column;
}

/* Title lives inline with the visual (left column of the grid)
   instead of a centered header above — breaks the centered-title
   cadence shared with "A longer life" and "The 360° view". */
.lp-problem-title {
  margin: 0 0 16px;
  max-width: 19ch;
  text-wrap: balance;
}

.lp-problem-tagline {
  font-family: var(--font-sans);
  font-size: clamp(15px, 1.2vw, 18px);
  line-height: 1.5;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  margin: 0 0 18px;
  max-width: 420px;
}

.lp-problem-eyebrow {
  color: var(--accent);
  letter-spacing: 3.2px;
  margin: 0 0 20px;
}

.lp-problem-lede {
  font-family: var(--font-sans);
  font-size: clamp(15px, 1.2vw, 18px);
  line-height: 1.55;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  margin: 0 0 18px;
  max-width: 420px;
}

.lp-problem-sub {
  font-family: var(--font-sans);
  font-size: clamp(14px, 1.05vw, 16.5px);
  line-height: 1.55;
  letter-spacing: -0.01em;
  color: var(--text-secondary);
  margin: 0;
  max-width: 420px;
}

.lp-problem-viz {
  position: relative;
  width: 100%;
  max-width: 540px;
  margin-left: auto;
}
/* Lit stage — a soft cyan bloom behind the chart, biased toward the tail
   where the YOU pin lands, so the curve sits on ambient light instead of
   bare page. Reveal-gated; same airy-glow language as the readiness orb. */
.lp-problem-viz::before {
  content: "";
  position: absolute;
  inset: -14% -8% -20% -8%;
  z-index: 0;
  background:
    radial-gradient(ellipse 56% 56% at 60% 64%, rgba(8, 145, 178, 0.10), transparent 70%),
    radial-gradient(ellipse 82% 72% at 48% 56%, rgba(148, 163, 184, 0.06), transparent 76%);
  filter: blur(6px);
  opacity: 0;
  transition: opacity 1000ms ease;
  pointer-events: none;
}
.lp-problem-viz.is-in-view::before { opacity: 1; }

/* ── Crowd — a real 3D scene (createCrowdScene). The WebGL mount fills the
   viz; the identity card is an HTML overlay pinned above YOU (JS sets its
   left/top by projecting a world anchor), fading in when the scene flags YOU
   has stepped out (.is-you-up). ── */
.lp-crowd3d {
  position: relative;
  z-index: 1;
  width: 100%;
  aspect-ratio: 7 / 5;
}
.lp-crowd3d-scene { position: absolute; inset: 0; }
.lp-crowd3d-scene canvas { display: block; width: 100%; height: 100%; }

/* Identity card — premium light card, positioned by JS, centered above its
   anchor. The N=1 proof: age, one identity line, two risk-relevant facts. */
.lp-crowd3d-card {
  position: absolute;
  left: 50%;
  top: 18%;
  transform: translate(-50%, -100%) translateY(-2px);
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 168px;
  padding: 12px 16px 13px;
  border-radius: 16px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  box-shadow:
    0 1px 2px rgba(15, 23, 42, 0.10),
    0 12px 26px -10px rgba(15, 23, 42, 0.22);
  opacity: 0;
  transition: opacity 520ms ease, transform 520ms cubic-bezier(.3, .8, .3, 1);
  pointer-events: none;
}
.lp-crowd3d-card::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -8px;
  width: 16px;
  height: 16px;
  transform: translateX(-50%) rotate(45deg);
  background: var(--bg-surface);
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  box-shadow: 5px 5px 12px -9px rgba(15, 23, 42, 0.22);
}
.lp-crowd3d.is-you-up .lp-crowd3d-card {
  opacity: 1;
  transform: translate(-50%, -100%) translateY(-10px);
  animation: lp-crowd-card-float 4.2s ease-in-out infinite;
}
@keyframes lp-crowd-card-float {
  0%, 100% { transform: translate(-50%, -100%) translateY(-10px); }
  50% { transform: translate(-50%, -100%) translateY(-16px); }
}
.lp-crowd3d-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 2px;
}
.lp-crowd3d-card-eyebrow {
  font-family: var(--font-mono);
  font-size: 13.5px;
  font-weight: 700;
  letter-spacing: 1.8px;
  color: var(--accent);
}
.lp-crowd3d-card-age {
  font-family: var(--font-mono);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.4px;
  color: var(--text-primary);
}
.lp-crowd3d-card-row {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2px;
  color: var(--text-secondary);
}

@media (max-width: 820px) {
  .lp-problem-grid {
    grid-template-columns: minmax(0, 1fr);
    /* The YOU card floats up out of the crowd's headroom and can poke above
       the scene box — give the copy above it real clearance so they don't
       crowd each other on a phone. */
    gap: clamp(52px, 12vw, 80px);
  }
  .lp-problem-copy { order: 1; }
  .lp-problem-viz {
    order: 2;
    max-width: 480px;
    margin: 0 auto;
  }
  .lp-problem-lede,
  .lp-problem-sub {
    max-width: none;
  }
}

/* ── Don't die — a real 3D scene (tree-scene.js). The WebGL mount stays
   transparent; this CSS owns the ambient wash so the scene blends into the
   page instead of sitting inside a visible rendered box. ── */
.lp-horizon-stage {
  position: relative;
  margin: 0 auto;
  max-width: 1080px;
  padding: 0 clamp(16px, 2.5vw, 32px) clamp(8px, 1.5vh, 20px);
}
.lp-tree {
  position: relative;
  width: 100%;
  max-width: 580px;
  margin: 0 auto;
  aspect-ratio: 1 / 1;
}
.lp-tree::before {
  content: "";
  position: absolute;
  inset: -14% -18% -10%;
  /* Faint neutral grounding glow only — the SKY/MOOD now lives inside the canvas
     (tree-scene.js): the lights warm, the cloud bank parts + thins, and the sun
     breaks through, all sized to fit fully in frame with no hard canvas
     rectangle to hide. This just softly seats the specimen on the page. */
  background:
    radial-gradient(circle at 50% 56%,
      rgba(226, 235, 239, 0.42) 0%,
      rgba(226, 235, 239, 0.2) 34%,
      rgba(226, 235, 239, 0.07) 56%,
      transparent 74%);
  filter: blur(18px);
  pointer-events: none;
}
.lp-tree-scene { position: absolute; inset: 0; z-index: 1; }
.lp-tree-scene canvas { display: block; width: 100%; height: 100%; }

/* Threat chips — small severity-colored labels pinned above each bare limb by
   the scene; fade in once it goes live. Icons are currentColor cod glyphs. */
@property --threat-bob {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --threat-glow {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
.lp-tree-label {
  position: absolute;
  left: 50%;
  top: 50%;
  --chip-x: 0px;
  --chip-y: 0px;
  --threat-bob: 0px;
  --threat-glow: 0;
  transform:
    translate(calc(-50% + var(--chip-x)), calc(-100% + var(--chip-y) + var(--threat-bob)))
    translateY(6px)
    scale(calc(1.06 - 0.1 * var(--dim)));
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 12px 5px 10px;
  border-radius: 999px;
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--sev) calc(10% * (1 - var(--dim))), rgba(255, 255, 255, 0.96)),
      rgba(255, 255, 255, 0.9));
  /* --dim (0→1, set per-frame by the scene as the threat heals) lerps the whole
     chip from its severity colour to GREEN — a clear "handled" state change,
     not a fade. --c is the live colour the border/icon/text all follow. */
  --dim: 0;
  --c: color-mix(in srgb, var(--green) calc(var(--dim) * 100%), var(--sev));
  --danger: color-mix(in srgb, var(--sev) calc((1 - var(--dim)) * 100%), var(--green) calc(var(--dim) * 100%));
  border: 1.5px solid color-mix(in srgb, var(--danger) 58%, var(--border));
  box-shadow:
    0 6px 16px -9px color-mix(in srgb, var(--danger) 68%, rgba(15, 40, 55, 0.34)),
    0 0 0 calc(2px + 3px * var(--threat-glow)) color-mix(in srgb, var(--danger) calc((12% + 8% * var(--threat-glow)) * (1 - var(--dim))), transparent);
  color: var(--c);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 500ms ease, transform 420ms cubic-bezier(.3, .8, .3, 1);
  pointer-events: none;
  z-index: 3;
}
.lp-tree.is-live .lp-tree-label {
  opacity: calc(1 - 0.48 * var(--dim));
  transform:
    translate(calc(-50% + var(--chip-x)), calc(-100% + var(--chip-y) + var(--threat-bob)))
    translateY(0)
    scale(calc(1.06 - 0.1 * var(--dim)));
  filter: saturate(calc(1.12 - 0.42 * var(--dim))) contrast(calc(1.04 - 0.08 * var(--dim)));
  animation: lp-tree-threat-breathe 3.8s ease-in-out infinite;
}
.lp-tree-label[data-threat="cardiac"] { --chip-y: 22px; }
.lp-tree-label[data-threat="cancer"] { --chip-x: 28px; --chip-y: 10px; }
.lp-tree-label[data-threat="metabolic"] { --chip-x: -58px; --chip-y: 16px; }
.lp-tree-label[data-threat="cancer"] { animation-delay: -1.15s; }
.lp-tree-label[data-threat="metabolic"] { animation-delay: -2.35s; }
.lp-tree.is-live .lp-tree-label[data-threat="cancer"] { transition-delay: 90ms; }
.lp-tree.is-live .lp-tree-label[data-threat="metabolic"] { transition-delay: 180ms; }
.lp-tree-label svg { width: 17px; height: 17px; flex: 0 0 auto; }
.lp-tree-label span {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 1.2px;
  color: color-mix(in srgb, var(--c) calc(84% - 20% * var(--dim)), var(--text-secondary));
}
@keyframes lp-tree-threat-breathe {
  0%, 100% { --threat-bob: 0px; --threat-glow: 0; }
  50% { --threat-bob: -3px; --threat-glow: 1; }
}

/* "+5 YEARS" payoff card — pinned at the cyan new growth, fades in once the
   tree reaches (.is-reached). */
.lp-tree-card {
  position: absolute;
  left: 50%;
  top: auto;
  bottom: 60px;
  transform: translateX(-50%) translateY(8px);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  padding: 9px 16px 10px;
  border-radius: 999px;
  /* A SOLID filled cyan reward — deliberately unlike the outline threat chips,
     so the payoff reads as an achievement, not just another label. */
  background: linear-gradient(135deg, #15a7c6, #0786a6);
  border: none;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.25),
    0 14px 30px -10px color-mix(in srgb, var(--accent) 70%, transparent);
  opacity: 0;
  transition: opacity 520ms ease, transform 520ms cubic-bezier(.3, .8, .3, 1);
  pointer-events: none;
  z-index: 4;
}
.lp-tree.is-reached .lp-tree-card {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
.lp-tree-card-big {
  font-family: var(--font-mono);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.8px;
  color: #fff;
}
.lp-tree-card-age {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 1.4px;
  color: rgba(255, 255, 255, 0.82);
}

/* Lifespan endpoint labels are a baseline readout below the specimen, not leaf
   callouts. AGE 82 hands off to AGE 87 once the gain lands. */
.lp-tree-age {
  position: absolute;
  left: 50%;
  top: auto;
  bottom: 18px;
  transform: translateX(-50%);
  padding: 6px 13px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.82);
  border: 1px solid var(--border);
  box-shadow: 0 5px 18px -14px rgba(15, 40, 55, 0.28);
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 1.2px;
  color: var(--text-secondary);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 500ms ease, transform 520ms cubic-bezier(.3, .8, .3, 1);
  pointer-events: none;
  z-index: 3;
}
.lp-tree.is-live .lp-tree-age[data-age="82"] { opacity: 1; }
.lp-tree.is-reached .lp-tree-age[data-age="82"] {
  opacity: 0;
  transform: translateX(-50%) translateY(5px);
}
.lp-tree-age--reach {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 38%, var(--border));
  transform: translateX(-50%) translateY(-5px);
}
.lp-tree.is-reached .lp-tree-age--reach {
  opacity: 1;
  transform: translateX(-50%);
}

/* Coach token — a quiet Aescle presence. It appears and pulses in place while
   the tree responds, instead of tracing every threat. */
.lp-tree-coach {
  position: absolute;
  left: 50%;
  top: 50%;
  --coach-op: 0;
  --coach-pulse: 0;
  --coach-wave: 0;
  --coach-wave-op: 0;
  width: 66px;
  height: 66px;
  /* -33 left centres the 66px token on its projected anchor; the extra negative
     top lifts it a touch higher off the canopy (desktop — mobile sets its own
     margin in the 600px query). */
  margin: -47px 0 0 -33px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  /* Pearlescent glass token: a glossy crown fading to a faint cyan base, so it
     reads as a polished object catching light — not a flat white disc against
     the dimensional tree. */
  background:
    radial-gradient(125% 108% at 50% 16%,
      rgba(255, 255, 255, 0.99),
      rgba(255, 255, 255, 0.74) 58%,
      color-mix(in srgb, var(--accent) 16%, rgba(255, 255, 255, 0.7)) 100%);
  border: 1px solid color-mix(in srgb, var(--accent) 26%, rgba(255, 255, 255, 0.55));
  box-shadow:
    0 12px 28px -12px rgba(15, 40, 55, 0.42),
    0 2px 7px -2px color-mix(in srgb, var(--accent) 28%, rgba(15, 40, 55, 0.16)),
    inset 0 1.5px 1px rgba(255, 255, 255, 0.95),
    inset 0 -7px 14px -7px color-mix(in srgb, var(--accent) 24%, transparent),
    inset 0 0 0 1px rgba(255, 255, 255, 0.5),
    0 0 calc(5px + 13px * var(--coach-pulse)) color-mix(in srgb, var(--accent) calc(14% + 26% * var(--coach-pulse)), transparent);
  color: var(--accent);
  opacity: var(--coach-op);
  transform: scale(calc(0.98 + 0.045 * var(--coach-pulse)));
  pointer-events: none;
  z-index: 4;
}
/* Specular crown — a soft glossy highlight arcing across the top, the tell of
   a premium glass surface. */
.lp-tree-coach::after {
  content: "";
  position: absolute;
  top: 5px;
  left: 12%;
  right: 12%;
  height: 42%;
  border-radius: 50%;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0));
  opacity: 0.7;
  pointer-events: none;
}
.lp-tree-coach::before {
  content: "";
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--accent) 42%, transparent);
  /* A localized "scanning" pulse around the coach — NOT a room-filling sonar.
     The coach sits high in the frame, so a large ring used to bleed up into the
     header copy; keep the travel short and fade it out quadratically so it
     dissolves well inside the visual. */
  opacity: calc(var(--coach-wave-op) * var(--coach-wave-op) * 0.34);
  transform: scale(calc(0.82 + 3.4 * var(--coach-wave)));
  pointer-events: none;
}
.lp-tree-coach svg { width: 38px; height: 38px; display: block; position: relative; z-index: 1; }
/* Soft outer aura that blooms as the coach lands (behind the white token). */
.lp-tree-coach-glow {
  position: absolute;
  inset: -55%;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--accent) 50%, transparent), transparent 70%);
  opacity: calc(0.06 + 0.28 * var(--coach-pulse));
  pointer-events: none;
}

/* ── Phone: the tree shrinks but the threat chips project to near-identical
   screen points and their fixed px offsets stop separating them — they pile
   up and overlap. Push them apart horizontally and stagger their heights so
   each reads as its own callout. The coach's expanding pulse ring also reaches
   the screen edges on a narrow viewport, so shrink its travel and steepen the
   opacity falloff so it dissolves well before the margins. ── */
@media (max-width: 600px) {
  .lp-tree-label {
    padding: 4px 10px 4px 9px;
  }
  .lp-tree-label svg { width: 15px; height: 15px; }
  .lp-tree-label span { font-size: 9.5px; letter-spacing: 0.9px; }
  /* A downward triangle inside the canopy: cancer + metabolic up on the sides,
     cardiac centered just below them — but kept high enough to sit ON the
     leaves, not down on the trunk. The vertical gap (≥ a chip height) keeps it
     collision-free regardless of how the side chips project. */
  .lp-tree-label[data-threat="cardiac"]   { --chip-x: 0px;   --chip-y: 30px; }
  .lp-tree-label[data-threat="cancer"]    { --chip-x: 54px;  --chip-y: 2px; }
  .lp-tree-label[data-threat="metabolic"] { --chip-x: -66px; --chip-y: 6px; }

  /* Smaller coach, lifted clear of the canopy (the extra negative top margin
     raises it off the leaves; -28 keeps it horizontally centered on its
     projected anchor). */
  .lp-tree-coach {
    width: 56px;
    height: 56px;
    margin: -58px 0 0 -28px;
  }
  .lp-tree-coach svg { width: 33px; height: 33px; }

  /* Ring dies back before it hits the gutters: ~60% of the desktop reach, with
     a quadratic opacity fade so it's gone by the time it's halfway out. */
  .lp-tree-coach::before {
    opacity: calc(var(--coach-wave-op) * var(--coach-wave-op) * 0.3);
    transform: scale(calc(0.82 + 2.8 * var(--coach-wave)));
  }

  /* The readout pills are fixed-px, so on the smaller mobile canvas they dwarf
     the (pulled-back) tree. Scale them down to stay proportional. */
  .lp-tree-card { padding: 7px 13px 8px; }
  .lp-tree-card-big { font-size: 12px; letter-spacing: 0.6px; }
  .lp-tree-card-age { font-size: 8.5px; }
  .lp-tree-age { padding: 5px 11px; font-size: 11px; letter-spacing: 1px; }
}


/* ═══════════ LANDING — PERFORM SECTION ═══════════
   Readiness orb. Seven felt dimensions are rendered as lobes on one living 3D object,
   not as separate chart axes. The argument is coordination: Aescle
   turns the user's health signals into a fuller, smoother state they
   can feel today.

   Motion: the orb idles with slow organic deformation, then each
   readiness lobe rises in sequence while its projected label sharpens.
   ENERGY borrows the battery language from the "Your data" stage so
   the two landing visuals share a consumer-performance vocabulary. */

/* ─ Layout ─
   Asymmetric two-column: orb LEFT, copy RIGHT. Breaks the
   centered-stack cadence shared by "A longer life" above and
   mirrors the asymmetric layout of "You're not average" with the
   columns flipped — the eye zigzags between the page's two
   asymmetric beats (copy-LEFT on problem → copy-RIGHT here)
   instead of reading them as parallel templates. The orb gets
   slightly more column width (6fr vs 5fr) because the projected
   labels need room to breathe; the copy column hugs the
   right edge with a tight measure that lets the title wrap into a
   chunky two-line statement.

   The orb and copy are vertically centered against each other so
   the title's baseline lands at roughly the orb's mid-height —
   reads as "instrument" (state object) paired with its "diagnostic
   header" (copy), not as a chart and a caption. */
.lp-perform-grid {
  display: grid;
  grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
  gap: clamp(32px, 4.5vw, 80px);
  align-items: center;
}

.lp-perform-copy {
  display: flex;
  flex-direction: column;
  gap: 18px;
  align-items: flex-start;
}
.lp-perform-title {
  margin: 0;
  text-align: left;
  text-wrap: balance;
  /* Tight measure forces the headline to wrap into 2–3 chunky
     lines so it reads as a statement, not a caption. */
  max-width: 14ch;
}
.lp-perform-sub {
  margin: 0;
  text-align: left;
  max-width: 36ch;
  text-wrap: balance;
}

/* On narrow viewports, stack: copy above the orb. The copy
   re-centers to harmonize with the page's other small-screen
   header treatments. */
@media (max-width: 880px) {
  .lp-perform-grid {
    grid-template-columns: 1fr;
    gap: clamp(28px, 5vw, 48px);
    align-items: stretch;
  }
  .lp-perform-grid .lp-perform-copy {
    align-items: center;
    order: -1;
  }
  .lp-perform-grid .lp-perform-title,
  .lp-perform-grid .lp-perform-sub {
    text-align: center;
  }
  /* Headline keeps a tight measure (chunky 2–3 lines); the subhead gets
     a wider one so it doesn't read as a pinched column beside the orb. */
  .lp-perform-grid .lp-perform-title { max-width: 22ch; }
  .lp-perform-grid .lp-perform-sub { max-width: 42ch; }
  /* Rein the orb in from full-bleed so the projected labels don't crowd
     the shell on phone and small tablet layouts. */
  .lp-perform-grid .lp-perform-stage {
    max-width: 420px;
    margin-inline: auto;
  }
}

.lp-perform-stage {
  /* Lives in the asymmetric grid alongside `.lp-perform-copy`. Must
     fill its column — earlier `margin: 0 auto` shrank the item to
     the visual's intrinsic width (~300px) in the grid context. */
  width: 100%;
  padding: clamp(8px, 1.5vh, 20px) 0;
}
/* Readiness orb. Replaces the old radar chart with a live 3D state object:
   the user's felt performance dimensions are lobes on one coordinated body,
   not separate chart axes. The canvas owns the organic deformation; the DOM
   owns labels so typography stays crisp and consistent with the landing UI. */
.lp-readiness-orb {
  position: relative;
  aspect-ratio: 1.08;
  width: min(100%, 520px);
  margin-inline: auto;
  isolation: isolate;
  overflow: visible;
}
.lp-readiness-orb-scene {
  position: absolute;
  inset: 11% 10%;
  z-index: 2;
}
.lp-readiness-canvas {
  display: block;
  width: 100%;
  height: 100%;
}
.lp-readiness-aura {
  position: absolute;
  inset: 18% 16%;
  z-index: 1;
  border-radius: 50%;
  background:
    radial-gradient(circle at 43% 40%, rgba(255,248,238,0.54) 0 8%, rgba(255,232,198,0.18) 10% 22%, transparent 48%),
    radial-gradient(circle at 58% 58%, rgba(217,119,6,0.10), transparent 62%),
    radial-gradient(circle at 42% 56%, rgba(225,29,72,0.08), transparent 60%),
    radial-gradient(circle at 60% 36%, rgba(180,83,9,0.08), transparent 58%),
    radial-gradient(circle at 50% 50%, rgba(120,53,15,0.03), transparent 75%);
  filter: blur(2px);
  opacity: 0;
  transform: scale(0.92);
  transition: opacity 900ms ease 220ms, transform 1200ms cubic-bezier(.2,.7,.2,1) 220ms;
}
.lp-perform-stage.is-in-view .lp-readiness-aura {
  opacity: 1;
  transform: scale(1);
}
.lp-readiness-label {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 6;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-width: max-content;
  padding: 9px 12px;
  color: #31536f;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 1.4px;
  opacity: 0;
  border-radius: 999px;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.97), rgba(238,247,255,0.78) 52%, rgba(232,239,255,0.66));
  box-shadow:
    0 14px 30px rgba(30, 64, 96, 0.13),
    0 4px 12px rgba(14, 116, 144, 0.06),
    0 0 0 1px rgba(59, 130, 246, 0.18),
    inset 0 1px 0 rgba(255,255,255,0.95),
    inset 0 -1px 0 rgba(59,130,246,0.08);
  backdrop-filter: blur(10px) saturate(1.18);
  -webkit-backdrop-filter: blur(10px) saturate(1.18);
  text-shadow:
    0 1px 0 rgba(255,255,255,0.9),
    0 0 10px rgba(255,255,255,0.85);
  will-change: transform, opacity;
  transition:
    opacity 420ms ease,
    color 320ms ease,
    background 320ms ease,
    box-shadow 320ms ease,
    filter 320ms ease;
}
.lp-readiness-label svg {
  width: 16px;
  height: 16px;
  color: currentColor;
  opacity: 0.76;
}
.lp-perform-stage.is-in-view .lp-readiness-label {
  opacity: var(--label-opacity, 0.72);
}
.lp-perform-stage.is-in-view .lp-readiness-label.is-front {
  color: #274663;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.98), rgba(224,242,254,0.82) 48%, rgba(219,234,254,0.72));
}
.lp-perform-stage.is-in-view .lp-readiness-label.is-active {
  color: #7c2d12;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.99), rgba(255,237,213,0.90) 48%, rgba(254,205,211,0.80));
  box-shadow:
    0 18px 34px rgba(154,52,18,0.21),
    0 6px 18px rgba(225,29,72,0.12),
    0 0 0 1px rgba(234,88,12,0.34),
    0 0 22px rgba(251,146,60,0.22),
    inset 0 1px 0 rgba(255,255,255,0.95);
  opacity: var(--label-opacity, 1);
  filter: drop-shadow(0 0 10px rgba(255,255,255,0.8));
}
.lp-perform-stage.is-in-view .lp-readiness-label--focus.is-active {
  color: #9a3412;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.99), rgba(255,237,213,0.90) 52%, rgba(254,215,170,0.82));
  box-shadow:
    0 18px 34px rgba(234,88,12,0.20),
    0 0 0 1px rgba(249,115,22,0.34),
    0 0 24px rgba(251,146,60,0.25),
    inset 0 1px 0 rgba(255,255,255,0.95);
}
.lp-perform-stage.is-in-view .lp-readiness-label--energy.is-active {
  color: #854d0e;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.99), rgba(254,243,199,0.90) 48%, rgba(254,215,170,0.82));
  box-shadow:
    0 18px 34px rgba(217,119,6,0.20),
    0 0 0 1px rgba(245,158,11,0.36),
    0 0 24px rgba(251,146,60,0.25),
    inset 0 1px 0 rgba(255,255,255,0.95);
}
.lp-perform-stage.is-in-view .lp-readiness-label--strength.is-active {
  color: #881337;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.99), rgba(255,228,230,0.88) 50%, rgba(254,205,211,0.82));
  box-shadow:
    0 18px 34px rgba(225,29,72,0.18),
    0 0 0 1px rgba(244,63,94,0.32),
    0 0 24px rgba(251,113,133,0.24),
    inset 0 1px 0 rgba(255,255,255,0.95);
}
.lp-perform-stage.is-in-view .lp-readiness-label--recovery.is-active {
  color: #7f1d1d;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.99), rgba(254,226,226,0.88) 48%, rgba(254,202,202,0.80));
  box-shadow:
    0 18px 34px rgba(185,28,28,0.18),
    0 0 0 1px rgba(220,38,38,0.30),
    0 0 24px rgba(248,113,113,0.22),
    inset 0 1px 0 rgba(255,255,255,0.95);
}
.lp-perform-stage.is-in-view .lp-readiness-label--sleep.is-active {
  color: #713f12;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.99), rgba(254,249,195,0.86) 48%, rgba(254,240,138,0.74));
  box-shadow:
    0 18px 34px rgba(202,138,4,0.18),
    0 0 0 1px rgba(234,179,8,0.30),
    0 0 24px rgba(250,204,21,0.20),
    inset 0 1px 0 rgba(255,255,255,0.95);
}
.lp-perform-stage.is-in-view .lp-readiness-label--presence.is-active {
  color: #9f1239;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.99), rgba(252,231,243,0.88) 48%, rgba(251,207,232,0.80));
  box-shadow:
    0 18px 34px rgba(219,39,119,0.18),
    0 0 0 1px rgba(236,72,153,0.32),
    0 0 24px rgba(244,114,182,0.22),
    inset 0 1px 0 rgba(255,255,255,0.95);
}
.lp-perform-stage.is-in-view .lp-readiness-label--calm.is-active {
  color: #78350f;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.99), rgba(255,247,237,0.88) 48%, rgba(253,186,116,0.72));
  box-shadow:
    0 18px 34px rgba(180,83,9,0.18),
    0 0 0 1px rgba(217,119,6,0.30),
    0 0 24px rgba(251,146,60,0.21),
    inset 0 1px 0 rgba(255,255,255,0.95);
}
.lp-perform-stage.is-in-view .lp-readiness-label.is-occluded {
  opacity: 0;
  filter: blur(0.35px);
}
/* Action card — an iOS-style notification banner that floats in front of
   the orb's lower body (not pinned to the bottom edge, where it used to
   sit on top of the recovery/sleep perimeter labels). One card shows at a
   time, synced to the active attribute, sliding down like a real banner. */
.lp-readiness-actions {
  position: absolute;
  left: 50%;
  top: 56%;
  z-index: 7;
  width: min(82%, 300px);
  height: 60px;
  transform: translateX(-50%);
  pointer-events: none;
}
.lp-readiness-action {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 13px;
  border-radius: 17px;
  border: 1px solid rgba(15, 40, 55, 0.08);
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(22px) saturate(1.7);
  -webkit-backdrop-filter: blur(22px) saturate(1.7);
  box-shadow:
    0 24px 48px -20px rgba(15, 40, 55, 0.5),
    0 3px 10px rgba(15, 40, 55, 0.09),
    inset 0 1px 0 rgba(255, 255, 255, 0.9);
  visibility: hidden;
  opacity: 0;
  transform: translateY(-12px) scale(.97);
  /* Opacity and transform MUST end together — if transform outlasts the fade,
     the card (and its crisp Aescle mark) keeps creeping for a beat after it's
     fully opaque, which reads as the icon shifting. Same duration = it lands
     exactly when it stops moving. */
  transition:
    visibility 0s linear 440ms,
    opacity 440ms ease,
    transform 440ms cubic-bezier(.2,.8,.2,1);
}
.lp-perform-stage.is-in-view .lp-readiness-action.is-active {
  visibility: visible;
  opacity: 1;
  transform: translateY(0) scale(1);
  transition-delay: 0s, 0s, 0s;
}
.lp-readiness-action-icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 11px;
  /* Coach app-icon: a saturated cyan tile carrying the Aescle mark in white —
     the sender identity, so each protocol reads as a note from the coach. */
  background: linear-gradient(150deg, #19b4d1 0%, #0a8aac 56%, #0683a4 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.45),
    inset 0 -7px 13px -7px rgba(2, 46, 60, 0.5),
    0 4px 10px -4px rgba(8, 120, 150, 0.55);
  color: #fff;
}
.lp-readiness-action-icon svg { width: 23px; height: 23px; display: block; }
.lp-readiness-action-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1 1 auto;
}
.lp-readiness-action-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
/* Left group: attribute icon + label, kept together so the time floats right. */
.lp-readiness-action-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  flex: 1 1 auto;
}
.lp-readiness-action-attr {
  flex: none;
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
  font-size: 11px;
  line-height: 1;
}
.lp-readiness-action-title,
.lp-readiness-action-time {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 1.1px;
  white-space: nowrap;
}
.lp-readiness-action-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text-secondary);
  text-transform: uppercase;
}
.lp-readiness-action-time {
  flex: none;
  font-weight: 600;
  letter-spacing: 0.5px;
  color: var(--text-dim);
}
.lp-readiness-action-text {
  font-family: var(--font-sans);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-primary);
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1.25;
}
.lp-readiness-battery {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.lp-readiness-battery-shell {
  display: grid;
  grid-template-columns: repeat(5, 3px);
  gap: 2px;
  padding: 2px;
  border: 1px solid currentColor;
  border-radius: 4px;
}
.lp-readiness-battery-shell span {
  width: 3px;
  height: 8px;
  border-radius: 2px;
  background: currentColor;
  opacity: 0.24;
}
.lp-readiness-battery-shell span:nth-child(-n+4) { opacity: 0.88; }
.lp-readiness-label.is-active .lp-readiness-battery-shell span { opacity: 0.95; }
.lp-readiness-battery-nub {
  width: 2px;
  height: 8px;
  border-radius: 0 2px 2px 0;
  background: currentColor;
  opacity: 0.7;
}

@media (max-width: 880px) {
  .lp-readiness-orb {
    width: min(100%, 460px);
  }
}

@media (max-width: 600px) {
  .lp-readiness-orb {
    aspect-ratio: 0.92;
    width: min(100%, 340px);
  }
  .lp-readiness-orb-scene { inset: 10% 5% 13%; }
  .lp-readiness-label {
    gap: 6px;
    padding: 7px 8px;
    font-size: 9px;
    letter-spacing: 1px;
  }
  .lp-readiness-label svg {
    width: 15px;
    height: 15px;
  }
  .lp-readiness-actions {
    top: 55%;
    width: min(88%, 270px);
    height: 56px;
  }
  .lp-readiness-action {
    gap: 9px;
    padding: 8px 11px;
    border-radius: 15px;
  }
  .lp-readiness-action-icon {
    width: 34px;
    height: 34px;
    font-size: 17px;
  }
  .lp-readiness-action-title,
  .lp-readiness-action-time {
    font-size: 9px;
  }
  .lp-readiness-action-text {
    font-size: 11.5px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .lp-readiness-aura,
  .lp-readiness-label,
  .lp-readiness-action {
    transition: none;
  }
}

/* ── Value prop / framework diagram ── */
/* Three stages, left → right:
     - Inputs column: 5 chips (demographics, history, genome, vitals, bloodwork)
     - Aescle engine: a hexagonal processing module that turns inputs into
       a profile. Hex = chip / computational unit; round dots are states.
     - Mortality profile: a framed region containing the threats / targets /
       actions triangle, with the "+ MORE YEARS" payoff chip at its centroid.
       The frame + top-center tab badge are what unify the triangle as a
       single named unit.
   Edges:
     - reveal (engine → profile): horizontal arrow into the frame
     - lever  (targets → threats): the causal claim — targets move threats
     - act    (actions → targets): the user's lever
     - frame  (threats → actions): closes the triangle into a directed cycle
   Per-card hover lights a discrete beat of the diagram via :has().
   Card colors are uniform (brand cyan) — only the diagram carries the
   semantic warm/cool color language. */
.lp-howflow-wrap {
  margin: 0 auto clamp(56px, 8vh, 88px);
  max-width: 1040px;
  padding: clamp(20px, 3vh, 36px) clamp(8px, 2vw, 24px);
  background:
    radial-gradient(ellipse 70% 80% at 50% 50%, rgba(255,255,255,0.6), transparent 80%),
    linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-surface) 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.lp-howflow {
  display: block;
  width: 100%;
  height: auto;
  max-height: 760px;
  font-family: var(--font-mono);
}
/* Input pills (horizontal row across the top of the diagram). */
.lp-flow-input rect {
  fill: var(--bg-panel);
  stroke: var(--border);
  stroke-width: 1;
}
.lp-flow-input text {
  font-size: 11px;
  font-weight: 500;
  fill: var(--text-primary);
  letter-spacing: 0.3px;
}

/* Convergent inputs → profile edges (idle: gradient stroke) */
.lp-flow-converge path { transition: stroke 240ms ease; }

/* Aescle engine — a hexagonal processing module and the visual focal
   point of the diagram. Hex shape reads as a chip / computational unit;
   the inner hex + vertex registration ticks sell the "machined
   instrument" feel. Label is the focal text; the pillar glyphs (bars /
   dots / lines) sit beneath as a quiet sub-row. */
.lp-engine-hex-outer {
  fill: #ffffff;
  stroke: #0891b2;
  stroke-width: 1.7;
  stroke-linejoin: round;
  transition: stroke-width 240ms ease, filter 240ms ease;
}
.lp-engine-hex-inner {
  fill: none;
  stroke: #0891b2;
  stroke-opacity: 0.34;
  stroke-width: 1.1;
  stroke-linejoin: round;
}
.lp-engine-ticks circle {
  fill: #0891b2;
}
.lp-engine-logo-ring {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.4;
  stroke-opacity: 0.55;
}
.lp-engine-logo-dot {
  fill: var(--accent);
}
.lp-engine-line1,
.lp-engine-line2 {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 2px;
  fill: var(--accent);
}
.lp-engine-glyphs {
  color: #0891b2;
  opacity: 0.55;
  pointer-events: none;
}
.lp-engine-glyph rect,
.lp-engine-glyph circle {
  fill: currentColor;
}
.lp-engine-glyph line {
  stroke: currentColor;
  stroke-width: 1.1;
  stroke-linecap: round;
}

/* Evidence spec block — technical-drawing title block annotating
   the engine with the peer-reviewed literature it draws from.
   Sits beside the engine, outside the top-to-bottom data flow;
   a hairline connector terminates at the east hex vertex with
   the existing cyan trunk arrowhead, reading as evidence flowing
   INTO the engine. Uses the same typographic vocabulary as the
   input pills above (monospace, 600 weight uppercase title) so
   it sits cleanly inside the diagram's UI grammar. */
.lp-flow-evidence {
  pointer-events: none;
  transition: opacity 240ms ease;
}
/* The mirrored YOUR DATA card (see landing.js). Hidden everywhere by
   default — including the desktop hero — and switched on only in the
   exploded step-2 band (≤880 rule below). Its rects/text reuse the
   .lp-evidence-* classes, so styling + the active-02 tint + the ≤600
   type bumps all apply without duplication. */
.lp-flow-userdata {
  display: none;
  pointer-events: none;
  transition: opacity 240ms ease;
}
.lp-evidence-wire {
  stroke: color-mix(in srgb, var(--text-dim) 72%, transparent);
  stroke-width: 1.1;
  fill: none;
  transition: stroke 240ms ease, stroke-opacity 240ms ease;
}
.lp-evidence-frame {
  fill: var(--bg-panel);
  stroke: var(--border);
  stroke-width: 1;
  stroke-opacity: 0.55;
  transition: stroke 240ms ease, fill 240ms ease, stroke-opacity 240ms ease;
}
.lp-evidence-frame-back {
  fill: var(--bg-panel);
  stroke: var(--border);
  stroke-width: 1;
  stroke-opacity: 0.55;
  transition: stroke 240ms ease, fill 240ms ease, stroke-opacity 240ms ease;
}
.lp-evidence-title {
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.9px;
  fill: var(--text-dim);
  text-transform: uppercase;
  transition: fill 240ms ease;
}
.lp-evidence-cite {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.2px;
  fill: var(--text-primary);
}
.lp-evidence-detail {
  font-size: 8.5px;
  font-weight: 500;
  letter-spacing: 0.2px;
  fill: var(--text-dim);
}
.lp-evidence-more {
  font-size: 7.5px;
  font-weight: 500;
  letter-spacing: 0.2px;
  fill: var(--text-dim);
}
/* YOUR DATA pillar rows — all equal weight (no anchored "primary" input).
   Carry the header color + semibold so the five rows read as one confident
   block, balancing the visual weight of the literature card's bold cite +
   stacked frame — WITHOUT singling out any single input. */
.lp-userdata-item {
  font-size: 8px;
  font-weight: 600;
  letter-spacing: 0.2px;
  fill: var(--text-secondary);
}

/* Engine "processing" motion — keyframes only; applied in the card 02
   block below so the engine animates only while step 2 is the active
   beat (the moment the engine is the focal point). Three coordinated
   cues: dashed energy flow along the inner hex, cycling pillar
   glyphs, and a cyan glow on the outer hex. */
@keyframes lp-engine-hex-flow {
  to { stroke-dashoffset: -8; }
}
@keyframes lp-engine-glyph-pulse {
  0%, 70%, 100% { opacity: 0.4; }
  20%, 45%      { opacity: 1; }
}
@keyframes lp-engine-hex-glow {
  0%, 100% { filter: drop-shadow(0 0 0 rgba(8, 145, 178, 0)); }
  50%      { filter: drop-shadow(0 0 14px rgba(8, 145, 178, 0.45)); }
}
/* Threats vertex — slow heartbeat while card 02 is active. Paired
   with the engine's faster ring/glow: engine reads as mechanical
   processing (0.7s linear dashes), threats reads as a living output
   (2.4s ease-in-out breath). Halo expands + brightens; dot nudges
   in sympathy so the whole vertex breathes as one. */
@keyframes lp-threats-halo-pulse {
  0%, 100% { r: 27; fill: rgba(220, 38, 38, 0.08); }
  50%      { r: 30; fill: rgba(220, 38, 38, 0.13); }
}
@keyframes lp-threats-dot-pulse {
  0%, 100% { r: 9.5;  filter: drop-shadow(0 0 0 rgba(220, 38, 38, 0)); }
  50%      { r: 10;   filter: drop-shadow(0 0 3px rgba(220, 38, 38, 0.32)); }
}

/* Mortality profile frame — wraps the triangle composition as a single
   named region. Subtle surface fill + a 1px border define the container;
   the pill badge is bisected by the frame's top edge so it reads as a
   panel title (fieldset-legend style). */
.lp-profile-frame {
  fill: rgba(8, 145, 178, 0.025);
  stroke: var(--border);
  stroke-width: 1;
  transition: stroke 240ms ease, fill 240ms ease, stroke-opacity 240ms ease;
}

/* Shared pill styling — used for both the YOUR HEALTH DATA pill above
   the inputs column and the MORTALITY PROFILE pill bisecting the
   profile frame's top edge. Identical dimensions / typography give the
   two columns matching headers. */
.lp-flow-pill-bg {
  fill: var(--bg-panel);
  stroke: var(--border);
  stroke-width: 1;
  transition: fill 240ms ease, stroke 240ms ease, stroke-opacity 240ms ease, filter 240ms ease;
}
.lp-flow-pill-text {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 2px;
  fill: var(--text-secondary);
  text-transform: uppercase;
  transition: fill 240ms ease, font-weight 240ms ease;
}

/* All four edges (reveal + three triangle sides) share at-rest styling.
   Bumped from 55% to 72% text-dim so the triangle closes visibly at
   rest, especially the long horizontal base which used to vanish into
   the page. */
.lp-flow-edge {
  stroke: color-mix(in srgb, var(--text-dim) 72%, transparent);
  stroke-width: 1.4;
  fill: none;
  transition: stroke 240ms ease, stroke-width 240ms ease, stroke-opacity 240ms ease;
}

/* Engine → profile reveal edge inherits the cyan tint of the incoming
   converge trunk so the engine's input and output read as the same
   flow. Matches the converge arrowhead's #0891b2 @ 0.7. */
.lp-flow-edge[data-edge="reveal"] {
  stroke: #0891b2;
  stroke-opacity: 0.7;
}

/* Vertex visuals — halo + dot + uppercase label + tiny example list */
.lp-flow-vertex .v-halo {
  fill: transparent;
  transition: r 280ms ease, fill 280ms ease;
}
.lp-flow-vertex[data-vertex="threats"] .v-halo { fill: rgba(220, 38, 38, 0.08); }
.lp-flow-vertex[data-vertex="targets"] .v-halo { fill: rgba(217, 119, 6, 0.10); }
.lp-flow-vertex[data-vertex="actions"] .v-halo { fill: rgba(22, 163, 74, 0.10); }
.lp-flow-vertex .v-dot { transition: r 280ms ease; }
.lp-flow-vertex .v-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 2px;
  fill: var(--text-secondary);
  text-transform: uppercase;
  transition: fill 200ms ease;
}
/* Payoff chip — the profile's output. Anchored at the triangle's
   centroid so the cycle (threats / targets / actions) runs around it.
   Text is green-700 against a faint green-tinted fill so the chip
   reads as the diagram's prize without losing legibility. The cycle
   itself is the centerpiece; the chip is the result, not the hero. */
.lp-payoff-pill {
  fill: rgba(22, 163, 74, 0.12);
  stroke: #16a34a;
  stroke-opacity: 0.7;
  stroke-width: 1.4;
  transition: stroke-opacity 240ms ease, fill 240ms ease, filter 240ms ease;
}
.lp-payoff-text {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 1.8px;
  fill: #15803d;
  transition: fill 240ms ease;
}

/* Opacity transition for every role-tagged element so dim-others is
   smooth across all card-hover states. */
.lp-howflow [data-role] { transition: opacity 280ms ease; }

/* Reverse-hover hit-zones: transparent SVG rects above the three
   diagram regions that share the same data-card attribute as the
   value-prop cards below. Hovering either side fires the same :has()
   rules — diagram-hover and card-hover are now interchangeable. */
.lp-flow-hitzone {
  fill: transparent;
  pointer-events: all;
}

/* ─── Card-driven hover (CSS-only via :has()) ─────────────────────── */
/* Each beat lights a discrete moment of the diagram, triggered by
   either hovering the matching value-prop card OR hovering the
   matching diagram region (the hit-zones above). All selectors target
   [data-card="X"]:hover unqualified, so both sources work:
     01 (SEE):  inputs converge into the engine. Nothing past the engine.
     02 (FIND): engine → profile. The whole profile lights as the
                "thing delivered"; payoff stays dim — that's card 03's job.
     03 (ACT):  the profile's internal cycle. Engine + reveal drop out,
                triangle perimeter animates, all three vertices light,
                payoff chip pulses + brightens. */
@keyframes lp-flow-dash { to { stroke-dashoffset: -22; } }
/* Soft outer halo only — never darkens the pill's fill, so the
   "+ MORE YEARS" text keeps its contrast against the chip during the
   pulse. (Earlier version brightened the fill on hover, which sank
   the green text into a greener background.) */
@keyframes lp-payoff-glow {
  0%, 100% { filter: drop-shadow(0 0 4px rgba(22, 163, 74, 0.18)); }
  50%      { filter: drop-shadow(0 0 16px rgba(22, 163, 74, 0.55)); }
}

/* ── Dim rules (per card) ── */
/* Card 01: engine is the destination — everything downstream of it
   fades back. The user reads "inputs become a single engine input." */
.lp-value[data-active="01"] [data-role="reveal"],
.lp-value[data-active="01"] [data-role="profile-frame"],
.lp-value[data-active="01"] [data-role="lever"],
.lp-value[data-active="01"] [data-role="act"],
.lp-value[data-active="01"] [data-role="frame"],
.lp-value[data-active="01"] [data-role="threats"],
.lp-value[data-active="01"] [data-role="targets"],
.lp-value[data-active="01"] [data-role="actions"],
.lp-value[data-active="01"] [data-role="payoff"],
.lp-value[data-active="01"] [data-role="evidence"] {
  opacity: 0.22;
}

/* Card 02: the reveal beat. The engine emits the profile and threats
   is the headline output (ranked list of what's actually shortening
   your life). Frame + threats vertex + triangle edges stay lit;
   targets / actions vertices dim with the payoff so threats is the
   single focal point inside the cycle. Card 03 brings the targets /
   actions vertices back as the levers and the things that move them. */
.lp-value[data-active="02"] [data-role="input"],
.lp-value[data-active="02"] [data-role="converge"],
.lp-value[data-active="02"] [data-role="payoff"],
.lp-value[data-active="02"] [data-role="targets"],
.lp-value[data-active="02"] [data-role="actions"] {
  opacity: 0.22;
}
/* Triangle edges also recede on card 02 — the profile container and
   the threats vertex inside it are the focal points; the cycle's
   internal wiring is card 03's job. Slightly less dim than the
   vertices (0.32 vs 0.22) so the triangle still reads as a
   continuous boundary around threats rather than three orphaned
   stubs anchored to the lit vertex. */
.lp-value[data-active="02"] [data-edge="lever"],
.lp-value[data-active="02"] [data-edge="act"],
.lp-value[data-active="02"] [data-edge="frame"] {
  opacity: 0.32;
}

/* Card 03: the profile's internal cycle. Engine + reveal drop out —
   by this beat the user is inside the profile, not entering it. */
.lp-value[data-active="03"] [data-role="input"],
.lp-value[data-active="03"] [data-role="converge"],
.lp-value[data-active="03"] [data-role="engine"],
.lp-value[data-active="03"] [data-role="reveal"],
.lp-value[data-active="03"] [data-role="evidence"] {
  opacity: 0.22;
}

/* ── Card 01 (SEE YOUR RISKS) ── */
/* Inputs animate into the engine. No reveal edge, no triangle lighting. */
.lp-value[data-active="01"] .lp-flow-converge path {
  stroke: #0891b2;
  stroke-opacity: 0.85;
  stroke-dasharray: 5 7;
  animation: lp-flow-dash 1.6s linear infinite;
}

/* ── Card 02 (FIND WHAT MOVES) ── */
/* Reveal edge animates from engine → profile, and the profile frame
   border picks up a cyan tint to mark it as the destination. No
   single-vertex emphasis — the whole profile reads as the thing
   being delivered. */
.lp-value[data-active="02"] [data-edge="reveal"] {
  stroke: #0891b2;
  stroke-width: 1.7;
  stroke-dasharray: 5 6;
  animation: lp-flow-dash 1.4s linear infinite;
}
.lp-value[data-active="02"] .lp-profile-frame {
  stroke: #0891b2;
  stroke-opacity: 0.55;
  fill: rgba(8, 145, 178, 0.045);
}
/* LONGEVITY PROFILE legend reads as the region's name on this beat —
   cyan-tinted pill, deeper text color, soft outer glow. The frame
   above tints the region; the pill labels what just arrived. */
.lp-value[data-active="02"] .lp-profile-tab .lp-flow-pill-bg {
  fill: var(--bg-panel);
  stroke: #0891b2;
  stroke-opacity: 0.75;
  filter: drop-shadow(0 0 8px rgba(8, 145, 178, 0.25));
}
.lp-value[data-active="02"] .lp-profile-tab .lp-flow-pill-text {
  fill: #0e7490;
  font-weight: 700;
}
/* Engine reads as "actively processing" while this beat is active.
   Three coordinated motions: dashed energy flow along the inner hex,
   cycling pillar glyphs, and a cyan glow on the outer hex. Engine is
   otherwise still at rest. */
.lp-value[data-active="02"] .lp-engine-hex-inner {
  stroke-dasharray: 4 4;
  stroke-opacity: 0.55;
  animation: lp-engine-hex-flow 0.7s linear infinite;
}
.lp-value[data-active="02"] .lp-engine-glyph {
  animation: lp-engine-glyph-pulse 3.6s ease-in-out infinite;
}
.lp-value[data-active="02"] .lp-engine-glyph[data-pillar="actuarial"] { animation-delay: 0s; }
.lp-value[data-active="02"] .lp-engine-glyph[data-pillar="cohort"]    { animation-delay: 1.2s; }
.lp-value[data-active="02"] .lp-engine-glyph[data-pillar="clinical"]  { animation-delay: 2.4s; }
.lp-value[data-active="02"] .lp-engine-hex-outer {
  animation: lp-engine-hex-glow 2.4s ease-in-out infinite;
}
/* Evidence spec block participates in the engine beat — the
   frame border + connector wire pick up a cyan tint, matching
   how the LONGEVITY PROFILE frame tints below. Reinforces that
   the literature is part of the engine's footprint without
   pulling focus from the hex itself. */
.lp-value[data-active="02"] .lp-evidence-frame,
.lp-value[data-active="02"] .lp-evidence-frame-back {
  stroke: #0891b2;
  stroke-opacity: 0.55;
}
.lp-value[data-active="02"] .lp-evidence-wire {
  stroke: #0891b2;
  stroke-opacity: 0.7;
}
.lp-value[data-active="02"] .lp-evidence-title {
  fill: #0e7490;
}
/* Threats is the engine's headline output: the ranked list of what
   actually shortens your life. Emphasize the threats vertex alone so
   step 2 has a discrete focal point (parallel to card 03's actions
   emphasis). Targets/actions stay at rest — their moment is card 03. */
.lp-value[data-active="02"] [data-vertex="threats"] .v-dot {
  animation: lp-threats-dot-pulse 2.4s ease-in-out infinite;
}
.lp-value[data-active="02"] [data-vertex="threats"] .v-halo {
  animation: lp-threats-halo-pulse 2.4s ease-in-out infinite;
}
.lp-value[data-active="02"] [data-vertex="threats"] .v-label { fill: var(--text-primary); }

/* ── Card 03 (TAKE ACTION) ── */
/* All three triangle edges animate as a single clockwise perimeter loop:
   lever (targets→threats), frame (threats→actions), act (actions→targets)
   are each drawn in clockwise direction, so a synced dash animation reads
   as dashes chasing around the triangle. All three vertices register;
   the payoff chip pulses as the cycle's downstream output, and its text
   darkens for extra punch. */
.lp-value[data-active="03"] [data-edge="lever"],
.lp-value[data-active="03"] [data-edge="act"],
.lp-value[data-active="03"] [data-edge="frame"] {
  stroke: #16a34a;
  stroke-width: 1.7;
  stroke-dasharray: 5 6;
  animation: lp-flow-dash 1.4s linear infinite;
}
.lp-value[data-active="03"] [data-vertex="actions"] .v-dot { r: 10; }
.lp-value[data-active="03"] [data-vertex="actions"] .v-halo { r: 28; }
.lp-value[data-active="03"] [data-vertex="actions"] .v-label { fill: var(--text-primary); }
.lp-value[data-active="03"] [data-vertex="targets"] .v-halo { r: 26; }
.lp-value[data-active="03"] [data-vertex="targets"] .v-label { fill: var(--text-primary); }
.lp-value[data-active="03"] [data-vertex="threats"] .v-halo { r: 26; }
.lp-value[data-active="03"] [data-vertex="threats"] .v-label { fill: var(--text-primary); }
.lp-value[data-active="03"] .lp-payoff-pill {
  stroke-opacity: 1;
  stroke-width: 1.7;
  animation: lp-payoff-glow 1.6s ease-in-out infinite;
}
.lp-value[data-active="03"] .lp-payoff-text {
  fill: #14532d;
}


/* ── Diagram + accordion (side-by-side coordinated view) ──
   The diagram sits on the left; an accordion-style step list sits on
   the right. Each row in the aside collapses to its title; the active
   row expands inline to reveal body copy. Hovering or clicking a row
   — or any of the diagram's three hit-zones — sets the active beat
   on the parent section, which CSS uses to (a) highlight the matching
   diagram zone and (b) expand the matching row's body.

   The expand/collapse uses the `grid-template-rows: 0fr → 1fr` trick
   so the body height animates from its natural rendered height — no
   measured max-height, no JS height syncing. */
/* The diagram is vertically oriented (480×660 viewBox), so it fits in
   a narrow column without compressing labels. The section uses the
   default 1080px shell; the diagram column is capped at 460px while the
   accordion claims the remaining width via 1fr — wider rows balance the
   diagram's vertical mass and give the body copy room to breathe. */
.lp-howflow-pair {
  display: grid;
  grid-template-columns: minmax(0, 460px) minmax(360px, 1fr);
  gap: clamp(24px, 4vw, 64px);
  align-items: center;
  justify-content: center;
}
/* Inside the pair, the diagram wrapper drops its standalone-block
   styling: no bottom margin (the section handles spacing), no
   max-width (the grid column controls width), tighter padding. */
.lp-howflow-pair > .lp-howflow-wrap {
  margin: 0;
  max-width: none;
  padding: clamp(16px, 2vh, 28px) clamp(8px, 1.5vw, 20px);
}

/* Aside (accordion column) */
.lp-howflow-aside {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}
.lp-howflow-row {
  border-top: 1px solid var(--border);
  transition: border-color 240ms ease;
}
.lp-howflow-row:last-child {
  border-bottom: 1px solid var(--border);
}
/* Per-step diagram band (mobile only). Built + injected by setupHowflow as
   a viewBox-cropped clone of the hero; hidden on desktop, where the full
   hero diagram beside the accordion is the showpiece. */
.lp-howflow-row-frag {
  display: none;
}
.lp-howflow-row-head {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  background: transparent;
  border: 0;
  padding: 54px 4px;
  cursor: pointer;
  text-align: left;
  font-family: var(--font-mono);
  color: var(--text-dim);
  transition:
    color 220ms ease,
    padding-top 360ms cubic-bezier(.2, .7, .2, 1),
    padding-bottom 360ms cubic-bezier(.2, .7, .2, 1);
}
.lp-howflow-row-head:hover { color: var(--text-secondary); }
.lp-howflow-row-head:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent);
  outline-offset: 2px;
  border-radius: 6px;
}
.lp-howflow-row-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-panel);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.5px;
  color: var(--text-dim);
  flex-shrink: 0;
  transition:
    background 240ms ease,
    border-color 240ms ease,
    color 240ms ease,
    box-shadow 240ms ease;
}
.lp-howflow-row-title {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
}
/* Thematic glyph leading each step title — scan / crosshair / trend-up.
   Sits between the number badge and the title. Inherits the head's color,
   so it animates dim → cyan with the active state. Body text is re-aligned
   under the title below to account for icon + gap. */
.lp-howflow-row-icon {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  color: inherit;
  opacity: 0.7;
  transition: opacity 240ms ease;
}
.lp-howflow-row-icon svg {
  display: block;
  width: 18px;
  height: 18px;
}
.lp-howflow-row-head:hover .lp-howflow-row-icon { opacity: 1; }
.lp-value[data-active="01"] .lp-howflow-row[data-card="01"] .lp-howflow-row-icon,
.lp-value[data-active="02"] .lp-howflow-row[data-card="02"] .lp-howflow-row-icon,
.lp-value[data-active="03"] .lp-howflow-row[data-card="03"] .lp-howflow-row-icon {
  opacity: 1;
}
.lp-howflow-row-body-wrap {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 360ms cubic-bezier(.2, .7, .2, 1);
}
.lp-howflow-row-body-wrap > .lp-howflow-row-body {
  overflow: hidden;
  margin: 0;
  padding: 0 4px 0 78px; /* align body under title (4 head pad + 28 num + 14 gap + 18 icon + 14 gap) */
  font-family: var(--font-sans);
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.58;
  letter-spacing: -0.01em;
  color: var(--text-secondary);
  opacity: 0;
  transform: translateY(-4px);
  transition:
    opacity 320ms ease 80ms,
    transform 320ms ease 80ms,
    padding-bottom 360ms cubic-bezier(.2, .7, .2, 1);
}

/* Active row: number badge fills cyan, title brightens, body expands. */
.lp-value[data-active="01"] .lp-howflow-row[data-card="01"],
.lp-value[data-active="02"] .lp-howflow-row[data-card="02"],
.lp-value[data-active="03"] .lp-howflow-row[data-card="03"] {
  border-top-color: color-mix(in srgb, var(--accent) 35%, var(--border));
}
.lp-value[data-active="01"] .lp-howflow-row[data-card="01"] + .lp-howflow-row,
.lp-value[data-active="02"] .lp-howflow-row[data-card="02"] + .lp-howflow-row,
.lp-value[data-active="03"] .lp-howflow-row[data-card="03"] + .lp-howflow-row {
  border-top-color: color-mix(in srgb, var(--accent) 35%, var(--border));
}
.lp-value[data-active="01"] .lp-howflow-row[data-card="01"] .lp-howflow-row-head,
.lp-value[data-active="02"] .lp-howflow-row[data-card="02"] .lp-howflow-row-head,
.lp-value[data-active="03"] .lp-howflow-row[data-card="03"] .lp-howflow-row-head {
  color: var(--accent);
}
.lp-value[data-active="01"] .lp-howflow-row[data-card="01"] .lp-howflow-row-num,
.lp-value[data-active="02"] .lp-howflow-row[data-card="02"] .lp-howflow-row-num,
.lp-value[data-active="03"] .lp-howflow-row[data-card="03"] .lp-howflow-row-num {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent);
}
.lp-value[data-active="01"] .lp-howflow-row[data-card="01"] .lp-howflow-row-body-wrap,
.lp-value[data-active="02"] .lp-howflow-row[data-card="02"] .lp-howflow-row-body-wrap,
.lp-value[data-active="03"] .lp-howflow-row[data-card="03"] .lp-howflow-row-body-wrap {
  grid-template-rows: 1fr;
}
.lp-value[data-active="01"] .lp-howflow-row[data-card="01"] .lp-howflow-row-head,
.lp-value[data-active="02"] .lp-howflow-row[data-card="02"] .lp-howflow-row-head,
.lp-value[data-active="03"] .lp-howflow-row[data-card="03"] .lp-howflow-row-head {
  padding-top: 76px;
  padding-bottom: 18px;
}
.lp-value[data-active="01"] .lp-howflow-row[data-card="01"] .lp-howflow-row-body,
.lp-value[data-active="02"] .lp-howflow-row[data-card="02"] .lp-howflow-row-body,
.lp-value[data-active="03"] .lp-howflow-row[data-card="03"] .lp-howflow-row-body {
  opacity: 1;
  transform: translateY(0);
  padding-bottom: 76px;
}

/* The last row has its own bottom border (no row-after to recolor),
   so highlight it directly when active. */
.lp-value[data-active="03"] .lp-howflow-row[data-card="03"]:last-child {
  border-bottom-color: color-mix(in srgb, var(--accent) 35%, var(--border));
}

/* Below the side-by-side breakpoint, stack the pair vertically: the
   diagram sits full-width above the accordion. The diagram still reads
   as the continuous top-to-bottom flow — it's not hidden. */
@media (max-width: 1080px) {
  .lp-howflow-pair {
    grid-template-columns: 1fr;
    align-items: stretch;
  }
}

/* ═══ Mobile: exploded diagram ═══
   Below 880 the single full diagram, stacked above the step text, loses the
   context that made it legible — by the time you scroll to the steps, the
   picture is off-screen. Replace it with per-step bands: each step shows the
   slice of the diagram it describes (cloned + viewBox-cropped from the hero
   in setupHowflow), so text + visual travel together and the downward scroll
   reads as the inputs → engine → profile pipeline. The hero hides, the
   accordion stops collapsing (every step stays open beside its band), and
   the auto-cycle is suppressed in JS. Desktop is untouched. */
@media (max-width: 880px) {
  /* The bands carry the diagram now — retire the full hero. */
  .lp-howflow-pair > .lp-howflow-wrap {
    display: none;
  }

  /* De-box the steps: the per-row dividers are what chop the flow into three
     separate cards (and what the old connector awkwardly crossed). Without
     them the section reads as one continuous column. */
  .lp-howflow-row,
  .lp-howflow-row:last-child {
    border-top: 0;
    border-bottom: 0;
  }

  /* Each step's band, injected above its head. Grouping: the image hugs its
     OWN text below (tiny bottom pad) and is held off from the previous step
     by a generous top moat — so as you scroll it's unambiguous which text a
     band belongs to. */
  .lp-howflow-row-frag {
    display: block;
    position: relative;
    padding: 12px 0 0;
  }
  .lp-howflow-row + .lp-howflow-row .lp-howflow-row-frag {
    padding-top: 64px;
  }
  .lp-howflow-frag-svg {
    display: block;
    width: 100%;
    height: auto;
    font-family: var(--font-mono);
    overflow: hidden; /* clip to the cropped viewBox — this IS the band slice */
    pointer-events: none; /* aria-hidden flavour — taps pass through to scroll */
  }

  /* Step 2 dual-feed: bring in the mirrored YOUR DATA card so the literature
     card on the left has a counterweight on the right (the top inputs that
     balanced it on desktop are cropped into step 1). Reads as: your data +
     clinical research → engine → profile. Hidden by default; shown only here. */
  .lp-howflow-row[data-card="02"] .lp-flow-userdata { display: block; }

  /* Step 2 only: soft-fade the tail below threats (the empty lower half of the
     profile card) so the band ends cleanly, without fading the engine's halo
     at the top or the threats node itself. Step 1 (full engine + halo) and
     step 3 (the self-contained profile card) are real edges and stay solid. */
  .lp-howflow-row[data-card="02"] .lp-howflow-frag-svg {
    -webkit-mask-image: linear-gradient(
      180deg,
      #000 0,
      #000 90%,
      transparent 100%
    );
    mask-image: linear-gradient(180deg, #000 0, #000 90%, transparent 100%);
  }

  /* Step 3: let LONGEVITY PROFILE and "+ MORE YEARS" carry the emphasis —
     dial the three vertex labels back in both weight (dim, holding against
     the active-state brightening) and size (smaller than the ≤600 bump). */
  .lp-howflow-row[data-card="03"] .v-label {
    fill: var(--text-dim) !important;
    font-size: 12px;
    letter-spacing: 1px;
  }

  /* Every step stays open: band + title + body read as one unit, top to
     bottom. The active-row growth/collapse choreography is desktop-only and
     the cycle is suppressed here, but force the open state so a stale
     data-active (e.g. left over from a resize off desktop) can't reflow. */
  .lp-howflow-row-head {
    padding: 30px 4px 16px !important;
    cursor: default;
  }
  .lp-howflow-row-body-wrap {
    grid-template-rows: 1fr !important;
  }
  /* Darker, stronger step titles — they're the scan anchors on mobile, so
     lift them from the dim at-rest color to the header color. */
  .lp-howflow-row-title {
    color: var(--text-secondary);
  }
  /* Drop the desktop body indent (78px, aligning under the title): on a phone
     it just strands a third of the width as left whitespace. Run the copy
     near full-width instead. */
  .lp-howflow-row-body-wrap > .lp-howflow-row-body {
    opacity: 1 !important;
    transform: none !important;
    padding-left: 4px;
    padding-bottom: 26px !important;
  }

  /* Pin every band part to full opacity so a partially-scrolled neighbour
     band never flashes dim as the active step changes. Animation is NOT
     killed — the band-flow observer drives data-active by scroll, and the
     diagram's own per-step animation (converge stream, engine pulse, triangle
     flow) is exactly what we want playing on the centred band. */
  .lp-howflow-row-frag [data-role],
  .lp-howflow-row-frag [data-edge],
  .lp-howflow-row-frag [data-vertex] {
    opacity: 1 !important;
  }
}

/* Phone tier. The diagram is a fixed 480-wide viewBox; at phone width
   it scales to ~0.7, and SVG <text> scales WITH the canvas — so the
   labels that read fine on desktop render at ~7–9px here. We don't
   restructure the (already vertical, already continuous) flow; we just
   counter-scale the type back up in user-units and drop the one block
   too dense to survive the shrink.
     - Labels bumped ~1.4–1.6× so they land readable on a real phone.
     - Evidence title-block hidden: at ~5–6px it's illegible, it's the
       most expendable annotation (credibility flavor, not the story),
       and removing it frees the left margin so the spine sits centered.
   Tune the multipliers against an actual device if they read large in
   the 480–600px band — phones (360–430px) are the target.

   Two constraints cap how far each label can grow:
     - Pills are fixed-width SVG rects (input chips 140u, LONGEVITY
       PROFILE 160u). "Family history" / "LONGEVITY PROFILE" are the
       tight strings — so the boxed labels stay conservative and shed
       letter-spacing to buy back horizontal room.
     - Free-standing labels (THREATS/TARGETS/ACTIONS, AESCLE ENGINE)
       have no container, so they take the bigger bump. */
@media (max-width: 600px) {
  .lp-flow-input text { font-size: 14px; }
  .lp-flow-vertex .v-label { font-size: 16px; letter-spacing: 1.2px; }
  .lp-flow-pill-text { font-size: 13px; letter-spacing: 0.6px; }
  .lp-engine-line1,
  .lp-engine-line2 { font-size: 18px; letter-spacing: 1.4px; }
  .lp-payoff-text { font-size: 14px; letter-spacing: 1px; }
  .lp-flow-evidence { display: none; }
  /* …but bring the LITERATURE card BACK in step 2's band. The exploded
     per-step layout gives the citation the room the cramped full diagram
     never had — and "backed by peer-reviewed research" is exactly step 2's
     claim. Scoped to card 02 (it's stripped from card 01's clone in JS), and
     counter-scaled like the other phone labels so the cite reads. It still
     picks up the cyan engine-beat tint from .lp-value[data-active="02"] as
     the band centers. */
  .lp-howflow-row[data-card="02"] .lp-flow-evidence { display: block; }
  .lp-howflow-row[data-card="02"] .lp-evidence-title { font-size: 11px; }
  .lp-howflow-row[data-card="02"] .lp-evidence-cite { font-size: 13px; }
  .lp-howflow-row[data-card="02"] .lp-evidence-detail { font-size: 10px; }
  .lp-howflow-row[data-card="02"] .lp-evidence-more { font-size: 9px; }
  .lp-howflow-row[data-card="02"] .lp-userdata-item { font-size: 10px; }
  /* The triangle's base labels (TARGETS / ACTIONS) crowd the profile
     frame's border once they're bumped to 16px. Grow the frame outward
     — sides + bottom — for breathing room. Top stays at y=520 so the
     LONGEVITY PROFILE pill keeps bisecting the top edge; the new bottom
     (754) clears the viewBox (758) so the border isn't re-clipped.
     (x / width / height are CSS-settable SVG geometry properties.) */
  .lp-profile-frame { x: 28px; width: 424px; height: 234px; }
  /* Lower the APEX only. The frame's extra height landed at the bottom,
     where the base labels already use it (their margin is correct and
     must stay put). The crowding is purely at the top — the 16px THREATS
     label sits jammed under the LONGEVITY PROFILE pill. So drop THREATS
     (and the payoff + the two edges that touch it) down ~10u to balance
     the top margin against the bottom one, while GOALS / ACTIONS and the
     base edge stay exactly where they were. d is CSS-settable, so the
     apex-side edge endpoints follow (575→585 ⇒ 582.07→592.07). */
  .lp-flow-triangle [data-vertex="threats"] { transform: translate(240px, 585px); }
  .lp-flow-triangle [data-edge="lever"] { d: path("M117.07 697.93 L232.93 592.07"); }
  .lp-flow-triangle [data-edge="frame"] { d: path("M247.07 592.07 L362.93 697.93"); }
  .lp-flow-triangle [data-role="payoff"] { transform: translate(240px, 665px); }
}
/* ── Mortality beat ──
   The somber existential one-two punch right after the hero. Two
   stacked beats, each occupying nearly its own viewport so the
   scroll-in lands as a dramatic page turn rather than a paragraph
   transition. Background is a deep slate band that tonally breaks
   from the bright page above; a soft accent vignette warms the
   second beat's region without breaking the band's continuity. */
/* Threshold-driven scrolljack. The section is transparent (no bg, no
   border) so the stage's dark backdrop is the *only* dark surface —
   that lets it fade in/out at the section's edges, dissolving into
   the page bg instead of having a hard section edge cross the
   viewport. */
.lp-mortality {
  position: relative;
  height: 200vh;
  padding: 0;
  /* Intentionally NO `overflow: hidden` here: that would make this
     section a CSS scroll container, which scopes the inner sticky
     stage to the section (which doesn't actually scroll) instead of
     the `.landing` scroll container outside. The stage handles its
     own clipping.

     Height is 200vh = 100vh sticky stage + 100vh of section travel.
     Sticky releases at p = (200-100)/200 = 0.50. Inside the section,
     JS toggles three state classes as scroll crosses each threshold:

         vh after pin → state class    → trigger p
              0vh     → .is-beat-1     = 0.000   (fires at pin)
             20vh     → .is-beat-2     = 0.100
             40vh     → .is-aescle     = 0.200
            100vh     → (release)      = 0.500

     Beat 1 fires the moment the stage pins so the user never lands
     on a stretch of empty dark. Threshold gaps are tight (20vh each)
     because pacing is enforced by FORCED DWELL — JS clamps scrollTop
     at each state's exit threshold until that beat's fade-in plays
     through (see `DWELL_MS` in landing.js: 1500ms / 2800ms / 3000ms
     for states 1 / 2 / 3). Fast scroll or trackpad inertia hits the
     clamp and stops. Sequential state advance means beats can't be
     skipped. Total forced pause from pin to release: ~7.3s, after
     which the user is freed and the section unpins on the next
     scroll. The fades themselves are CSS-timed (see state-class
     rules below), NOT scroll-scrubbed. Forward-only: scrolling back
     up inside the section does NOT un-activate a beat; state resets
     only when the section drops back below the fold. The backdrop
     fades in scroll-driven during entry (before pin), so the dark
     band rises from the page bg before any beat lands. The top nav
     fades back in (CSS-timed, ~800ms) on a transition-delay so it
     lands as the final cascade step rather than racing it. */
}
.lp-mortality-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  /* dvh, not vh: with viewport-fit=cover the large-viewport 100vh extends
     BEHIND the iOS Safari toolbar, which buried the bottom-anchored scroll
     hint behind the bar. dvh tracks the visible viewport so the hint (and
     anything else bottom-pinned in the stage) clears it. Falls back to the
     100vh above where dvh is unsupported. The beat thresholds are fractions
     of the 200vh SECTION, not the stage, so the scroll math is unaffected. */
  height: 100dvh;
  overflow: hidden;
}
/* The dark backdrop is its own layer so JS can drive its opacity
   independently of the text on top. Fades in over the first 5% of
   scroll progress through the section and out over the last ~10%,
   so the dark band materializes from the page bg and dissolves back
   into it — no visible section edge. The vignette gradients sit on
   top of the solid dark fill in the same element. */
.lp-mortality-backdrop {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 70% 55% at 50% 60%, rgba(8,145,178,0.16), transparent 72%),
    radial-gradient(ellipse 90% 60% at 50% 8%, rgba(255,255,255,0.04), transparent 70%),
    #0b1422;
  opacity: 0;
  will-change: opacity;
  pointer-events: none;
}
/* Beats stack on top of each other inside the stage. The beat
   container is always fully visible — title and body fade in via the
   threshold-state rules below as a one-two punch (title first, body
   follows on a 350ms delay), and they fade out together when the
   state class is removed. */
.lp-mortality-beat {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(80px, 12vh, 140px) clamp(20px, 5vw, 60px);
  text-align: center;
}
.lp-mortality-inner {
  max-width: 820px;
  width: 100%;
}
.lp-mortality-title {
  font-family: var(--font-sans);
  font-size: clamp(32px, 4.6vw, 64px);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: 0.01em;
  color: #f4f7fb;
  margin: 0 0 clamp(20px, 3vh, 36px);
  text-shadow:
    0 4px 36px rgba(0,0,0,0.45),
    0 0 64px rgba(8,145,178,0.08);
  opacity: 0;
  will-change: opacity;
}
.lp-mortality-body {
  font-family: var(--font-sans);
  font-size: clamp(14px, 1.25vw, 19px);
  line-height: 1.58;
  letter-spacing: -0.01em;
  color: rgba(220, 230, 245, 0.7);
  margin: 0 auto;
  max-width: 600px;
  opacity: 0;
  will-change: opacity;
}
.lp-mortality-beat--meantime .lp-mortality-title {
  font-size: clamp(27px, 3.9vw, 54px);
  letter-spacing: -0.5px;
}
.lp-mortality-beat--meantime .lp-mortality-title-accent {
  color: color-mix(in srgb, var(--accent) 60%, #f4f7fb 40%);
  text-shadow: 0 0 80px rgba(8,145,178,0.22);
}
.lp-mortality-beat--meantime .lp-mortality-body {
  color: rgba(220, 230, 245, 0.82);
}

/* ── "Burn bright" shooting-star arc ──
   A glowing head streaks from the bottom-left up to the top-right behind
   the beat-2 copy, drawing a bright arc as it climbs, then flies off the
   top-right edge and fades — the arc it burned stays behind. Makes the
   Jobs reference ("a bright arc while we're here") literal.

   Entirely CSS, gated on `.is-beat-2` so it plays once when the beat
   lands and replays if the section is re-entered (the state class is
   removed on scroll-out and re-added on re-entry). Two synchronized
   motions on the SAME path data:
     - trail  → stroke-dashoffset 100→0 reveals the arc from start to tip
     - star   → offset-distance 0%→100% rides the path; kept INSIDE the
                SVG so it shares the path's user-coordinate space and stays
                pinned to the drawing tip under the slice scaling, with no
                JS coordinate-matching. pathLength="100" makes the dash math
                resolution-independent.
   Sits behind the centered copy (z-index 0; the inner text is lifted to
   z-index 1 below). */
.lp-meantime-arc {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
  /* Fades out with the rest of the beat when `.is-beat-2` is removed,
     matching the title/body fade timing. */
  transition: opacity 1000ms ease;
}
.lp-meantime-arc-svg {
  display: block;
  width: 100%;
  height: 100%;
  /* Let the star's bloom spill past the viewBox; the pinned stage's own
     `overflow:hidden` still clips it to the screen. */
  overflow: visible;
}
.lp-meantime-arc-trail {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 100;
  stroke-dashoffset: 100; /* hidden until drawn */
  filter: drop-shadow(0 0 6px rgba(34, 184, 207, 0.45));
}
.lp-meantime-arc-star {
  /* Warm ember head — goes with "burn". The cyan trail (brand) heats up
     toward the leading edge (see the gradient's warm final stop) and hands
     off to this warm core + amber bloom. */
  fill: #fff2dc;
  opacity: 0;
  offset-path: path("M -40 560 C 80 180, 420 72, 1040 60");
  offset-distance: 0%;
  offset-rotate: 0deg;
  filter:
    drop-shadow(0 0 5px #ffe1b0)
    drop-shadow(0 0 14px rgba(255, 162, 72, 0.9))
    drop-shadow(0 0 32px rgba(255, 120, 40, 0.55));
}
.lp-mortality-beat--meantime .lp-mortality-inner {
  position: relative;
  z-index: 1; /* keep the copy above the arc */
}

/* Run the streak when beat 2 lands. Both animations share the same
   easing/duration/delay so the head stays welded to the drawing tip.
   Delay 2600ms so the headline has fully landed (title in by ~2300ms)
   and been read FIRST — the ember then burns through as the body line
   ("This is your one shot…", in at 2800ms) arrives, rather than racing
   the copy. */
.lp-mortality.is-beat-2 .lp-meantime-arc { opacity: 1; }
/* Two animations on the trail: the draw welds to the star (same
   easing/duration/delay), then a separate fade lets the burned arc HOLD
   for a solid beat after the star flies off (~4700ms) before dissolving —
   the streak is the moment, not a permanent fixture. Fade starts at
   5400ms (~0.7s hold past the draw) and runs 1400ms. */
.lp-mortality.is-beat-2 .lp-meantime-arc-trail {
  animation:
    lp-meantime-arc-draw 2100ms cubic-bezier(0.25, 0.55, 0.3, 1) 2600ms forwards,
    lp-meantime-arc-fade 1400ms ease 5400ms forwards;
}
.lp-mortality.is-beat-2 .lp-meantime-arc-star {
  animation: lp-meantime-arc-fly 2100ms cubic-bezier(0.25, 0.55, 0.3, 1) 2600ms forwards;
}
@keyframes lp-meantime-arc-draw {
  from { stroke-dashoffset: 100; }
  to   { stroke-dashoffset: 0; }
}
@keyframes lp-meantime-arc-fly {
  0%   { offset-distance: 0%;   opacity: 0; }
  9%   { opacity: 1; }
  82%  { opacity: 1; }
  100% { offset-distance: 100%; opacity: 0; } /* flies off-frame, fades */
}
@keyframes lp-meantime-arc-fade {
  to { opacity: 0; }
}

/* Reduced motion: no streak. Show the resolved frame — the burned arc
   fully drawn, the head parked off the top-right edge (invisible) — the
   same payoff-frame convention the rest of the cinematic uses. */
@media (prefers-reduced-motion: reduce) {
  .lp-mortality.is-beat-2 .lp-meantime-arc-trail {
    animation: none;
    stroke-dashoffset: 0;
  }
  .lp-mortality.is-beat-2 .lp-meantime-arc-star {
    animation: none;
    offset-distance: 100%;
    opacity: 0;
  }
}

/* ── Meet Aescle beat ──
   The third beat in the sticky mortality stage. Crossfades against
   the dark backdrop (which is fading out at the same time) — so the
   user sees the dark band dissolve into the bright cyan glow as one
   continuous opacity motion, with no vertical scrolling. Sits on
   top of the page's normal light background; the glow layer beneath
   adds the soft cyan that previously lived in `.lp-intro`. */
.lp-mortality-aescle-glow {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 60% 70% at 50% 50%, rgba(8,145,178,0.10), transparent 70%),
    radial-gradient(ellipse 90% 30% at 50% 0%, rgba(8,145,178,0.05), transparent 70%);
  opacity: 0;
  will-change: opacity;
  pointer-events: none;
}
.lp-mortality-aescle-inner {
  max-width: 880px;
  width: 100%;
  text-align: center;
  /* No opacity here — children fade in sequentially via the
     `.is-aescle` cascade (mark+eyebrow → title +500ms → logos
     +1100ms) so each piece arrives on its own beat. */
}
.lp-mortality-aescle-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: clamp(64px, 6vw, 84px);
  height: clamp(64px, 6vw, 84px);
  color: var(--accent);
  margin: 0 0 clamp(16px, 2.2vh, 24px);
  filter: drop-shadow(0 6px 24px rgba(8,145,178,0.28));
  opacity: 0;
  will-change: opacity;
}
.lp-mortality-aescle-mark svg { width: 100%; height: 100%; }
.lp-mortality-aescle-eyebrow {
  font-size: 16px;
  color: var(--accent);
  letter-spacing: 4px;
  margin: 0 0 clamp(32px, 4.5vh, 44px);
  opacity: 0;
  will-change: opacity;
}
.lp-mortality-aescle-title {
  font-family: var(--font-sans);
  font-size: clamp(28px, 3.2vw, 44px);
  font-weight: 600;
  line-height: 1.32;
  letter-spacing: 0.01em;
  color: var(--text-primary);
  margin: 0 0 clamp(36px, 5.5vh, 60px);
  opacity: 0;
  will-change: opacity;
}
/* Research band, embedded as the final cascade step of Meet Aescle.
   Lives inside the pinned stage rather than as a sibling section, so
   the credentials feel structurally part of Aescle's identity — and
   the gap between the pinned beat and the next content section
   disappears. The eyebrow/grid sit on the same cyan glow as the title
   above. Container fades in as the final step of the `.is-aescle`
   cascade — see the threshold-state rules below. */
.lp-mortality-aescle-logos {
  margin-top: clamp(80px, 11vh, 128px);
  opacity: 0;
  /* Hidden until the aescle beat lands — kill pointer events so the
     `title` tooltips on the social marks can't fire while invisible. */
  pointer-events: none;
  will-change: opacity;
}
.lp-mortality-aescle-logos-eyebrow {
  text-align: center;
  margin: 0 0 clamp(20px, 3vh, 32px);
}

/* Threshold-driven beat activation. Each beat's elements default to
   opacity:0 (declared above with their layout rules); the state class
   on `.lp-mortality` flips them to 1, and the transition does the
   work. Transition-delay is set ONLY on the fade-in side, so removing
   the state class fades elements out together with no stagger.

   Timing model (default transition: 1000ms ease):
     - Beat 1 lands as soon as the stage pins (no fade-in delay), with
       its body 1000ms behind the title.
     - Each subsequent state waits 1300ms after activation before its
       first element starts fading in: that's enough for the prior
       beat's 1000ms fade-out plus a 300ms empty-dark pause.
     - Inside the aescle cascade, mark+eyebrow → title → logos each
       wait for the previous element to finish + a ~500ms breath, so
       the three pieces arrive on three distinct beats rather than as
       overlapping fades.
     - Total aescle cascade time after activation: ~2.5s + 1s last
       fade = ~3.5s for logos; nav restoration tops it off at ~4.7s.
       With 400vh of scroll budget after the threshold, even moderate
       scrollers see the full cascade before sticky releases.

   See the comment on `.lp-mortality` above for the threshold model
   and the policies (skip-ahead + forward-only). */
.lp-mortality-title,
.lp-mortality-body,
.lp-mortality-aescle-glow,
.lp-mortality-aescle-mark,
.lp-mortality-aescle-eyebrow,
.lp-mortality-aescle-title,
.lp-mortality-aescle-logos {
  transition: opacity 1000ms ease;
}
.lp-mortality.is-beat-1 .lp-mortality-beat--certain .lp-mortality-title { opacity: 1; }
.lp-mortality.is-beat-1 .lp-mortality-beat--certain .lp-mortality-body  { opacity: 1; transition-delay: 1500ms; }
.lp-mortality.is-beat-2 .lp-mortality-beat--meantime .lp-mortality-title { opacity: 1; transition-delay: 1300ms; }
.lp-mortality.is-beat-2 .lp-mortality-beat--meantime .lp-mortality-body  { opacity: 1; transition-delay: 2800ms; }
.lp-mortality.is-aescle .lp-mortality-aescle-glow,
.lp-mortality.is-aescle .lp-mortality-aescle-mark,
.lp-mortality.is-aescle .lp-mortality-aescle-eyebrow { opacity: 1; transition: opacity 500ms ease; transition-delay: 0ms; }
.lp-mortality.is-aescle .lp-mortality-aescle-title { opacity: 1; transition-delay: 1000ms; }
.lp-mortality.is-aescle .lp-mortality-aescle-logos { opacity: 1; pointer-events: auto; transition-delay: 2500ms; }

/* When the cascade hands off to aescle, snap beat 2's title + body out
   instantly instead of fading them over 1s. The scroll-input lock leaks
   during the state 2→3 handoff (macOS scroll-latching defeats
   preventDefault for the rest of the gesture, and the dwell clamp yanks
   scrollTop back over the next few frames). With nothing to jostle on
   screen, the leaked scroll is invisible — the aescle backdrop + glow
   handle the gradual visual transition on their own. */
.lp-mortality.is-aescle .lp-mortality-beat--meantime .lp-mortality-title,
.lp-mortality.is-aescle .lp-mortality-beat--meantime .lp-mortality-body {
  transition: none;
  opacity: 0;
}

/* Nav restoration during aescle. The nav is held at opacity:0 by JS
   while the mortality stage is pinned and beats 1+2 are running.
   When `.is-aescle` activates, JS adds `.is-restored` to the nav and
   sets opacity:1 inline — this CSS rule provides the transition AND
   a deliberate delay so the nav fades in *after* the research logos
   have fully landed (logos start at 2500ms with a 1000ms fade, so
   they're done at 3500ms; nav waits an extra 400ms breath beyond
   that). Lands strictly last in the cascade — first the figure and
   eyebrow, then the title, then "Grounded in research from" + logos,
   THEN the nav returns. Class is removed when the section resets. */
.landing-nav.is-restored {
  transition: opacity 800ms ease 3900ms;
}
/* Skip path: when the user opts out of the cinematic, the cascade
   doesn't play out — there's nothing for the nav to wait behind. JS
   adds `.is-restored-instant` alongside `.is-restored` so the nav
   returns as part of the same crossfade as the aescle reveal itself
   (~260ms), not 4s later. */
.landing-nav.is-restored.is-restored-instant {
  transition: opacity 260ms ease;
}

/* Scroll affordance pinned at the bottom of the mortality stage. JS
   schedules it to surface `HINT_DELAY_AFTER_DWELL_MS` after each
   beat's dwell ends (see landing.js). It's a quiet fallback: if the
   user tried to scroll during the hard-locked dwell window, got
   blocked, and gave up — they'll see this nudge once the lock
   releases. Most users scroll on again on their own well before it
   appears, never seeing it. Bounces gently to draw the eye without
   stealing attention from the beat content above. */
.lp-mortality-scroll-hint {
  position: absolute;
  bottom: calc(clamp(28px, 4vh, 56px) + var(--safe-bottom));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 2.2px;
  text-transform: uppercase;
  /* Default color is light, for the dark beat 1 / beat 2 backdrops.
     During aescle the section flips to a light backdrop and we swap
     to a cyan (see the `.is-aescle` rule below). Opacity bumped so
     the hint reads as a clear affordance on first glance — too
     subtle and users miss it as the dwell window expires. */
  color: rgba(220, 230, 245, 0.82);
  opacity: 0;
  pointer-events: none;
  /* Default (hidden) transition governs the fade-OUT when the class is
     removed — slow so dismissing the hint feels gentle, not snapped
     away. The fade-IN is overridden to a snappier 280ms on the visible
     rule below, so it lands inside the JS lead window. */
  transition: opacity 900ms ease, color 600ms ease;
  z-index: 4;
}
.lp-mortality-scroll-hint-arrow {
  width: 36px;
  height: 36px;
  animation: lp-mortality-scroll-hint-bounce 1.8s ease-in-out infinite;
}
/* Thicker stroke for the larger size — the original 1.6 in a 16×16
   viewBox reads spindly when scaled to 36px. */
.lp-mortality-scroll-hint-arrow path {
  stroke-width: 1.8;
}
.lp-mortality.is-hint-visible .lp-mortality-scroll-hint {
  opacity: 1;
  transition: opacity 280ms ease-out, color 600ms ease;
}
.lp-mortality.is-aescle .lp-mortality-scroll-hint {
  color: color-mix(in srgb, var(--accent) 92%, transparent);
}
/* Reveal beat only: the research footer is the bottom anchor here, so the
   scroll cue shares the floor with it — at full size the two read as a busy
   stack. Demote the cue to a smaller arrow tucked lower, so it's a quiet
   "more below" hint beneath the footer rather than a competing element. Beats
   1-2 (no footer, locked dwell) keep the prominent arrow untouched. */
.lp-mortality.is-aescle .lp-mortality-scroll-hint {
  bottom: calc(clamp(16px, 2.5vh, 30px) + var(--safe-bottom));
}
.lp-mortality.is-aescle .lp-mortality-scroll-hint-arrow {
  width: 22px;
  height: 22px;
}
@keyframes lp-mortality-scroll-hint-bounce {
  0%, 100% { transform: translateY(-3px); opacity: 0.78; }
  50%      { transform: translateY(5px);  opacity: 1; }
}

/* Phone: rebalance the Meet Aescle reveal. On desktop the inner block is
   centered as one unit — fine when it's wide. On a phone the tall research
   band + its big top margin sit below the title, so centering the WHOLE block
   drags the brand lockup up into the top third and parks "built on research
   by" near the optical center, where a credentials footer shouldn't be.
   Fix: make the inner a full-height flex column and split the free space with
   two auto margins — the brand lockup (mark/eyebrow/title) settles at the
   optical center, the research band drops to a footer with enough bottom
   clearance to stay off the scroll-hint chevron + home indicator. Desktop is
   untouched (the single centered block is correct at width). */
@media (max-width: 720px) {
  /* Lift the research band out of flow so the brand lockup centers on its
     own (the beat already centers its single remaining in-flow child), and
     pin the credentials as a true footer just above the scroll-hint chevron.
     An earlier flex/auto-margin split coupled the two — lockup centered, but
     the research band stranded in the lower-MIDDLE with a big void beneath it
     (the beat's own bottom padding stacked on the footer margin). Absolute
     positioning decouples them: lockup at optical center, research at the
     floor. Desktop keeps the single centered block. */
  .lp-mortality-aescle-title { margin-bottom: 0; }
  .lp-mortality-aescle-logos {
    position: absolute;
    left: 0;
    right: 0;
    /* Clear the chevron: its bottom offset + arrow height + a breath. Both
       resolve to the same dvh-bottom (stage / beat bottom), so the footer
       and the chevron track together across devices. */
    bottom: calc(clamp(28px, 4vh, 56px) + 92px + var(--safe-bottom));
    margin: 0;
    padding-inline: clamp(20px, 5vw, 40px);
  }
}

/* Skip-intro affordance. Surfaces in the top-right of the pinned stage
   for repeat viewers — users who reached the aescle reveal once and
   then scrolled back to the top. The state machine sets
   `.has-completed-once` on `.lp-mortality` the first time state 3
   activates and never clears it for the rest of the session. Visible
   only on beats 1 + 2; aescle is itself the natural exit. Click lands
   the user inside aescle with the cinematic disarmed — see
   `skipToReveal()` in landing.js.

   Sits in the corner the top nav vacates during the dark beats (nav
   fades to 0 while beats 1/2 are active), so there's no visual clash.
   Cyan border + cool-gray text — a quiet "you can leave" cue rather
   than something that competes with the title for the eye. */
.lp-mortality-skip {
  position: absolute;
  top: clamp(20px, 3vh, 32px);
  right: clamp(20px, 3vw, 36px);
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 7px 14px;
  border: 1px solid rgba(220, 230, 245, 0.18);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.03);
  color: rgba(220, 230, 245, 0.6);
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transform: translateY(-4px);
  transition: opacity 320ms ease, transform 320ms ease, color 180ms ease, border-color 180ms ease, background 180ms ease;
  z-index: 5;
}
.lp-mortality-skip:hover {
  color: rgba(240, 247, 255, 0.95);
  border-color: rgba(220, 230, 245, 0.42);
  background: rgba(255, 255, 255, 0.06);
}
.lp-mortality-skip:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--accent) 70%, transparent);
  outline-offset: 3px;
}
.lp-mortality-skip-x {
  font-size: 13px;
  line-height: 1;
  letter-spacing: 0;
  opacity: 0.85;
}
/* Only repeat viewers, and only on the dark beats. Aescle is already
   the cinematic's exit — no skip needed there. */
.lp-mortality.has-completed-once.is-beat-1 .lp-mortality-skip,
.lp-mortality.has-completed-once.is-beat-2 .lp-mortality-skip {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
  /* Brief delay so the button doesn't crash into the beat title's
     fade-in. Title is fully visible at ~1000ms post-state-activation
     for beat 1; landing at 600ms means the skip arrives just before
     the body copy, as a distinct beat. */
  transition-delay: 600ms;
}

/* `.is-skipped` overrides the normal aescle cascade so the reveal
   appears in a quick simultaneous fade instead of the 3s staggered
   choreography. Repeat viewers who hit Skip want the destination,
   not the journey. */
.lp-mortality.is-skipped .lp-mortality-aescle-glow,
.lp-mortality.is-skipped .lp-mortality-aescle-mark,
.lp-mortality.is-skipped .lp-mortality-aescle-eyebrow,
.lp-mortality.is-skipped .lp-mortality-aescle-title,
.lp-mortality.is-skipped .lp-mortality-aescle-logos {
  opacity: 1;
  transition: opacity 260ms ease;
  transition-delay: 0ms;
}
/* Snap both dark beats out instantly during a skip — the normal
   beat-2 snap-out rule above only covers `meantime`; in the skip
   path we may be coming from beat 1, so cover `certain` too. */
.lp-mortality.is-skipped .lp-mortality-beat--certain .lp-mortality-title,
.lp-mortality.is-skipped .lp-mortality-beat--certain .lp-mortality-body,
.lp-mortality.is-skipped .lp-mortality-beat--meantime .lp-mortality-title,
.lp-mortality.is-skipped .lp-mortality-beat--meantime .lp-mortality-body {
  transition: opacity 200ms ease;
  opacity: 0;
}
/* Smooth-fade the dark backdrop out during a skip rather than
   snapping it. The inline opacity is set to "0" by skipToReveal();
   without this transition, the dark band would cut instantly while
   aescle's cyan glow is still fading in (260ms) — a visible flash.
   Matching durations gives a clean crossfade between dark band and
   aescle reveal. */
.lp-mortality.is-skipped .lp-mortality-backdrop {
  transition: opacity 260ms ease;
}
/* Hide the skip button itself the moment a skip is in progress so it
   doesn't linger over the revealing aescle card. */
.lp-mortality.is-skipped .lp-mortality-skip {
  opacity: 0;
  pointer-events: none;
  transition-delay: 0ms;
}

/* ── Meet Aescle (intro) ── DEPRECATED.
   Meet Aescle now lives inside the mortality sticky stage as a
   third beat (see `.lp-mortality-beat--aescle` / `.lp-mortality-
   aescle-*` above). These classes are kept temporarily in case
   anything else still references them, but no element on the
   landing page uses them. Safe to delete in a follow-up. */
.lp-intro {
  position: relative;
  min-height: clamp(460px, 64vh, 680px);
  display: flex;
  align-items: center;
  padding: clamp(72px, 11vh, 128px) 0 clamp(28px, 4vh, 48px);
  overflow: hidden;
}
/* Research band reads as a credential footer to "Meet Aescle" — pull
   it tight against the intro instead of standing alone. */
.lp-intro + .lp-social {
  padding-top: 0;
  padding-bottom: clamp(72px, 11vh, 128px);
  border-bottom: 1px solid var(--border-subtle);
}
.lp-intro-glow {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 60% 70% at 50% 50%, rgba(8,145,178,0.10), transparent 70%),
    radial-gradient(ellipse 90% 30% at 50% 0%, rgba(8,145,178,0.05), transparent 70%);
  pointer-events: none;
}
.lp-intro-shell {
  position: relative;
}
.lp-intro-card {
  max-width: 880px;
  margin: 0 auto;
  text-align: center;
}
.lp-intro-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: clamp(64px, 6vw, 84px);
  height: clamp(64px, 6vw, 84px);
  color: var(--accent);
  margin: 0 0 clamp(20px, 3vh, 32px);
  filter: drop-shadow(0 6px 24px rgba(8,145,178,0.28));
}
.lp-intro-mark svg { width: 100%; height: 100%; }
.lp-intro-eyebrow {
  color: var(--accent);
  letter-spacing: 3.2px;
  margin-bottom: 22px;
}
.lp-intro-title {
  font-family: var(--font-sans);
  font-size: clamp(38px, 5.4vw, 72px);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  margin: 0 0 clamp(20px, 3vh, 32px);
}
.lp-intro-sub {
  font-family: var(--font-sans);
  font-size: clamp(15px, 1.25vw, 19px);
  line-height: 1.58;
  letter-spacing: -0.01em;
  color: var(--text-secondary);
  margin: 0 auto;
  max-width: 600px;
}

/* ── Reveal-on-scroll ──
   Any element with `data-reveal` starts shifted down + faded, then
   slides into place when an IntersectionObserver toggles
   `is-in-view`. Different element classes get slightly different
   distances / durations so headers feel lighter than full cards. */
[data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition:
    opacity 900ms cubic-bezier(.2, .7, .2, 1),
    transform 900ms cubic-bezier(.2, .7, .2, 1);
  will-change: opacity, transform;
}
[data-reveal].is-in-view {
  opacity: 1;
  transform: translateY(0);
}
/* Per-element overrides. Because the base `[data-reveal].is-in-view`
   rule has the same specificity as `.<class>[data-reveal]`, anything
   that overrides `transform` for the start state must also re-declare
   the end state — otherwise CSS source order pins the element to the
   shifted position even after `.is-in-view` is added. */
.lp-howflow-pair[data-reveal] { transition-delay: 140ms; }

.lp-intro-card[data-reveal] {
  transform: translateY(36px);
  transition-duration: 1100ms;
}
.lp-intro-card[data-reveal].is-in-view { transform: translateY(0); }

/* ── FAQ ── */
.lp-faq {
  background: var(--bg-surface);
  border-top: 1px solid var(--border-subtle);
  border-bottom: 1px solid var(--border-subtle);
  padding: clamp(96px, 14vh, 180px) 0;
}
.lp-faq-shell {
  display: grid;
  grid-template-columns: minmax(220px, 1fr) minmax(0, 1.8fr);
  gap: clamp(32px, 5vw, 80px);
  align-items: start;
}
.lp-faq-head {
  align-self: start;
}
.lp-faq-head .lp-section-title {
  margin: 0 0 12px;
  font-size: clamp(24px, 2.6vw, 32px);
}
.lp-faq-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--border);
}
.lp-faq-list li {
  border-bottom: 1px solid var(--border);
}
.lp-faq-row {
  /* native <details> — no JS needed for collapse/expand */
}
.lp-faq-row > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 20px 0;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-sans);
  font-size: 15.5px;
  font-weight: 500;
  letter-spacing: -0.005em;
  color: var(--text-secondary);
  user-select: none;
}
.lp-faq-row > summary::-webkit-details-marker { display: none; }
/* hover: hover only — on touch, :hover sticks after a tap and would leave a
   just-collapsed question stuck in accent color. */
@media (hover: hover) {
  .lp-faq-row > summary:hover { color: var(--accent); }
}
.lp-faq-chev {
  font-size: 0; /* hide the literal "+"; bars are drawn below */
  position: relative;
  display: inline-block;
  width: 14px;
  height: 14px;
  color: var(--text-dim);
  line-height: 1;
}
.lp-faq-chev::before,
.lp-faq-chev::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  background: currentColor;
  transform: translate(-50%, -50%);
  transition: opacity 180ms ease;
}
.lp-faq-chev::before { width: 14px; height: 2px; } /* horizontal bar */
.lp-faq-chev::after { width: 2px; height: 14px; } /* vertical bar */
.lp-faq-row[open] .lp-faq-chev { color: var(--accent); }
.lp-faq-row[open] .lp-faq-chev::after { opacity: 0; } /* drop vertical -> minus */
.lp-faq-body {
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.6;
  letter-spacing: -0.01em;
  color: var(--text-secondary);
  padding: 0 0 22px;
  max-width: 60ch;
}
.lp-faq-body a {
  color: var(--accent);
  text-decoration: none;
  transition: color 160ms ease;
}
.lp-faq-body a:hover { color: var(--text-primary); }
/* "Show all" reveal: desktop shows the full list, so the button is inert
   here. It only surfaces (and the list only truncates) at ≤760px below. */
.lp-faq-more { display: none; }

/* ── Final CTA card ── */
.lp-final {
  padding: clamp(56px, 9vh, 100px) 0;
}
.lp-final-card {
  /* Light closer: the page runs white end-to-end, so the final beat reads
     as the same atmosphere — fog clearing toward a horizon — rather than
     a foreign dark slab. The cyan glow rising from below carries the
     trajectory motif into the CTA itself. */
  position: relative;
  max-width: 760px;
  margin: 0 auto;
  border-radius: var(--radius-lg);
  padding: clamp(72px, 11vh, 112px) clamp(28px, 5vw, 64px) clamp(64px, 9vh, 96px);
  text-align: center;
  background:
    radial-gradient(ellipse 70% 60% at 50% 118%, rgba(8,145,178,0.16), transparent 68%),
    radial-gradient(ellipse 90% 70% at 50% -10%, rgba(8,145,178,0.04), transparent 60%),
    linear-gradient(180deg, #ffffff 0%, #f7fafb 100%);
  border: 1px solid color-mix(in srgb, var(--accent) 18%, var(--border));
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.9) inset,
    0 30px 80px -36px rgba(8, 47, 73, 0.18),
    0 0 0 1px rgba(255, 255, 255, 0.4) inset;
  overflow: hidden;
  isolation: isolate;
}
/* Tick marks at the four card corners — quiet instrument-panel detail. */
.lp-final-card::after {
  content: "";
  position: absolute;
  inset: 14px;
  border-radius: calc(var(--radius-lg) - 6px);
  pointer-events: none;
  z-index: 0;
  background:
    linear-gradient(90deg, var(--accent) 0 18px, transparent 18px) top left / 18px 1px no-repeat,
    linear-gradient(0deg,  var(--accent) 0 18px, transparent 18px) top left / 1px 18px no-repeat,
    linear-gradient(90deg, transparent calc(100% - 18px), var(--accent) calc(100% - 18px)) top right / 18px 1px no-repeat,
    linear-gradient(0deg,  var(--accent) 0 18px, transparent 18px) top right / 1px 18px no-repeat,
    linear-gradient(90deg, var(--accent) 0 18px, transparent 18px) bottom left / 18px 1px no-repeat,
    linear-gradient(180deg, transparent calc(100% - 18px), var(--accent) calc(100% - 18px)) bottom left / 1px 18px no-repeat,
    linear-gradient(90deg, transparent calc(100% - 18px), var(--accent) calc(100% - 18px)) bottom right / 18px 1px no-repeat,
    linear-gradient(180deg, transparent calc(100% - 18px), var(--accent) calc(100% - 18px)) bottom right / 1px 18px no-repeat;
  opacity: 0.32;
}
.lp-final-card > * { position: relative; z-index: 1; }

.lp-final-title {
  font-family: var(--font-sans);
  font-size: clamp(30px, 3.8vw, 48px);
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: 0.01em;
  color: var(--text-primary);
  margin: 0 0 16px;
  /* Same scattered-light halo the hero headline uses — keeps the page's
     atmosphere coherent from open to close. */
  text-shadow:
    0 0 14px rgba(255,255,255,0.95),
    0 0 28px rgba(255,255,255,0.7),
    0 10px 36px rgba(8,145,178,0.10);
}
.lp-final-sub {
  font-family: var(--font-sans);
  font-size: clamp(13px, 1.05vw, 16px);
  line-height: 1.55;
  letter-spacing: -0.01em;
  color: var(--text-secondary);
  margin: 0 auto clamp(36px, 5vh, 48px);
  max-width: 460px;
}
.lp-final-card .landing-cta {
  /* Cyan CTA lifted on a soft halo — same color story as the hero CTA
     but inverted (cyan-on-white vs ink-on-white), marking the closer
     as the moment of commitment. */
  background: linear-gradient(180deg, var(--accent) 0%, color-mix(in srgb, var(--accent) 86%, #000) 100%);
  border-color: color-mix(in srgb, var(--accent) 65%, #000);
  color: #ffffff;
  box-shadow:
    0 18px 38px -14px color-mix(in srgb, var(--accent) 50%, transparent),
    0 0 0 9px color-mix(in srgb, var(--accent) 10%, rgba(232, 248, 252, 0.6)),
    0 1px 0 rgba(255, 255, 255, 0.28) inset;
}
.lp-final-card .landing-cta:hover {
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 92%, #fff) 0%, var(--accent) 100%);
  border-color: var(--accent);
  transform: translateY(-1px);
  box-shadow:
    0 22px 44px -12px color-mix(in srgb, var(--accent) 60%, transparent),
    0 0 0 9px color-mix(in srgb, var(--accent) 14%, rgba(232, 248, 252, 0.7)),
    0 1px 0 rgba(255, 255, 255, 0.32) inset;
}

/* ── Footer ── */
.lp-footer {
  padding: clamp(36px, 5vh, 56px) 0;
  border-top: 1px solid var(--border);
  background: var(--bg-panel);
}
.lp-footer-shell {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: clamp(28px, 4vw, 72px);
  flex-wrap: wrap;
}
.lp-footer-brand {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 360px;
}
.lp-footer-cols {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(36px, 5vw, 72px);
}
.lp-footer-col {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 140px;
}
.lp-footer-col-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 6px 0;
}
.lp-footer-col a {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.4px;
  color: var(--text-secondary);
  text-decoration: none;
  transition: color 160ms ease;
}
.lp-footer-col a:hover { color: var(--accent); }
.lp-footer-col-all {
  margin-top: 4px;
  color: var(--accent) !important;
  font-weight: 600 !important;
  letter-spacing: 1px !important;
  text-transform: uppercase;
  font-size: 10px !important;
}
.lp-footer-mark {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--accent);
}
.lp-footer-word {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 4px;
}
.lp-footer-tagline {
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: var(--text-secondary);
  margin: 0;
}
.lp-footer-legal {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.4px;
  color: var(--text-dim);
  margin: 6px 0 0;
}

@media (prefers-reduced-motion: reduce) {
  .landing-mark,
  .landing-recency,
  .landing-headline,
  .landing-sub,
  .landing-foot,
  .landing-scrollcue {
    animation: none;
    opacity: 1;
    transform: none;
  }
  .landing-recency-dot { animation: none; }
  [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }
  /* Engine "processing" motion (card 02 only) — disable per user preference. */
  .lp-value[data-active="02"] .lp-engine-hex-inner,
  .lp-value[data-active="02"] .lp-engine-glyph,
  .lp-value[data-active="02"] .lp-engine-hex-outer,
  .lp-value[data-active="02"] [data-vertex="threats"] .v-halo,
  .lp-value[data-active="02"] [data-vertex="threats"] .v-dot {
    animation: none;
  }
  .lp-value[data-active="02"] [data-vertex="threats"] .v-halo { r: 28; }
  .lp-value[data-active="02"] [data-vertex="threats"] .v-dot { r: 10; }
  /* Reduced-motion: undo the pinned scrollytelling on the mortality
     section. The outer section collapses to natural height, the stage
     becomes regular flow, the dark backdrop stays visible behind
     beats 1+2, and the beats stack vertically at full opacity. The
     Meet Aescle beat needs its own light background since the dark
     backdrop covers the whole stage. */
  .lp-mortality {
    height: auto;
  }
  .lp-mortality-stage {
    position: relative;
    height: auto;
  }
  .lp-mortality-backdrop {
    position: absolute;
    opacity: 1;
  }
  .lp-mortality-aescle-glow {
    display: none;
  }
  .lp-mortality-beat {
    position: relative;
    inset: auto;
    min-height: clamp(480px, 80vh, 820px);
  }
  .lp-mortality-beat--aescle {
    background:
      radial-gradient(ellipse 60% 70% at 50% 50%, rgba(8,145,178,0.10), transparent 70%),
      radial-gradient(ellipse 90% 30% at 50% 0%, rgba(8,145,178,0.05), transparent 70%),
      var(--bg-page);
    z-index: 1;
  }
  .lp-mortality-title,
  .lp-mortality-body,
  .lp-mortality-aescle-mark,
  .lp-mortality-aescle-eyebrow,
  .lp-mortality-aescle-title,
  .lp-mortality-aescle-logos {
    opacity: 1;
  }
  .landing-scrollcue-arrow { animation: none; }
  .landing-trajectory-track {
    animation: none;
    stroke-dashoffset: 0;
  }
  .landing-trajectory-area {
    animation: none;
    opacity: 1;
  }
  .landing-orb,
  .landing-trajectory-ring,
  .landing-trajectory-glint,
  .landing-trajectory-dot,
  .landing-trajectory-pulse {
    animation: none;
    opacity: 1;
  }
  /* Crowd problem section: the 3D scene handles reduced motion itself
     (revealStatic renders YOU stepped-out + flags .is-you-up for the card). */
}

/* Narrow viewports — collapse the multi-column grids, tighten the hero
   typography, and shrink the trajectory layer so the marker lands above
   the CTA rather than colliding with it. */
@media (max-width: 760px) {
  .lp-faq-shell { grid-template-columns: 1fr; gap: 32px; }
  .lp-faq-head { position: static; }
  /* The full 9-row stack is long on phones — show the first 5, reveal the
     rest via the "show all" button. Rows stay in the DOM (display:none, not
     removed) so they remain indexable and findable once expanded. */
  .lp-faq-list:not(.is-expanded) > li:nth-child(n + 6) { display: none; }
  .lp-faq-more {
    display: block;
    margin: 0;
    /* Tucks directly under the last row's border — no trailing border of its
       own (it's a terminal action, not a list row) and left-aligned to the
       same edge as the question labels. */
    padding: 16px 0 0;
    background: none;
    border: none;
    font-family: var(--font-mono);
    font-size: 13px;
    font-weight: 500;
    letter-spacing: 0.5px;
    color: var(--accent);
    text-align: left;
    cursor: pointer;
  }
  @media (hover: hover) {
    .lp-faq-more:hover { color: var(--text-primary); }
  }
  /* Once expanded, the button has done its job — drop it. */
  .lp-faq-list.is-expanded ~ .lp-faq-more { display: none; }
  .landing-nav-links { display: none; }
  .landing-nav { gap: 12px; }
  /* Tighter lockup on phones: the wordmark + EARLY ACCESS pill carry the
     hero; the tagline is redundant at this size and crowds the top. With
     the tagline gone, drop the pill's margin-top (which used to space it
     off the tagline) so it tucks right under the wordmark. */
  .landing-mark-tag { display: none; }
  .landing-recency { margin-top: 2px; }
  /* Scroll cue: on phones the label is redundant (people scroll) and was
     the hardest text to read — keep just a larger arrow as the nudge. */
  .landing-scrollcue-label { display: none; }
  .landing-scrollcue-arrow { font-size: 22px; }
  /* The links carried the margin-left:auto that pushed the CTA to the
     right edge; with them hidden on mobile, hand that off to the CTA so
     it pins right (logo left, CTA right) instead of crowding the logo. */
  .landing-nav-cta { margin-left: auto; }
  /* No hover on touch — the hero CTA never gets the lift that desktop
     reveals on hover. Blue fought the cool sky and frosted glass vanished
     over the dark mountaineer silhouette, so default to a solid light pill
     with dark ink: palette-neutral, maximum figure/ground contrast against
     whatever it sits over. This is the primary action — make it read like
     one. */
  .landing-hero-section--video .landing-cta {
    background: #f4f8fc;
    border-color: #f4f8fc;
    color: #060b14;
    font-weight: 700;
    box-shadow:
      0 16px 36px -16px rgba(4, 10, 18, 0.6),
      0 0 0 4px rgba(232, 248, 252, 0.22);
  }
}

@media (max-width: 640px) {
  .landing-mark-word { font-size: 26px; letter-spacing: 11px; }
  .landing-headline { font-size: clamp(30px, 10.2vw, 42px); }
  .landing-sub { font-size: 16.5px; line-height: 1.6; }
  .landing-trajectory-wrap { height: 35vh; min-height: 190px; }
  .landing-trajectory-dot {
    width: 9px;
    height: 9px;
  }
  .landing-trajectory-pulse {
    width: 30px;
    height: 30px;
  }
  /* The scroll cue is absolutely pinned at the section's bottom (24px).
     A small foot margin dropped the CTA right onto it — and the CTA's
     glow ring + drop shadow eat further into the gap. Lift the CTA so
     there's clear air between it and "LEARN MORE". */
  .landing-foot {
    margin-bottom: clamp(32px, 5vh, 52px);
  }
  .lp-footer-shell { gap: 28px; }
  .lp-footer-cols {
    flex-direction: column;
    gap: 24px;
  }
  .lp-footer-col { min-width: 0; }
}

/* ═══════════ TOUR ═══════════
   One-time product orientation. Three surfaces share the same bottom-
   right corner (toast → pill → narration card) so the user's eye never
   has to reacquire the affordance. The dim layer is intentionally light:
   the data should still be legible — the focus ring carries emphasis,
   not the dim. Severity reds and amber are reserved; the ring uses the
   leverage axis (cyan) since this is the "your axis of control" tour. */

/* — Shared button vocabulary — */
.tour-btn {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  padding: 8px 14px;
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease;
  background: transparent;
}
.tour-btn-primary {
  background: var(--text-primary);
  color: var(--bg-panel);
  border-color: var(--text-primary);
}
.tour-btn-primary:hover {
  background: #000;
  border-color: #000;
}
.tour-btn-text {
  color: var(--text-secondary);
  padding: 8px 10px;
}
.tour-btn-text:hover {
  color: var(--text-primary);
  background: var(--bg-hover);
}
.tour-btn-text:disabled {
  color: var(--text-dim);
  cursor: not-allowed;
  background: transparent;
}

/* — Toast: first-time prompt — */
.tour-toast {
  position: fixed;
  right: 20px;
  bottom: 84px;
  width: 332px;
  max-width: calc(100vw - 40px);
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  padding: 16px 18px 14px;
  z-index: 80;
  box-shadow:
    0 1px 0 rgba(20, 25, 32, 0.04),
    0 12px 32px -8px rgba(20, 25, 32, 0.18),
    0 2px 6px rgba(20, 25, 32, 0.06);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 520ms ease, transform 560ms cubic-bezier(0.2, 0.7, 0.2, 1);
  pointer-events: auto;
}
.tour-toast.is-visible {
  opacity: 1;
  transform: translateY(0);
  /* Hold off the first attention bloom until the toast has fully
     arrived and settled (~1.6s) — it glides in quietly, then breathes. */
  animation: tourToastPulse 4.2s ease-out 1.6s infinite;
}
/* Slow breathing cycle: the ring blooms briefly at the start of each
   beat then holds quiet for ~2.5s. Long enough that peripheral vision
   catches motion without it becoming a billboard. */
@keyframes tourToastPulse {
  0%, 100% { box-shadow: 0 1px 0 rgba(20,25,32,0.04), 0 12px 32px -8px rgba(20,25,32,0.18), 0 2px 6px rgba(20,25,32,0.06), 0 0 0 0 transparent; }
  6%       { box-shadow: 0 1px 0 rgba(20,25,32,0.04), 0 12px 32px -8px rgba(20,25,32,0.18), 0 2px 6px rgba(20,25,32,0.06), 0 0 0 0 var(--accent-soft); }
  40%      { box-shadow: 0 1px 0 rgba(20,25,32,0.04), 0 12px 32px -8px rgba(20,25,32,0.18), 0 2px 6px rgba(20,25,32,0.06), 0 0 0 14px transparent; }
}
.tour-toast-arrow {
  display: inline-block;
  transition: transform 140ms ease;
}
.tour-toast.is-visible .tour-toast-arrow {
  animation: tourToastArrow 4.2s ease-in-out 0.4s infinite;
}
.tour-toast .tour-btn-primary:hover .tour-toast-arrow {
  animation: none;
  transform: translateX(3px);
}
@keyframes tourToastArrow {
  0%, 60%, 100% { transform: translateX(0); }
  12%           { transform: translateX(3px); }
  28%           { transform: translateX(0); }
}
.tour-toast-eyebrow {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 8px;
}
.tour-toast-body {
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-primary);
  margin-bottom: 14px;
}
.tour-toast-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  justify-content: flex-end;
}
.tour-toast-timer {
  position: absolute;
  top: 0;
  left: 0;
  height: 3px;
  width: 100%;
  background: var(--accent);
  transform-origin: left center;
  transform: scaleX(0);
  animation: tourTimerFill 3.2s linear forwards;
}
@keyframes tourTimerFill {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
/* Shared dismiss for unbidden corner toasts (tour celebration + engagement
   prompt): a quiet top-right ✕, clear of the celebration's timer bar at the
   very top edge. One rule so the two can't drift apart. */
.tour-celebration-close,
.engagement-toast-close {
  position: absolute;
  top: 8px;
  right: 8px;
  background: transparent;
  border: none;
  padding: 6px 8px;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 1;
  color: var(--text-dim);
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: color 140ms ease, background 140ms ease;
}
.tour-celebration-close:hover,
.engagement-toast-close:hover {
  color: var(--text-primary);
  background: var(--bg-hover);
}

/* — Engagement-triggered iOS waitlist prompt (js/engagement.js). Same
     bottom-right slot and chrome as the tour toast, fired once the user
     has earned it through real usage. No perpetual pulse — this is a
     deliberate, one-time nudge, not an ambient affordance, so it enters
     once and then sits quiet. — */
.engagement-toast {
  position: fixed;
  right: 20px;
  bottom: 84px;
  width: 332px;
  max-width: calc(100vw - 40px);
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  padding: 16px 18px 14px;
  z-index: 80;
  box-shadow:
    0 1px 0 rgba(20, 25, 32, 0.04),
    0 12px 32px -8px rgba(20, 25, 32, 0.18),
    0 2px 6px rgba(20, 25, 32, 0.06);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 280ms ease, transform 280ms cubic-bezier(0.2, 0.7, 0.2, 1);
  pointer-events: auto;
}
.engagement-toast.is-visible {
  opacity: 1;
  transform: translateY(0);
}
.engagement-toast-eyebrow {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 8px;
}
.engagement-toast-body {
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-primary);
  margin-bottom: 14px;
}
.engagement-toast-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  justify-content: flex-end;
}
.engagement-toast-arrow {
  display: inline-block;
  transition: transform 140ms ease;
}
.eng-btn {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.4px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  padding: 8px 14px;
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease;
  background: transparent;
}
.eng-btn-primary {
  background: var(--text-primary);
  color: var(--bg-panel);
  border-color: var(--text-primary);
}
.eng-btn-primary:hover {
  background: #000;
  border-color: #000;
}
.eng-btn-primary:hover .engagement-toast-arrow {
  transform: translateX(3px);
}

/* — Pill: persistent / resume affordance — */
.tour-pill {
  position: fixed;
  right: 20px;
  bottom: 84px;
  display: inline-flex;
  align-items: stretch;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 999px;
  z-index: 79;
  box-shadow:
    0 1px 0 rgba(20, 25, 32, 0.04),
    0 6px 18px -6px rgba(20, 25, 32, 0.14);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 220ms ease, transform 220ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
.tour-pill.is-visible {
  opacity: 1;
  transform: translateY(0);
}
.tour-pill-main {
  background: transparent;
  border: none;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.6px;
  color: var(--text-secondary);
  padding: 8px 14px 8px 14px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  border-radius: 999px 0 0 999px;
  transition: color 140ms ease, background 140ms ease;
}
.tour-pill-main:hover {
  color: var(--text-primary);
  background: var(--bg-hover);
}
.tour-pill-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  color: var(--accent);
  font-size: 13px;
}
.tour-pill-dismiss {
  background: transparent;
  border: none;
  border-left: 1px solid var(--border);
  padding: 0 12px;
  font-size: 12px;
  color: var(--text-dim);
  cursor: pointer;
  border-radius: 0 999px 999px 0;
  transition: color 140ms ease, background 140ms ease;
}
.tour-pill-dismiss:hover {
  color: var(--text-primary);
  background: var(--bg-hover);
}

/* — Focus ring: spotlights the focal element. The big outer box-shadow
     carries the dim outside the ring, so the focal area reads at full
     brightness — no separate dim layer compounds atop it. JS clamps the
     ring to the visible region between topbar and bottom-nav so tall
     sections don't slice across the chrome. pointer-events:none so the
     user can still click through if they want to. — */
.tour-focus-ring {
  position: fixed;
  z-index: 71;
  pointer-events: none;
  border-radius: var(--radius-md);
  border: 1.5px solid var(--accent);
  box-shadow:
    0 0 0 9999px rgba(20, 25, 32, 0.14),
    0 0 0 1px var(--bg-panel) inset,
    0 0 18px -2px var(--accent-soft);
  opacity: 0;
  transition: opacity 280ms ease,
              top 320ms cubic-bezier(0.2, 0.7, 0.2, 1),
              left 320ms cubic-bezier(0.2, 0.7, 0.2, 1),
              width 320ms cubic-bezier(0.2, 0.7, 0.2, 1),
              height 320ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
.tour-focus-ring.is-visible { opacity: 1; }

/* Attention pulse — fires once per beat (JS toggles `.is-pulsing`).
   A second cyan ring expands outward and fades, drawing the eye to the
   newly highlighted region without leaving a continuous animation
   running (which would read as anxious next to actuarial data). */
.tour-focus-ring::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1.5px solid var(--accent);
  opacity: 0;
  pointer-events: none;
}
.tour-focus-ring.is-pulsing::after {
  /* Three back-to-back pulses (~3.6s total), then quiet. One pulse is
     too easy to miss when the user's eye is mid-narration; a continuous
     loop reads as anxious. Three is the sweet spot — definitely seen,
     definitely finite. */
  animation: tourRingPulse 1200ms cubic-bezier(0.2, 0.7, 0.2, 1) 3;
}
@keyframes tourRingPulse {
  0%   { inset: 0;     opacity: 0.7; }
  60%  { inset: -10px; opacity: 0.18; }
  100% { inset: -14px; opacity: 0; }
}

/* — Card: the active narration. Bottom-right, same horizontal slot as
     the toast / pill so the eye doesn't have to track. Slightly larger,
     more chrome (stepper, ticks, controls). — */
/* The card pairs with the focus ring — same cyan border + accent glow so
   both surfaces read as part of one highlighted state. Without this, the
   card's neutral border blends into the dimmed page backdrop and the card
   looks muted next to the spotlit focal element. */
.tour-card {
  position: fixed;
  right: 20px;
  bottom: 84px;
  width: 380px;
  max-width: calc(100vw - 40px);
  background: var(--bg-panel);
  border: 1.5px solid var(--accent);
  border-radius: var(--radius-md);
  padding: 14px 16px 12px;
  z-index: 82;
  box-shadow:
    0 1px 0 rgba(20, 25, 32, 0.04),
    0 16px 40px -8px rgba(20, 25, 32, 0.20),
    0 4px 10px rgba(20, 25, 32, 0.06),
    0 0 18px -2px var(--accent-soft);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 280ms ease, transform 280ms cubic-bezier(0.2, 0.7, 0.2, 1);
  pointer-events: auto;
}
.tour-card.is-visible {
  opacity: 1;
  transform: translateY(0);
}
.tour-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.tour-card-eyebrow {
  flex: 1;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-secondary);
  min-width: 0;
}
.tour-card-step {
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.tour-card-eyebrow-sep {
  color: var(--text-dim);
}
.tour-card-eyebrow-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tour-card-exit {
  background: transparent;
  border: none;
  padding: 4px 6px;
  margin: -4px -6px -4px 0;
  font-size: 13px;
  color: var(--text-dim);
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: color 140ms ease, background 140ms ease;
}
.tour-card-exit:hover {
  color: var(--text-primary);
  background: var(--bg-hover);
}
.tour-card-ticks {
  display: flex;
  gap: 4px;
  margin-bottom: 12px;
}
.tour-card-tick {
  flex: 1;
  height: 2px;
  background: var(--border);
  border-radius: 1px;
  transition: background 240ms ease;
}
.tour-card-tick.is-done { background: var(--accent); }
.tour-card-tick.is-current {
  background: var(--accent);
  position: relative;
}
.tour-card-tick.is-current::after {
  content: '';
  position: absolute;
  inset: -2px -3px;
  border-radius: 2px;
  background: var(--accent-soft);
  z-index: -1;
}
.tour-card-title {
  /* Reading face — the beat headline is language, paired with the body below
     it (the eyebrow above stays mono: it's an ALL-CAPS tracked step label). */
  font-family: var(--font-sans);
  font-size: 18px;
  font-weight: 500;
  letter-spacing: -0.2px;
  line-height: 1.3;
  color: var(--text-primary);
  margin-bottom: 8px;
}
.tour-card-body {
  font-family: var(--font-sans);
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text-primary);
  margin-bottom: 14px;
}
.tour-card-body em {
  font-style: normal;
  font-weight: 600;
  color: var(--text-primary);
  background: var(--accent-soft);
  padding: 0 4px;
  border-radius: 3px;
  font-variant-numeric: tabular-nums;
}
.tour-card-foot {
  display: flex;
  align-items: center;
  gap: 4px;
  padding-top: 8px;
  border-top: 1px solid var(--border-subtle);
}
.tour-card-foot-spacer { flex: 1; }

/* — Tour tab highlight. While a beat is active, the corresponding tab
     gets a persistent accent backplate so the user can see at a glance
     which surface the narration is anchored to. The existing
     `.nav-tab.active` cyan only signals "current tab" — during the tour
     we want a stronger affordance that reads as "tour focus". A one-shot
     icon scale on entry signals the *change*; a softly breathing halo
     keeps the focus visible for the duration of the beat. */
.nav-tab.is-tour-pulse {
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent),
    0 0 0 2px color-mix(in srgb, var(--accent) 14%, transparent);
  animation: tourTabHalo 2400ms ease-in-out infinite;
}
.nav-tab.is-tour-pulse .nav-tab-icon {
  animation: tourIconPulse 1100ms cubic-bezier(0.2, 0.7, 0.2, 1) 1;
}
@keyframes tourIconPulse {
  0%   { transform: scale(1);    filter: drop-shadow(0 0 0 transparent); }
  35%  { transform: scale(1.22); filter: drop-shadow(0 0 8px var(--accent)); }
  100% { transform: scale(1);    filter: drop-shadow(0 0 0 transparent); }
}
@keyframes tourTabHalo {
  0%, 100% { box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 32%, transparent),
    0 0 0 2px color-mix(in srgb, var(--accent) 10%, transparent); }
  50%      { box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 48%, transparent),
    0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .nav-tab.is-tour-pulse { animation: none; }
  .nav-tab.is-tour-pulse .nav-tab-icon { animation: none; }
}

/* Narrow viewports — cards span the bottom edge, ring still tracks.
   The bottom-nav is ~65px tall (taller once a home-indicator safe area
   is added), so clear it with a comfortable gap plus the inset rather
   than parking flush against it. */
@media (max-width: 540px) {
  .tour-toast,
  .tour-card,
  .engagement-toast {
    right: 12px;
    left: 12px;
    width: auto;
    bottom: calc(84px + env(safe-area-inset-bottom, 0px));
  }
  .tour-pill { right: 12px; bottom: calc(84px + env(safe-area-inset-bottom, 0px)); }
}

@media (prefers-reduced-motion: reduce) {
  .tour-toast,
  .tour-pill,
  .tour-card,
  .tour-focus-ring,
  .engagement-toast {
    transition: none;
    animation: none;
  }
}


/* ═══════════════════════════════════════════════════════════════════
   How-it-works 3D stage (landing) — replaces the old flow-diagram SVG.
   A persistent wireframe figure (YOU) on a dark stage panel, reused from
   onboarding (createWireframeModelScene). DOM readouts — threat / energy /
   data motes / Aescle coach orb + speech / life curve — fade per beat off
   .lp-value[data-active]; the 3D pose/build/camera is driven in JS
   (driveScene in setupHowflow). Three beats: 01 discover · 02 act · 03 win.
   ═══════════════════════════════════════════════════════════════════ */
.lp-howflow-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 5;
  border-radius: 20px;
  overflow: hidden;
  isolation: isolate;
  color: var(--text-secondary);
  /* Light, on-brand stage (flat-white HOW chapter). A soft cyan halo sits
     where the figure stands so it reads as a clean scanning light pool, not
     a flat blank card. */
  background:
    radial-gradient(64% 46% at 50% 40%, rgba(8, 145, 178, 0.08) 0%, rgba(8, 145, 178, 0) 72%),
    linear-gradient(180deg, #ffffff 0%, #f3f8fb 100%);
  border: 1px solid var(--border);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.85),
    0 26px 64px -34px rgba(15, 40, 55, 0.22);
}
.lp-howflow-pair > .lp-howflow-stage { margin: 0; }

/* Fixed-aspect composition layer holding the scene + every HUD element. Desktop:
   the stage is 4/5, so inset:0 fills it exactly (rendering is unchanged). Mobile
   (below): re-fitted to a centered 4/5 box so the same desktop % coordinates
   place everything correctly — no per-element mobile position overrides. */
.lp-hf-stage-inner {
  position: absolute;
  inset: 0;
}

.lp-hf-live {
  position: absolute;
  inset: 0;
  z-index: 3;
  transform-origin: 58% 46%;
  transition:
    opacity 760ms ease,
    transform 1600ms cubic-bezier(.18,.82,.18,1),
    filter 900ms ease;
}
.lp-hf-live::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 9;
  width: min(78%, 350px);
  aspect-ratio: 1;
  transform: translate(-50%, -50%) scale(1.18);
  border: 1px solid rgba(8, 145, 178, 0.22);
  border-radius: 12px;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.76),
    0 22px 42px -30px rgba(15, 40, 55, 0.34);
  opacity: 0;
  pointer-events: none;
}
.lp-value[data-active="03"] .lp-hf-live {
  /* WIN: the figure stays the hero — full presence, running — but shifts to the
     lower-left origin of its trajectory so the curve climbs away from it
     instead of arcing over its head. (translate/scale are eyeball-tunable.) */
  transform: translate(-21%, -13%) scale(.4);
  transition: transform 1300ms cubic-bezier(.2, .8, .2, 1);
  /* Anchor the scale at the bottom so the figure shrinks UPWARD with its feet
     planted (not collapsing toward its midpoint). The -13% Y lift raises the
     whole figure (constant across the zoom, so feet stay planted) so the curve
     passes BELOW the feet instead of crossing through the legs. */
  transform-origin: 50% 100%;
  /* Dynamic zoom: start as a near-full hero, then pull WAY out in sync with the
     days accumulating, settling at a dramatically small resting .4 (the life
     trajectory becomes the hero, the figure a small origin point). Both scales
     are <1 (downscale) so the figure stays crisp throughout. */
  animation: lp-hf-figure-zoom 10s ease-in-out infinite;
}
@keyframes lp-hf-figure-zoom {
  /* 10s loop. Hold zoomed-in through the run-alone beat, then pull out with an
     EASE-IN (slow→fast) so the zoom tracks the back-loaded cell fill (exp 9) and
     FINISHES with them (~35%). Then HOLD the small .4 through the long final view
     before easing back in for the loop. Dramatic .82→.4 gap; feet stay planted. */
  0%, 7%   { transform: translate(-21%, -13%) scale(.82); animation-timing-function: cubic-bezier(.6, 0, .85, .5); }
  35%, 90% { transform: translate(-21%, -13%) scale(.4); }
  100%     { transform: translate(-21%, -13%) scale(.82); }
}
/* The act-beat readouts don't belong in the win frame; the figure no longer
   collapses to hide them, so hide them explicitly. */
.lp-value[data-active="03"] .lp-hf-hud { opacity: 0; }

/* 3D canvas mount. The WIN beat no longer scales the figure to a point — it
   recedes (via .lp-hf-live) while the life trajectory charges in front. */
.lp-hf-scene {
  position: absolute;
  inset: 0;
  z-index: 2;
  transform-origin: 50% 47%;
  transition: transform 1100ms cubic-bezier(.5, .08, .2, 1);
}
.lp-value[data-active="03"] .lp-hf-scene {
  transform: none;
}
/* ── Life trajectory (scene 03) — area charging under a faint envelope ─── */
/* Two registered layers: a faint always-present envelope (--ghost) and a
   bright area+line (--charge) drawn left→right by a clip wipe. Sits BEHIND the
   running figure (z 2 < .lp-hf-live z 3) — the hero is in front, the curve
   climbs away from it. */
.lp-hf-curve {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 520ms ease 160ms, transform 1100ms cubic-bezier(.18,.82,.18,1) 140ms;
  pointer-events: none;
}
.lp-hf-curve-line {
  stroke-width: 3;
  vector-effect: non-scaling-stroke;
  /* Tighter glow so the curve's halo doesn't bleed through the faint wireframe
     figure in front of it (the curve sits BEHIND the figure, z 2 < live z 3). */
  filter: drop-shadow(0 0 4px rgba(34, 184, 207, 0.4));
}
/* The bright layer starts fully clipped from the right; the keyframe wipes it
   open left→right, ONCE, then holds (forwards). Ongoing life comes from the
   running figure + the pulsing tip, not a repeating wipe. */
.lp-hf-curve--charge { clip-path: inset(0 100% 0 0); }

.lp-value[data-active="03"] .lp-hf-curve {
  opacity: 1;
  transform: translateY(0);
}
/* Sequence each 10s loop: days fill + figure zooms out (to ~35%), the cells fade
   a notch (~39%), then the trajectory draws (42→58%), the coach congratulates
   (~60%), and the whole final view HOLDS long (58→90%) before a clean fade. */
.lp-value[data-active="03"] .lp-hf-curve--charge {
  animation: lp-hf-curve-charge 10s cubic-bezier(.4, 0, .2, 1) infinite;
}
@keyframes lp-hf-curve-charge {
  0%, 42%   { clip-path: inset(0 100% 0 0); opacity: 1; }
  58%, 90%  { clip-path: inset(0 0 0 0); opacity: 1; }
  /* Hold the bright trajectory through the long final view, then fade for the
     pause before the next loop banks days afresh. */
  94%, 100% { clip-path: inset(0 0 0 0); opacity: 0; }
}

/* Leading-edge node — appears when the wipe reaches the tip, breathes through
   the hold, then fades with the curve at the loop reset. Round HTML element
   (immune to the SVG's non-uniform stretch), parked on the curve's end point. */
.lp-hf-curve-tip {
  position: absolute;
  left: 93%;
  top: 18%;
  z-index: 3;
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: #16a34a;
  opacity: 0;
  pointer-events: none;
}
.lp-value[data-active="03"] .lp-hf-curve-tip {
  animation: lp-hf-tip-loop 10s ease-in-out infinite;
}
@keyframes lp-hf-tip-loop {
  0%, 56%   { opacity: 0; box-shadow: 0 0 0 0 rgba(22, 163, 74, 0); }
  61%       { opacity: 1; box-shadow: 0 0 10px 2px rgba(22, 163, 74, 0.65); }
  74%       { opacity: .78; box-shadow: 0 0 5px 1px rgba(22, 163, 74, 0.35); }
  82%       { opacity: 1; box-shadow: 0 0 10px 2px rgba(22, 163, 74, 0.65); }
  90%       { opacity: 1; box-shadow: 0 0 8px 2px rgba(22, 163, 74, 0.5); }
  94%, 100% { opacity: 0; box-shadow: 0 0 0 0 rgba(22, 163, 74, 0); }
}

/* ── Step 03 "your life is a grid of days": a faint full-background grid of
   square cells. Fills in from the bottom-left (a growing rectangle anchored
   there) as the figure runs — days accumulating into a life — ending as a
   complete grid, then the trajectory rises over it. Sits behind everything
   (z below the curve at 2 and the figure at 3); deliberately subtle. ── */
.lp-hf-grid {
  position: absolute;
  inset: 0;
  /* Backmost layer of the stage — strictly behind the curve (z 2) and the
     figure (.lp-hf-live z 3), so the model and everything else sit on top. */
  z-index: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 500ms ease;
  pointer-events: none;
}
/* Only present on the WIN beat; cells inside are then revealed cell-by-cell. */
.lp-value[data-active="03"] .lp-hf-grid { opacity: 1; }
/* When the curve starts drawing, the whole grid drops a notch (JS adds .is-dimmed)
   so the trajectory reads clearly over a fainter field of days. */
/* Slow, gentle fade-down (not a snap) as the curve takes over. */
.lp-value[data-active="03"] .lp-hf-grid.is-dimmed { opacity: 0.22; transition: opacity 1600ms ease; }
/* One day. Faint + muted so the grid reads as background texture; each cell
   pops on its own when JS adds .is-on (bottom-left → up reading order). */
.lp-hf-day {
  fill: rgba(8, 145, 178, 0.045);
  opacity: 0;
  transform: scale(.55);
  transform-box: fill-box;
  transform-origin: center;
  transition: opacity 220ms ease, transform 220ms cubic-bezier(.3, .8, .3, 1);
}
.lp-hf-day.is-on {
  opacity: 1;
  transform: scale(1);
}

/* ── Data streams (scene 01) — data converging on the figure's core ──── */
/* Wires run from the frame edges to the chest; packets ride them inward
   (SMIL animateMotion in the markup) and a flowing-dash current runs along
   each wire. The whole layer fades in only on beat 01. */
.lp-hf-streams {
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 500ms ease;
  pointer-events: none;
}
/* Hold the wires + data dots back until the six inputs have all popped in
   (~1s of staggered entrance), so the data only starts flowing once the
   sources are present. Fades out immediately on leaving 01 (base transition). */
.lp-value[data-active="01"] .lp-hf-streams {
  opacity: 1;
  transition: opacity 600ms ease 2200ms;
}
.lp-hf-wire {
  fill: none;
  stroke: rgba(8, 145, 178, 0.30);
  stroke-width: .8;
  stroke-linecap: round;
  stroke-dasharray: 1.5 16;
  vector-effect: non-scaling-stroke;
  opacity: 0;
}
/* Dashes flow toward the path end (the core) — data current. */
.lp-value[data-active="01"] .lp-hf-wire {
  opacity: 0.4;
  animation: lp-hf-wire-flow 1.15s linear infinite;
}
@keyframes lp-hf-wire-flow { to { stroke-dashoffset: -17.5; } }
/* The flat 2D dots are retired — the data intake is now a real 3D particle
   stream converging on the chest core (createWireframeModelScene → setStream),
   which has the depth the SVG circles lacked. The faint wires stay as quiet
   connection hints from the source pills to the figure. */
.lp-hf-packet { display: none; }

.lp-hf-source {
  /* Per-input identity hue (set by each --bloodwork/--vitals/… modifier).
     Orthogonal to the severity axis — these say WHICH input, not how bad.
     Defaults to the brand accent so an unkeyed pill stays on-brand. */
  --src: var(--accent);
  position: absolute;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 92px;
  height: 30px;
  padding: 0 12px;
  border-radius: 999px;
  /* Pill chrome stays neutral; only the icon + label carry the hue. */
  background: rgba(255, 255, 255, 0.78);
  border: 1px solid rgba(8, 145, 178, 0.16);
  box-shadow: 0 8px 20px -15px rgba(15, 40, 55, 0.28);
  color: var(--src);
  opacity: 0;
  transform: translateY(4px) scale(.96);
  transition: opacity 380ms ease, transform 560ms cubic-bezier(.3, .8, .3, 1);
  pointer-events: none;
}
.lp-hf-source svg {
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.35;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: .9;
}
.lp-hf-source span {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.15px;
  /* Labels are NEUTRAL ink — only the icon carries the identity hue. Six colored
     WORDS read as six headlines and steal focus from the gold insight bulb; a
     uniform mono ring + a small colored icon dab keeps identity without the noise. */
  color: var(--text-secondary);
}
/* The six inputs pop in one-by-one, clockwise around the figure — a circular
   "pulling it all in" intake. `both` holds them open while beat 01 is active;
   leaving 01 drops the rule and the base transition fades them out. */
.lp-value[data-active="01"] .lp-hf-source {
  animation: lp-hf-source-in 620ms cubic-bezier(.3, .8, .3, 1) both;
}
@keyframes lp-hf-source-in {
  0% { opacity: 0; transform: translateY(6px) scale(.9); }
  100% { opacity: .82; transform: translateY(0) scale(1); }
}
.lp-value[data-active="01"] .lp-hf-source--vitals       { animation-delay: 0ms; }
.lp-value[data-active="01"] .lp-hf-source--history      { animation-delay: 320ms; }
.lp-value[data-active="01"] .lp-hf-source--mood         { animation-delay: 640ms; }
.lp-value[data-active="01"] .lp-hf-source--demographics { animation-delay: 960ms; }
.lp-value[data-active="01"] .lp-hf-source--genome       { animation-delay: 1280ms; }
.lp-value[data-active="01"] .lp-hf-source--bloodwork    { animation-delay: 1600ms; }
/* Identity hues (icon-only now), spread around the wheel so spatially-adjacent
   pills (clockwise: bloodwork → vitals → history → mood → demographics → genome)
   never share a neighbour's hue. Desaturated so the icon dabs sit BACK as a
   supporting ring — and the two warm offenders pulled off the gold band
   (crimson → cool rose, amber → taupe-bronze) so nothing competes with the
   insight bulb's gold for the "one warm thing" focal slot. */
.lp-hf-source--bloodwork    { top: 12%; left: 9%;  --src: #b14a63; }
.lp-hf-source--vitals       { top: 11%; right: 8%; --src: #2f8f86; }
.lp-hf-source--genome       { top: 39%; left: 6%;  --src: #7d62ad; }
.lp-hf-source--history      { top: 43%; right: 6%; --src: #8a7250; }
.lp-hf-source--demographics { left: 8%; bottom: 22%; --src: #4570c4; }
.lp-hf-source--mood         { right: 10%; bottom: 23%; --src: #a84bb3; }

/* ── Output readouts — threat + energy emitted from the formed profile ─ */
.lp-hf-hud {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  transition: opacity 500ms ease;
}
/* Act-3 happy emote — a smiley that pops above the figure's head once the
   readouts improve, while the figure keeps walking. */
.lp-hf-emote {
  position: absolute;
  top: 13%;
  left: 50%;
  margin-left: -16px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: #ffffff;
  border: 1px solid var(--green, #16a34a);
  box-shadow: 0 8px 20px -12px rgba(15, 40, 55, 0.4);
  color: var(--green, #16a34a);
  opacity: 0;
}
.lp-hf-emote svg { width: 20px; height: 20px; }
.lp-value[data-active="02"] .lp-hf-emote {
  animation: lp-hf-emote-pop 10s ease-in-out infinite;
}
@keyframes lp-hf-emote-pop {
  /* Pops as the energy finishes charging green (~54%), then HOLDS through the
     long success beat (the figure walking, all rewards up) before clearing. */
  0%, 54% { opacity: 0; transform: translateY(6px) scale(.5); }
  60%, 86% { opacity: 1; transform: translateY(0) scale(1); }
  92%, 100% { opacity: 0; transform: translateY(-4px) scale(.85); }
}

/* ── Reward "+years" chip — the modifiable-life payoff cashed as an
   "achievement unlocked" badge, paired beneath the happy emote. Gain reads
   on the leverage/cool axis (green), kept clear of the severity reds. ── */
.lp-hf-gain {
  /* Upper-left toast — clears the figure's head and balances the orb on the
     upper-right; the source pills are gone by the time this beat plays. */
  position: absolute;
  top: 16%;
  left: 6%;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px 5px 9px;
  border-radius: 999px;
  background: linear-gradient(135deg, color-mix(in srgb, var(--green) 13%, #fff), #fff 70%);
  border: 1px solid color-mix(in srgb, var(--green) 44%, transparent);
  box-shadow:
    0 10px 24px -13px color-mix(in srgb, var(--green) 72%, transparent),
    inset 0 1px 0 rgba(255, 255, 255, 0.7);
  color: var(--green, #059669);
  white-space: nowrap;
  overflow: hidden;
  opacity: 0;
}
.lp-hf-gain-icon { width: 13px; height: 13px; flex: 0 0 auto; }
.lp-hf-gain-val {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .6px;
}
/* One-shot gloss that crosses the badge as it lands — the "unlocked" shine. */
.lp-hf-gain-shine {
  position: absolute;
  top: 0;
  bottom: 0;
  left: -45%;
  width: 38%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.85), transparent);
  transform: skewX(-18deg);
  opacity: 0;
}
.lp-value[data-active="02"] .lp-hf-gain {
  animation: lp-hf-gain-pop 10s cubic-bezier(.2, .7, .3, 1) infinite;
}
.lp-value[data-active="02"] .lp-hf-gain-shine {
  animation: lp-hf-gain-shine 10s ease-in-out infinite;
}
@keyframes lp-hf-gain-pop {
  /* Lands just after the happy emote (~58%) with a slight overshoot, holds
     through the success beat, then clears with the emote (~92%). */
  0%, 57% { opacity: 0; transform: translateY(8px) scale(.6); }
  62% { opacity: 1; transform: translateY(0) scale(1.12); }
  66%, 86% { opacity: 1; transform: translateY(0) scale(1); }
  92%, 100% { opacity: 0; transform: translateY(-5px) scale(.9); }
}
@keyframes lp-hf-gain-shine {
  0%, 60% { left: -45%; opacity: 0; }
  63% { opacity: 1; }
  73% { left: 108%; opacity: 0; }
  100% { left: 108%; opacity: 0; }
}
.lp-hf-threat,
.lp-hf-battery {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 124px;
  padding: 10px 12px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.82);
  border: 1px solid rgba(15, 40, 55, 0.1);
  box-shadow: 0 14px 32px -20px rgba(15, 40, 55, 0.34);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  transition: opacity 420ms ease, transform 560ms cubic-bezier(.3, .8, .3, 1), filter 420ms ease;
}
.lp-hf-threat {
  left: 3%;
  top: 58%;
  transform: translate(-10px, -50%) scale(.96);
}
.lp-hf-battery {
  right: 3%;
  top: 54%;
  transform: translate(10px, -50%) scale(.96);
}
.lp-value[data-active="02"] .lp-hf-threat,
.lp-value[data-active="02"] .lp-hf-battery {
  opacity: 1;
  transform: translate(0, -50%) scale(1);
}
.lp-value[data-active="03"] .lp-hf-threat,
.lp-value[data-active="03"] .lp-hf-battery {
  opacity: .92;
  transform: translate(0, -50%) scale(.86);
  transition: opacity 420ms ease, transform 900ms cubic-bezier(.3,.8,.2,1);
}
.lp-hf-threat::before,
.lp-hf-battery::before {
  content: "";
  position: absolute;
  top: 50%;
  width: 42px;
  height: 1px;
  background: linear-gradient(90deg, rgba(8, 145, 178, 0), rgba(8, 145, 178, .42));
}
.lp-hf-threat::before {
  right: -42px;
  transform: translateY(-50%);
}
.lp-hf-battery::before {
  left: -42px;
  transform: translateY(-50%) rotate(180deg);
}

/* ── Threat readout (longevity axis) — left of the HUD ───────────────── */
.lp-hf-threat-icon { width: 20px; height: 20px; color: var(--red, #dc2626); }
.lp-hf-readout-copy {
  display: grid;
  gap: 3px;
}
.lp-hf-readout-kicker {
  font-family: var(--font-mono);
  font-size: 7.5px;
  font-weight: 600;
  letter-spacing: 1px;
  color: var(--text-dim);
}
.lp-hf-threat-label {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 1.4px;
  color: var(--red, #dc2626);
}
/* Act: findings appear, then recede slightly as action begins. */
.lp-value[data-active="02"] .lp-hf-threat {
  animation: lp-hf-finding-stage 10s ease-in-out infinite;
}
@keyframes lp-hf-threat-pulse {
  0%, 100% { filter: drop-shadow(0 0 0 rgba(220, 38, 38, 0)); }
  50% { filter: drop-shadow(0 0 5px rgba(220, 38, 38, 0.55)); }
}
@keyframes lp-hf-finding-stage {
  /* 10s loop. Surfaces red right after the "!", holds through the walk (~26%)
     and a clear gap, recedes in the reward (40→48%), then HOLDS receded through
     the long success beat (48→86%) before fading for the back-end empty. */
  0%, 3% { opacity: 0; transform: translate(-8px, -50%) scale(.96); filter: none; }
  8%, 40% { opacity: 1; transform: translate(0, -50%) scale(1); filter: drop-shadow(0 0 5px rgba(220, 38, 38, 0.35)); }
  48%, 86% { opacity: .5; transform: translate(0, -50%) scale(.9); filter: grayscale(.6) hue-rotate(80deg); }
  90%, 100% { opacity: 0; transform: translate(-8px, -50%) scale(.9); filter: grayscale(.6) hue-rotate(80deg); }
}

/* ── Energy readout (performance axis) — segmented battery, right of HUD ─ */
/* Pack the label + battery together (no space-between spreading them wide) and
   drop the shared min-width so the card hugs its content rather than the
   threat card's width. */
.lp-hf-battery {
  justify-content: flex-start;
  min-width: 0;
}
.lp-hf-battery-label {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 1.4px;
  color: var(--amber, #f59e0b);
  flex-shrink: 0;
}
.lp-hf-battery-wrap {
  display: flex;
  align-items: center;
}
.lp-hf-battery-shell {
  display: flex;
  align-items: center;
  gap: 2px;
  height: 15px;
  padding: 2px;
  border: 1.4px solid var(--text-dim);
  border-radius: 3px;
  transition: border-color 500ms ease, box-shadow 500ms ease;
}
.lp-hf-battery-nub {
  width: 2px;
  height: 6px;
  margin-left: -5px;
  border-radius: 0 2px 2px 0;
  background: var(--text-dim);
  transition: background 500ms ease;
}
.lp-hf-cell {
  width: 6px;
  height: 100%;
  border-radius: 1px;
  background: rgba(15, 40, 55, 0.1);
  transition: background 500ms ease;
}
/* Act: charges to 4 cells green. */
.lp-value[data-active="02"] .lp-hf-battery {
  animation: lp-hf-energy-stage 10s ease-in-out infinite;
}
/* The energy readout surfaces amber/dim (current low state) and only turns
   green AS it charges in act 3 — in lockstep with the cells flipping at 70–80%.
   The initial element is never green. */
.lp-value[data-active="02"] .lp-hf-battery-shell {
  animation: lp-hf-battery-shell-charge 10s ease-in-out infinite;
}
.lp-value[data-active="02"] .lp-hf-battery-label {
  animation: lp-hf-battery-label-charge 10s ease-in-out infinite;
}
.lp-value[data-active="02"] .lp-hf-battery-nub {
  animation: lp-hf-battery-nub-charge 10s ease-in-out infinite;
}
@keyframes lp-hf-battery-shell-charge {
  0%, 48% { border-color: var(--text-dim); }
  54%, 100% { border-color: var(--green, #059669); }
}
@keyframes lp-hf-battery-label-charge {
  0%, 48% { color: var(--amber, #f59e0b); }
  54%, 100% { color: var(--green, #059669); }
}
@keyframes lp-hf-battery-nub-charge {
  0%, 48% { background: var(--text-dim); }
  54%, 100% { background: var(--green, #059669); }
}
.lp-value[data-active="03"] .lp-hf-battery-shell { border-color: var(--green, #059669); }
.lp-value[data-active="03"] .lp-hf-battery-label { color: var(--green, #059669); }
.lp-value[data-active="03"] .lp-hf-battery-nub { background: var(--green, #059669); }
.lp-value[data-active="03"] .lp-hf-cell:nth-child(-n+4) { background: var(--green, #059669); }
.lp-value[data-active="02"] .lp-hf-cell:nth-child(-n+2) {
  background: var(--amber, #f59e0b);
  animation: lp-hf-cell-to-green 10s ease-in-out infinite;
}
.lp-value[data-active="02"] .lp-hf-cell:nth-child(n+3):nth-child(-n+4) {
  animation: lp-hf-cell-fill-late 10s ease-in-out infinite;
}
@keyframes lp-hf-cell-flicker {
  0%, 100% { opacity: .55; }
  50% { opacity: 1; }
}
@keyframes lp-hf-cell-pulse {
  0%, 100% { box-shadow: 0 0 6px -1px rgba(5, 150, 105, 0.4); }
  50% { box-shadow: 0 0 13px 0 rgba(5, 150, 105, 0.7); }
}
@keyframes lp-hf-energy-stage {
  /* Surfaces amber after the "!", holds through the walk, charges green in the
     reward (~54%), HOLDS green through the long success beat, then fades. */
  0%, 4% { opacity: 0; transform: translate(8px, -50%) scale(.96); }
  9%, 86% { opacity: 1; transform: translate(0, -50%) scale(1); }
  90%, 100% { opacity: 0; transform: translate(8px, -50%) scale(.96); }
}
@keyframes lp-hf-cell-to-green {
  0%, 48% { background: var(--amber, #f59e0b); }
  54%, 100% { background: var(--green, #059669); }
}
@keyframes lp-hf-cell-fill-late {
  0%, 48% { background: rgba(15, 40, 55, 0.1); }
  54%, 100% { background: var(--green, #059669); }
}

/* ── Aescle coach orb + speech bubble — Navi, hugging the figure ─────── */
/* Per-beat position is driven by top/left (transitioned) so the float bob
   (a transform animation) composes on top without conflict. */
.lp-hf-orb {
  position: absolute;
  top: 15%;
  left: 62%;
  z-index: 6;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  perspective: 180px;
  opacity: 0;
  animation: lp-hf-orb-float 5s ease-in-out infinite;
  transition:
    opacity 360ms ease,
    top 900ms cubic-bezier(.4, .1, .2, 1),
    left 900ms cubic-bezier(.4, .1, .2, 1);
}
/* Darts in toward the torso as it speaks; settles up at the win. */
.lp-value[data-active="01"] .lp-hf-orb,
.lp-value[data-active="02"] .lp-hf-orb { opacity: 1; }
.lp-value[data-active="01"] .lp-hf-orb { top: 20%; left: 69%; }
.lp-value[data-active="02"] .lp-hf-orb { top: 24%; left: 68%; }
/* Step 03: the coach STAYS with the figure the whole beat — present and
   floating, parked upper-right like the other steps — then cheers (a glow pulse
   + the confetti burst) once the trajectory finishes drawing (~2s). */
.lp-value[data-active="03"] .lp-hf-orb {
  top: 18%;
  left: 66%;
  opacity: 1;
  /* The orb lives INSIDE .lp-hf-live, so it tracks + shrinks fully with the
     figure (down to .4). Its confetti + congrats are counter-scaled below so
     they stay readable despite the tiny coach. */
}
/* Cheer glow, looped on the 7s cycle: a quick double pulse right after the
   trajectory finishes drawing (~24%), then quiet until the next loop. */
.lp-value[data-active="03"] .lp-hf-orb-mark {
  animation: lp-hf-coach-cheer 10s ease infinite;
}
@keyframes lp-hf-coach-cheer {
  0%, 58% { filter: drop-shadow(0 0 4px rgba(8, 145, 178, 0.32)); }
  63% { filter: drop-shadow(0 0 13px rgba(8, 145, 178, 0.85)); }
  68% { filter: drop-shadow(0 0 4px rgba(8, 145, 178, 0.32)); }
  73% { filter: drop-shadow(0 0 13px rgba(8, 145, 178, 0.85)); }
  79%, 100% { filter: drop-shadow(0 0 4px rgba(8, 145, 178, 0.32)); }
}
.lp-hf-orb-body {
  position: absolute;
  inset: 4px 5px 5px 4px;
  border-radius: 48% 54% 50% 46% / 45% 50% 55% 52%;
  background:
    radial-gradient(circle at 36% 28%, rgba(255, 255, 255, 0.9) 0 7%, transparent 8%),
    radial-gradient(circle at 54% 56%, rgba(8, 145, 178, 0.12), rgba(8, 145, 178, 0) 62%),
    rgba(255, 255, 255, 0.34);
  border: 1px solid rgba(8, 145, 178, 0.42);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.58),
    inset -4px -6px 12px rgba(8, 145, 178, 0.08),
    0 10px 18px -15px rgba(15, 40, 55, 0.36),
    0 0 14px rgba(8, 145, 178, 0.16);
  transform: rotateX(8deg) rotateY(-12deg) rotateZ(-7deg);
}
.lp-hf-orb-body::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -8px;
  width: 26px;
  height: 9px;
  transform: translateX(-50%);
  border-radius: 999px;
  background: rgba(8, 145, 178, 0.13);
  filter: blur(5px);
}
.lp-hf-orb-mark {
  position: relative;
  width: 28px;
  height: 28px;
  opacity: .82;
  filter: drop-shadow(0 0 4px rgba(8, 145, 178, 0.32));
}
.lp-hf-processing {
  position: absolute;
  /* Sits clearly OUTSIDE the coach, up and to the left. Far enough out that even
     the deepest trailing dot (::after, right:-25/bottom:-26) only reaches the
     orb's top-left CORNER — a thought floating off the coach, dots bridging back
     to it, nothing inside it. */
  left: -80px;
  top: -66px;
  width: 54px;
  height: 40px;
  color: var(--accent);
  opacity: 0;
  transform-origin: 50% 50%;
  overflow: visible;
  filter: drop-shadow(0 10px 18px rgba(15, 40, 55, 0.11));
}
.lp-hf-processing::before,
.lp-hf-processing::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 0 0 1px rgba(8, 145, 178, 0.14);
}
.lp-hf-processing::before {
  right: -16px;
  bottom: -16px;
  width: 9px;
  height: 9px;
}
.lp-hf-processing::after {
  right: -25px;
  bottom: -26px;
  width: 5px;
  height: 5px;
}
.lp-hf-processing-lobe {
  position: absolute;
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 0 0 1px rgba(8, 145, 178, 0.16);
}
.lp-hf-processing-lobe--a {
  left: 4px;
  top: 9px;
  width: 37px;
  height: 25px;
  border-radius: 16px;
  z-index: 1;
}
.lp-hf-processing-lobe--b {
  left: 15px;
  top: 1px;
  width: 26px;
  height: 25px;
  border-radius: 50%;
  z-index: 2;
}
.lp-hf-processing-lobe--c {
  left: 29px;
  top: 10px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  z-index: 3;
}
.lp-hf-processing-lobe--c::after {
  content: "";
  position: absolute;
  left: -24px;
  top: 4px;
  width: 38px;
  height: 14px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.9);
}
.lp-hf-processing-cog {
  position: absolute;
  left: 15px;
  top: 8px;
  z-index: 4;
  width: 26px;
  height: 26px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.55;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Scan pulse while discovering. */
.lp-value[data-active="01"] .lp-hf-orb-mark {
  animation: lp-hf-orb-scan 1.8s ease-in-out infinite;
}
/* The coach is present scanning from the start, but its "processing" thought
   cloud only pops once the data has begun streaming in (~2.9s, after the pills
   and the wires) — the coach starts crunching what just arrived. */
.lp-value[data-active="01"] .lp-hf-processing {
  animation: lp-hf-thought-pop 720ms cubic-bezier(.3, .9, .3, 1.08) 2900ms both;
}
@keyframes lp-hf-orb-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-7px); }
}
@keyframes lp-hf-orb-scan {
  0%, 100% { filter: drop-shadow(0 0 6px rgba(8, 145, 178, 0.4)); }
  50% { filter: drop-shadow(0 0 14px rgba(8, 145, 178, 0.85)); }
}
@keyframes lp-hf-thought-pop {
  0% { opacity: 0; transform: translate(5px, 5px) scale(.76); }
  100% { opacity: 1; transform: translate(0, 0) scale(1); }
}
.lp-value[data-active="01"] .lp-hf-processing-cog {
  transform-origin: 50% 50%;
  animation: lp-hf-cog-spin 4.2s linear infinite;
  transition: opacity 260ms ease;
}
@keyframes lp-hf-cog-spin { to { transform: rotate(360deg); } }
/* Insight glyph (lightbulb) — overlays the cog's spot, hidden until the coach
   uncovers a signal. .is-insight cross-fades cog→bulb and pops the bulb with a
   discovery glow; setupHowflow toggles .is-insight at randomized intervals. */
.lp-hf-processing-insight {
  position: absolute;
  left: 14px;
  top: 7px;
  z-index: 5;
  width: 28px;
  height: 28px;
  /* Gold "idea" bulb — a warm, lit discovery that pops against the cool cyan
     cog/figure. The lightbulb shape keeps it reading as insight, not alert. */
  fill: rgba(251, 191, 36, 0.28);
  stroke: #f59e0b;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
  transform: scale(.4);
  transform-origin: 50% 60%;
  transition: opacity 200ms ease, transform 380ms cubic-bezier(.3, 1.5, .4, 1);
}
.lp-hf-processing.is-insight .lp-hf-processing-cog { opacity: 0; }
.lp-hf-processing.is-insight .lp-hf-processing-insight {
  opacity: 1;
  transform: scale(1);
  /* Pulse the gold glow while shown so it grabs the eye — a found idea, lit up. */
  animation: lp-hf-insight-glow 1.15s ease-in-out;
}
@keyframes lp-hf-insight-glow {
  0% { filter: drop-shadow(0 0 0 rgba(245, 158, 11, 0)); }
  30% { filter: drop-shadow(0 0 13px rgba(251, 191, 36, 0.95)); }
  60% { filter: drop-shadow(0 0 6px rgba(251, 191, 36, 0.55)); }
  80% { filter: drop-shadow(0 0 13px rgba(251, 191, 36, 0.9)); }
  100% { filter: drop-shadow(0 0 7px rgba(251, 191, 36, 0.65)); }
}
/* ── Eureka beat (02): coach lands on a finding and thinks a "!" before the
   two insights surface. Reuses 01's thought cloud, swapping cog → bang. ── */
.lp-hf-processing-bang {
  position: absolute;
  left: 15px;
  top: 6px;
  z-index: 4;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 17px;
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
  opacity: 0;
}
.lp-value[data-active="02"] .lp-hf-processing {
  opacity: 1;
  animation: lp-hf-eureka-stage 10s ease-in-out infinite;
}
.lp-value[data-active="02"] .lp-hf-processing-cog { opacity: 0; }
.lp-value[data-active="02"] .lp-hf-processing-bang {
  opacity: 1;
  animation: lp-hf-bang-pop 10s ease-in-out infinite;
}
/* Act 1: the "!" pops and HOLDS while the two warnings surface beneath it,
   then gives way as the figure starts moving in act 2. */
@keyframes lp-hf-eureka-stage {
  /* The "!" pops IMMEDIATELY at loop start (first play opens on the insight),
     holds while the warnings surface, then gives way (~33%) to the recommend.
     The empty pause is on the back end (after the reward), not here. */
  0% { opacity: 0; transform: translate(6px, 8px) scale(.7); }
  5%, 20% { opacity: 1; transform: translate(0, 0) scale(1); }
  26%, 100% { opacity: 0; transform: translate(0, -6px) scale(.9); }
}
@keyframes lp-hf-bang-pop {
  0% { transform: scale(.5); }
  6% { transform: scale(1.18); }
  12%, 100% { transform: scale(1); }
}
/* Sits above-left of the orb — where the thought cloud appears — so the
   coach's "thinking" and "speaking" read from the same spot. */
.lp-hf-speech {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 16px);
  transform: translateX(-72%) scale(.6);
  transform-origin: 62% 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 38px;
  border-radius: 12px;
  background: #ffffff;
  border: 1px solid var(--border);
  box-shadow: 0 8px 22px -10px rgba(15, 40, 55, 0.35);
  color: var(--accent);
  opacity: 0;
  transition: opacity 360ms ease, transform 480ms cubic-bezier(.3, .9, .3, 1.2);
}
/* little tail pointing down toward the orb */
.lp-hf-speech::after {
  content: "";
  position: absolute;
  left: 62%;
  bottom: -4px;
  width: 9px;
  height: 9px;
  transform: translateX(-50%) rotate(45deg);
  background: #ffffff;
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.lp-hf-speech-icon { width: 22px; height: 22px; position: relative; z-index: 1; }
/* Glyphs share the bubble: the walk recommendation (step 02), an early CHEER
   and a congratulation (step 03). One shows per beat. */
.lp-hf-speech-icon--win { display: none; color: var(--green, #16a34a); }
/* Supportive cyan (the brand/coach hue), not an alarmist warm — applause, not
   a warning. */
.lp-hf-speech-icon--cheer { display: none; color: var(--accent, #0891b2); }
.lp-value[data-active="03"] .lp-hf-speech-icon--walk { display: none; }
/* Step 03 runs two beats off one bubble: stack the cheer + win glyphs and
   cross-fade them on the loop (positions overlap; opacity is keyframed). */
.lp-value[data-active="03"] .lp-hf-speech-icon--cheer,
.lp-value[data-active="03"] .lp-hf-speech-icon--win {
  display: block;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  opacity: 0;
}
.lp-value[data-active="03"] .lp-hf-speech-icon--cheer {
  animation: lp-hf-icon-cheer 10s ease-in-out infinite;
}
.lp-value[data-active="03"] .lp-hf-speech-icon--win {
  animation: lp-hf-icon-win 10s ease-in-out infinite;
}
@keyframes lp-hf-icon-cheer {
  0%, 10% { opacity: 0; }
  16%, 40% { opacity: 1; }
  46%, 100% { opacity: 0; }
}
@keyframes lp-hf-icon-win {
  0%, 60% { opacity: 0; }
  66%, 90% { opacity: 1; }
  94%, 100% { opacity: 0; }
}

/* Act 2: the coach speaks the walk recommendation; the bubble clears before
   the improvement lands in act 3. */
.lp-value[data-active="02"] .lp-hf-speech {
  animation: lp-hf-speech-stage 10s ease-in-out infinite;
}
@keyframes lp-hf-speech-stage {
  /* Recommends right after the "!" gives way (~26%), in step with the walk
     starting (~26%); clears before the reward lands. */
  0%, 26% { opacity: 0; transform: translateX(-72%) scale(.6); }
  30%, 40% { opacity: 1; transform: translateX(-72%) scale(1); }
  46%, 100% { opacity: 0; transform: translateX(-72%) scale(.72); }
}
/* Step 03: the coach congratulates in a bubble, looped on the 7s cycle —
   appears just after the trajectory finishes drawing (~26%), holds, then clears
   before the loop resets. Pairs with the confetti burst. */
.lp-value[data-active="03"] .lp-hf-speech {
  animation: lp-hf-congrats-speech 10s ease-in-out infinite;
}
@keyframes lp-hf-congrats-speech {
  /* Two humps on one bubble: an early CHEER while the figure starts running,
     then it clears, then the congratulation appears as the curve finishes and
     HOLDS through the long final view. The held scale (2.2–2.4) counter-acts
     the tiny .4 coach (net ~1.0) so both beats read at full size. */
  0%, 8%    { opacity: 0; transform: translateX(-72%) scale(1.4); }
  15%, 40%  { opacity: 1; transform: translateX(-72%) scale(2.2); }
  47%, 59%  { opacity: 0; transform: translateX(-72%) scale(1.6); }
  66%, 90%  { opacity: 1; transform: translateX(-72%) scale(2.4); }
  94%, 100% { opacity: 0; transform: translateX(-72%) scale(1.7); }
}
/* Step 03 congratulation pairs a confetti burst with the speech bubble above.
   Particles originate at the coach orb and fly up/outward, looped on the 7s
   cycle, erupting just after the trajectory finishes drawing. */
.lp-hf-confetti {
  position: absolute;
  left: 50%;
  top: 46%;
  width: 0;
  height: 0;
  z-index: 7;
  pointer-events: none;
}
/* Counter-scale the burst so it reads full-size from the tiny .4 coach (it only
   fires during the .4 rest hold, so a static counter nets ~1.0). */
.lp-value[data-active="03"] .lp-hf-confetti { transform: scale(2.4); }
.lp-hf-confetti i {
  position: absolute;
  left: 0;
  top: 0;
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 1px;
  opacity: 0;
}
/* Burst looped on the 7s cycle: erupts right after the draw (~24%), settles,
   then waits for the next loop. */
.lp-value[data-active="03"] .lp-hf-confetti i {
  animation: lp-hf-confetti-pop 10s cubic-bezier(.2, .7, .3, 1) infinite;
}
@keyframes lp-hf-confetti-pop {
  0%, 60% { opacity: 0; transform: translate(0, 0) rotate(0deg) scale(.3); }
  67% { opacity: 1; }
  90%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--rot)) scale(1); }
}
.lp-hf-confetti i:nth-child(1)  { background: var(--accent);        --dx: -30px; --dy: -34px; --rot: 200deg; }
.lp-hf-confetti i:nth-child(2)  { background: var(--green, #16a34a); --dx: -16px; --dy: -46px; --rot: -160deg; }
.lp-hf-confetti i:nth-child(3)  { background: var(--amber, #f59e0b); --dx: 2px;   --dy: -50px; --rot: 140deg; }
.lp-hf-confetti i:nth-child(4)  { background: var(--accent);        --dx: 20px;  --dy: -44px; --rot: -120deg; }
.lp-hf-confetti i:nth-child(5)  { background: var(--green, #16a34a); --dx: 34px;  --dy: -30px; --rot: 220deg; }
.lp-hf-confetti i:nth-child(6)  { background: var(--amber, #f59e0b); --dx: -40px; --dy: -12px; --rot: -200deg; }
.lp-hf-confetti i:nth-child(7)  { background: var(--accent);        --dx: 42px;  --dy: -8px;  --rot: 180deg; }
.lp-hf-confetti i:nth-child(8)  { background: var(--green, #16a34a); --dx: -24px; --dy: 14px;  --rot: -150deg; }
.lp-hf-confetti i:nth-child(9)  { background: var(--amber, #f59e0b); --dx: 26px;  --dy: 16px;  --rot: 160deg; }
.lp-hf-confetti i:nth-child(10) { background: var(--accent);        --dx: 8px;   --dy: -22px; --rot: -180deg; }

/* Mobile scrollytelling travel spacer — inert on desktop. */
.lp-howflow-scroll-spacer { display: none; }

/* ── Mobile: sticky scrollytelling ────────────────────────────────────────
   The whole pair (stage + step list) locks in place as one unit
   (`position: sticky`), and the active step is driven by scroll position
   through `.lp-howflow-scroll-spacer` — the empty travel the pinned pair
   scrolls against (its height is read by setupHowflow). Reverse-scrolling
   steps back through the sequence symmetrically; native scroll owns the
   physics, no gesture-hijacking. Desktop is untouched. */
@media (max-width: 880px) {
  .lp-howflow-pair {
    display: block;
    position: sticky;
    top: 82px;
    z-index: 2;
  }
  .lp-howflow-stage {
    position: relative;
    aspect-ratio: auto;
    /* Taller (more portrait) so the 4/5 composition box is bigger — the fixed-px
       pills then have room around the figure instead of crowding it. */
    height: clamp(340px, 52svh, 460px);
    margin: 0 auto 28px;
    z-index: 3;
    /* Center the composition box with flexbox (NOT a transform) — a translate
       would land the 3D canvas on half-pixels and blur it. */
    display: flex;
    align-items: center;
    justify-content: center;
    /* Decorative on mobile (no taps/hovers drive the steps here — that's
       desktop-only). The 3D canvas's OrbitControls sets touch-action:none,
       which would swallow a swipe that starts on the diagram; pass touches
       through to the scroller so scrollytelling works from anywhere. */
    pointer-events: none;
  }
  .lp-howflow-pair > .lp-howflow-stage {
    margin: 0 auto 28px;
  }
  /* Re-fit the composition to a 4/5 box inside the mobile card (flex-centered by
     the stage above), so every child's desktop % position resolves against the
     same aspect — a scaled-down desktop, no per-element repositioning, no blur. */
  .lp-hf-stage-inner {
    position: relative;
    inset: auto;
    height: 100%;
    width: auto;
    aspect-ratio: 4 / 5;
  }
  .lp-howflow-scroll-spacer {
    display: block;
    height: 210svh;
  }
  .lp-hf-source {
    min-width: 0;
    height: 24px;
    padding: 0 8px;
  }
  .lp-hf-source svg {
    width: 12px;
    height: 12px;
  }
  .lp-hf-source span {
    font-size: 8.5px;
    letter-spacing: .85px;
  }
  /* NOTE: element POSITIONS (source pills, orb, emote, threat/battery) are no
     longer overridden here — the fixed-aspect .lp-hf-stage-inner makes the
     desktop % coordinates correct on mobile too. Only SIZES are tuned below. */
  .lp-hf-threat,
  .lp-hf-battery {
    min-width: 104px;
    padding: 8px 9px;
  }
  .lp-hf-threat::before,
  .lp-hf-battery::before {
    width: 26px;
  }
  .lp-hf-threat::before { right: -26px; }
  .lp-hf-battery::before { left: -26px; }
  .lp-hf-readout-kicker { display: none; }
  .lp-howflow-aside {
    display: block;
    position: relative;
    min-height: 196px;
    padding-top: 24px;
    z-index: 4;
  }
  .lp-howflow-row,
  .lp-howflow-row:last-child {
    border: 0;
    min-height: 0;
    display: none;
    opacity: 0;
    transform: translateY(10px);
    transition: opacity 220ms ease, transform 260ms cubic-bezier(.3,.8,.2,1);
  }
  .lp-howflow-row-head { padding: 8px 4px 12px !important; }
  .lp-howflow-row-body-wrap {
    max-height: none !important;
    opacity: 1 !important;
    overflow: visible !important;
  }
  .lp-howflow-row-body-wrap > .lp-howflow-row-body {
    padding-bottom: 0 !important;
  }
  .lp-value[data-active="01"] .lp-howflow-row[data-card="01"],
  .lp-value[data-active="02"] .lp-howflow-row[data-card="02"],
  .lp-value[data-active="03"] .lp-howflow-row[data-card="03"] {
    display: block;
    opacity: 1;
    transform: translateY(0);
  }
  .lp-value:not([data-active]) .lp-howflow-row[data-card="01"] {
    display: block;
    opacity: .48;
    transform: translateY(0);
  }
}

/* ── Reduced motion: hold a representative mid-beat, no looping motion ── */
/* JS holds beat "02" statically, so the data-stream layer is already hidden
   (it only shows on 01); freeze every looping decoration here. */
@media (prefers-reduced-motion: reduce) {
  .lp-hf-wire,
  .lp-hf-orb,
  .lp-hf-orb-mark,
  .lp-hf-scene,
  .lp-hf-threat,
  .lp-hf-battery,
  .lp-hf-cell,
  .lp-hf-speech,
  .lp-hf-emote,
  .lp-hf-gain,
  .lp-hf-gain-shine,
  .lp-hf-processing,
  .lp-hf-processing-bang,
  .lp-hf-battery-shell,
  .lp-hf-battery-label,
  .lp-hf-battery-nub {
    animation: none !important;
  }
  /* No looping motion to charge the readout, so hold its composed end-state
     (energy high / green) rather than the amber initial frame. */
  .lp-value[data-active="02"] .lp-hf-battery-shell { border-color: var(--green, #059669); }
  .lp-value[data-active="02"] .lp-hf-battery-label { color: var(--green, #059669); }
  .lp-value[data-active="02"] .lp-hf-battery-nub { background: var(--green, #059669); }
  .lp-value[data-active="02"] .lp-hf-cell:nth-child(-n+4) { background: var(--green, #059669); }
  /* The "!" eureka is a transient pre-insight beat — don't freeze it on screen. */
  .lp-value[data-active="02"] .lp-hf-processing { opacity: 0; }
  /* The grid's cell-by-cell reveal is JS-driven and never runs in reduced
     motion, so hold the composed end-state: the full grid of days, statically. */
  .lp-value[data-active="03"] .lp-hf-day {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
