/* ============================================================================
   Span AI, the homepage's data world

   landing.html was a draft homepage; on 2026-09-24 its two distinctive parts
   were merged into index.html and the draft retired (it now redirects to /):

     1. the hero's live field, full bleed behind the copy (#hero)
     2. the sticky scroll story between "Where Span sits" and the demo (#insight)

   Everything else on the homepage is styled by styles.css. This file only adds
   what the world needs, and landing.js draws it.

   Tokens come from styles.css. The only new ones are the two the field needs:
   a ground darker than --bg-deep (a luminous trace needs somewhere dark to be
   luminous against) and a gold for the latent curve, which is the one line on
   the page that is not a measurement.
   ========================================================================= */

:root{
  --void:#0B0A09;
  --truth:#E9A23B;
  --meas:#9B82FF;
  /* Type over the field needs a lighter violet than the marks do: measured on
     the composited page, the mark violet drops under 4.5:1 where the censored
     band is at its brightest. */
  --meas-text:#B29EFF;
  --mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
  --gut:clamp(20px,5vw,80px);
}

.vh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
  color:var(--on-deep)}

.w-far,.w-near{position:absolute;inset:0;width:100%;height:100%;display:block}
.w-far{z-index:0}
.w-near{z-index:2;pointer-events:none}

/* ---------------------------------------------------------------- the hero
   Production's hero was a two-column grid with a static figure beside the
   copy. The field takes the whole frame instead and the copy sits on it, so
   the first screen carries the argument visually while keeping every
   landing-page fundamental production already had: the header above it, both
   CTAs, the backed-by line. */
/* Bottom-aligned, and taller than production's: the field needs a band of its
   own above the copy. Centring the copy put the limit-of-detection rule
   straight through the third line of the headline, which reads as a
   strike-through rather than as depth. */
.hero--world{position:relative;overflow:hidden;background:var(--void);
  min-height:clamp(600px,84vh,880px);display:flex;align-items:flex-end;
  padding-block:clamp(40px,6vh,72px) clamp(56px,8vh,96px)}
.hero--world .hero-inner{position:relative;z-index:1;width:100%}
.hero--world .hero-copy{max-width:min(96%,660px)}
.hero--world .hero-lead{color:var(--on-deep-2);max-width:52ch}

/* A sibling, never a ::before on the copy: the verification pass hides the
   copy element to photograph what is behind it, and a scrim attached to that
   element disappears with it and reports a contrast nobody ever sees. */
.hero__scrim{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(100deg,rgba(11,10,9,.95) 0%,rgba(11,10,9,.88) 34%,rgba(11,10,9,.5) 62%,rgba(11,10,9,0) 88%)}

.hero-stamp{position:absolute;left:var(--gut);bottom:14px;z-index:3;margin:0;
  font-family:var(--mono);font-size:.75rem;letter-spacing:.02em;
  color:var(--on-deep-3);pointer-events:none}

/* -------------------------------------------------------------- the flight
   One sticky stage for the whole section. Deliberately NOT a sequence of
   pinned blocks: a document made of pinned blocks has seams, and the seam is
   what reads as cheap. */
.flight{position:relative;height:460vh;background:var(--void)}
/* --header-h is measured and published by landing.js: the site header is
   sticky at top:0 too, and without this it sits on the first line of every
   waypoint. */
.flight__stage{position:sticky;top:var(--header-h,0px);
  height:calc(100svh - var(--header-h,0px));overflow:hidden;background:var(--void)}
.flight__copy{position:absolute;inset:0;z-index:1}

/* opacity, not visibility: all five panels are real content in reading order,
   and visibility:hidden would drop four of them out of the accessibility tree,
   so a screen-reader user would get the argument one waypoint at a time and
   only if they scrolled. */
.fp{position:absolute;left:var(--gut);max-width:min(52vw,600px);
  opacity:0;pointer-events:none;transform:translateY(14px);
  transition:opacity .6s cubic-bezier(.4,0,.2,1),transform .6s cubic-bezier(.4,0,.2,1)}
.fp.is-on{opacity:1;pointer-events:auto;transform:none}
.fp__body{position:relative;z-index:1}
.fp__scrim{position:absolute;inset:-26% -30% -34% -26%;z-index:0;pointer-events:none;
  background:radial-gradient(closest-side at 34% 50%,rgba(11,10,9,.94) 0%,rgba(11,10,9,.84) 42%,rgba(11,10,9,.46) 68%,rgba(11,10,9,0) 100%)}

/* Every panel lives wholly above the horizon or wholly below it. The floor is
   a bright ivory rule and the one fixed thing in the section, so a block that
   straddles it both loses contrast on the row the line crosses and muddles the
   only distinction the section makes. */
.fp{top:6%}
.fp--peak{top:9%}
.fp--low{top:47%}

.fp__eyebrow{font-family:var(--mono);font-size:.6875rem;letter-spacing:.14em;
  text-transform:uppercase;color:var(--meas-text);margin:0 0 1rem}
.fp__h{font-family:'Familjen Grotesk',system-ui,sans-serif;font-weight:400;
  font-size:clamp(2rem,4.1vw,3.5rem);line-height:.98;letter-spacing:-.028em;
  color:var(--on-deep);margin:0}
.fp__h--big{font-size:clamp(2rem,3.9vw,3.4rem);text-wrap:balance}
.fp__num{font-size:clamp(4rem,10vw,8.5rem);line-height:.82;letter-spacing:-.05em;
  color:var(--meas-text);display:inline-block}
.fp__p{margin:1.2rem 0 0;font-size:clamp(1rem,1.2vw,1.2rem);line-height:1.5;
  color:var(--on-deep-2);max-width:46ch;text-wrap:pretty}
.fp__p em{font-style:normal;color:var(--on-deep);
  border-bottom:1px solid var(--meas);padding-bottom:1px}
.fp__fine{margin:1.3rem 0 0;font-size:.8125rem;line-height:1.5;
  color:var(--on-deep-3);max-width:44ch}

/* ---------------------------------------------------------------- the ruler
   Where the camera is in the patient's timeline, and a way to jump. A space
   you cannot move around in is a video. */
.ruler{position:absolute;left:var(--gut);right:var(--gut);bottom:clamp(16px,2.4vh,24px);
  height:26px;z-index:3;opacity:calc(1 - var(--mix,0))}
.ruler::before{content:'';position:absolute;left:0;right:0;top:12px;height:1px;
  background:rgba(250,247,241,.16)}
.ruler::after{content:'';position:absolute;left:0;top:12px;height:1px;
  width:calc(var(--rp,0) * 100%);background:var(--meas)}
.ruler__mark{position:absolute;top:0;transform:translateX(-50%);
  background:none;border:0;padding:6px 8px;cursor:pointer;
  font-family:var(--mono);font-size:.6875rem;letter-spacing:.1em;text-transform:uppercase;
  color:var(--on-deep-3);white-space:nowrap;transition:color .3s}
.ruler__mark i{display:block;width:1px;height:16px;margin:0 auto 5px;
  background:rgba(250,247,241,.34);transform:scaleY(.75);transform-origin:top;
  transition:background .3s,transform .3s}
.ruler__mark span{opacity:0;transition:opacity .3s}
.ruler__mark:hover,.ruler__mark:focus-visible{color:var(--on-deep)}
.ruler__mark:hover span,.ruler__mark:focus-visible span,.ruler__mark.is-on span{opacity:1}
.ruler__mark.is-on{color:var(--meas-text)}
.ruler__mark.is-on i{background:var(--meas);transform:none}
.ruler__mark:focus-visible{outline:1px solid var(--meas);outline-offset:2px;border-radius:2px}
.is-landed .ruler{pointer-events:none}

.flight__stamp{position:absolute;right:var(--gut);bottom:clamp(16px,2.4vh,24px);z-index:3;margin:0;
  opacity:calc(1 - var(--mix,0));font-family:var(--mono);font-size:.75rem;
  letter-spacing:.02em;color:var(--on-deep-3);pointer-events:none}

/* The flight lands on paper, so the demo section that follows needs no divider
   rule of its own: the colour change is the boundary. */
#try{padding-top:clamp(56px,8vh,104px)}

/* ================================================================== PHONE */
@media (max-width:820px){
  :root{--gut:20px}
  .hero--world{min-height:clamp(480px,78vh,720px)}
  .hero--world .hero-copy{max-width:none}
  .hero--world .hero-lead{max-width:none}
  .hero__scrim{background:linear-gradient(to top,rgba(11,10,9,.96) 0%,rgba(11,10,9,.9) 56%,rgba(11,10,9,.62) 80%,rgba(11,10,9,.25) 100%)}
  .flight{height:420vh}
  .fp{left:var(--gut);right:var(--gut);max-width:none}
  /* Below the horizon, not bottom-anchored. Anchoring to the bottom put the
     eyebrow straight on the limit-of-detection rule, and that rule carries
     the brightest label on the page, which measured 1.64:1 under the text. */
  .fp,.fp--low{top:48%;bottom:auto}
  .fp--peak{top:8%;bottom:auto}
  .fp__h{font-size:clamp(1.75rem,7.6vw,2.4rem)}
  .fp__h--big{font-size:clamp(1.75rem,7.2vw,2.3rem)}
  .fp__p{font-size:1rem;margin-top:.9rem}
  .fp__fine{margin-top:1rem}
  .fp__eyebrow{margin-bottom:.7rem}
  .fp__scrim{inset:-16% -8vw -26% -8vw;
    background:linear-gradient(to top,rgba(11,10,9,.96) 0%,rgba(11,10,9,.9) 52%,rgba(11,10,9,.6) 78%,rgba(11,10,9,0) 100%)}
  .fp--peak .fp__scrim{background:linear-gradient(to bottom,rgba(11,10,9,.96) 0%,rgba(11,10,9,.88) 54%,rgba(11,10,9,.5) 80%,rgba(11,10,9,0) 100%)}
  .fp__p,.fp__fine{max-width:none}
  .ruler__mark span{display:none}
  .flight__stamp{left:var(--gut);right:auto;bottom:48px}
}

/* ========================================================= REDUCED MOTION
   No camera. One composed frame (parked where the bounds are open and the
   alarm is up, because the establishing shot shows the setup and none of the
   payoff), and the five panels as an ordinary column beneath it. */
@media (prefers-reduced-motion:reduce){
  .flight{height:auto}
  .flight__stage{position:static;height:auto;overflow:visible;
    padding:0 0 clamp(48px,8vh,88px)}
  /* `inset:auto` must not follow top/left here: it resets them, and the near
     canvas then falls to its static position below the copy column, which
     looks exactly like the data layer failing to render. */
  .flight .w-far,.flight .w-near{position:absolute;inset:0 0 auto 0;width:100%;height:72svh}
  .flight .w-near{z-index:1}
  .flight__copy{position:static;inset:auto;display:grid;
    gap:clamp(40px,7vh,84px);max-width:640px;
    padding-top:calc(72svh + clamp(40px,7vh,84px));
    margin-left:var(--gut);margin-right:var(--gut)}
  .fp,.fp--peak,.fp--low{position:static;opacity:1;pointer-events:auto;transform:none;
    transition:none;inset:auto;max-width:none;top:auto;bottom:auto}
  .fp__scrim{display:none}
  .ruler{display:none}
  .flight__stamp{position:static;margin:14px var(--gut) 0}
}

/* ============================================================================
   The device layer's states.

   Keyed off [data-ld-armed], which landing.js sets at runtime, so if the
   script never runs nothing is hidden and the page renders plainly. Same
   contract production's `.js-anim` uses.
   ========================================================================= */

/* wipe, a reveal that travels, not a fade. The section arrives from its own
   left edge, which is where its text begins.

   The clip is on an INNER wrapper that landing.js inserts, never on the
   observed element itself. Chrome computes IntersectionObserver visibility
   after the target's own clip-path, so an element armed with
   `clip-path: inset(0 100% 0 0)` has zero visible area and never reports as
   intersecting: it waits forever for the observer that was supposed to
   un-clip it, and the content is simply invisible with no error. Verified by
   removing the clip, at which point the same elements fired immediately. */
[data-ld-inner]{display:block}
[data-ld="wipe"][data-ld-armed] > [data-ld-inner]{
  clip-path:inset(0 100% 0 0);opacity:.001;
  transition:clip-path .62s cubic-bezier(.33,0,.15,1),opacity .3s linear}
[data-ld="wipe"][data-ld-in] > [data-ld-inner]{clip-path:inset(0 0 0 0);opacity:1}

/* lines: the copy arrives a paragraph at a time, rising. Different from a
   block fade because the block never moves as one thing. */
[data-ld="lines"][data-ld-armed] > *{
  opacity:0;transform:translateY(22px);
  transition:opacity .6s ease,transform .6s cubic-bezier(.33,0,.15,1)}
[data-ld="lines"][data-ld-in] > *{opacity:1;transform:none}

/* edge: each numbered item opens from its own left rule outward, so the list
   reads as a sequence being laid down rather than a block appearing. */
[data-ld="edge"][data-ld-armed] > *{
  opacity:0;clip-path:inset(0 100% 0 0);
  transition:opacity .3s linear,clip-path .5s cubic-bezier(.33,0,.15,1)}
[data-ld="edge"][data-ld-in] > *{opacity:1;clip-path:inset(0 0 0 0)}

/* draw: the plates hold still; landing.js animates the strokes inside them */
[data-ld="draw"][data-ld-armed] .step{opacity:0;transition:opacity .5s ease}
[data-ld="draw"][data-ld-in] .step{opacity:1}
[data-ld="draw"][data-ld-in] .step:nth-child(2){transition-delay:.14s}
[data-ld="draw"][data-ld-in] .step:nth-child(3){transition-delay:.28s}

/* drift (the inverted band's copy moves at its own rate against the section */
[data-ld="drift"][data-ld-armed]{transform:translateY(var(--ld-drift,0));
  will-change:transform}

/* count) nothing to arm; the figures are already their own final text and
   landing.js only overwrites them while they tick. The one state worth having
   is the moment the re-fit score drops under the chance floor. */
.stat-num--fall{transition:color .3s}
.stat-num--fall.is-below{color:var(--accent)}

/* pin: the ask stops the page moving and starts it asking: the left column
   holds while the four numbered items travel past it. */
@media (min-width:900px){
  .ask-pin{position:sticky;top:calc(var(--header-h,72px) + 40px);align-self:start}
}

@media (prefers-reduced-motion:reduce){
  [data-ld-armed],[data-ld-armed] > *,[data-ld-inner]{
    clip-path:none!important;opacity:1!important;transform:none!important}
  .ask-pin{position:static}
}


/* A table ignores width:1px - it sizes to its nowrap rows, so the
   screen-reader copy of the figure was 738px wide on a 390px phone and gave
   the whole page a horizontal scroll. As a block it takes the clip. */
table.vh{display:block;max-width:1px}

/* The stamp is absolute at the hero's foot; on a phone it wrapped to three
   lines and climbed into "Backed by". Smaller and bounded on the right it is
   two lines, inside the hero's own bottom padding. Moving the copy up to make
   room instead put the limit-of-detection rule through the headline. */
@media (max-width:820px){
  .hero-stamp{right:var(--gut);bottom:10px;font-size:.6875rem;line-height:1.35}
}

/* The ruler's marks are buttons (they jump the story to a beat), and with the
   label hidden they were a 1px tick in 17px of padding - under WCAG 2.2's
   24x24 minimum. The hit area grows; the tick stays a tick. On a touch screen
   they get the full 44. */
.ruler__mark{min-width:28px;min-height:28px}
@media (pointer:coarse){.ruler__mark{min-width:44px;min-height:44px}}


/* The footer Motion toggle, mirrored: the block above only answers the OS
   preference, so a visitor who switched Motion off on the page still got the
   sticky camera. Same rules, keyed on the class site.js sets. */
html.motion-off .flight{height:auto}
html.motion-off .flight__stage{position:static;height:auto;overflow:visible;
    padding:0 0 clamp(48px,8vh,88px)}
html.motion-off .flight .w-far,html.motion-off .flight .w-near{position:absolute;inset:0 0 auto 0;width:100%;height:72svh}
html.motion-off .flight .w-near{z-index:1}
html.motion-off .flight__copy{position:static;inset:auto;display:grid;
    gap:clamp(40px,7vh,84px);max-width:640px;
    padding-top:calc(72svh + clamp(40px,7vh,84px));
    margin-left:var(--gut);margin-right:var(--gut)}
html.motion-off .fp,html.motion-off .fp--peak,html.motion-off .fp--low{position:static;opacity:1;pointer-events:auto;transform:none;
    transition:none;inset:auto;max-width:none;top:auto;bottom:auto}
html.motion-off .fp__scrim{display:none}
html.motion-off .ruler{display:none}
html.motion-off .flight__stamp{position:static;margin:14px var(--gut) 0}

/* The film (#film). A plain <video controls> with a poster, so it plays with
   no script at all; landing.js swaps the native first frame for one labelled
   button and hands back the native controls on the first press. preload=none:
   17 MB is not spent on a visitor who never presses play. */
.film{margin:0}
.film__frame{position:relative;border-radius:var(--radius);overflow:hidden;
    background:var(--bg-tint);box-shadow:0 0 0 1px var(--line)}
.film__video{display:block;width:100%;height:auto;aspect-ratio:16/9}
.film__play{position:absolute;left:50%;top:50%;translate:-50% -50%;
    display:inline-flex;align-items:center;gap:12px;min-height:48px;
    padding:14px 22px 14px 20px;border:0;border-radius:999px;cursor:pointer;
    background:var(--ink);color:var(--bg);font:inherit;font-size:1rem;font-weight:600;white-space:nowrap;
    box-shadow:0 8px 28px rgba(26,25,24,.28);transition:background .2s,scale .2s}
.film__play:hover{background:var(--accent-deep);scale:1.04}
.film__play:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
.film__play[hidden]{display:none}
.film__len{font-weight:400;opacity:.85;font-variant-numeric:tabular-nums}
.film__transcript summary{cursor:pointer;color:var(--ink-2);font-weight:600;width:max-content}
.film__transcript p{max-width:62ch;margin-top:12px;color:var(--ink-2)}
@media (prefers-reduced-motion:reduce){.film__play{transition:none}.film__play:hover{scale:none}}

/* The story's copy on the page's own left edge: it sat at the gutter (72px on
   a laptop) while the hero copy above it sits on the 1180px container. */
@media (min-width:821px){
  .fp,.ruler{left:max(var(--gut), calc((100vw - 1180px) / 2 + 28px))}
  .hero-stamp{left:max(var(--gut), calc((100vw - 1180px) / 2 + 28px))}
}

/* The last beat carries the most copy (the payoff numeral, the paragraph and
   the base-rate line) and it ran out of the stage on short windows: 31px on a
   1280x800 laptop, 72px at 760px tall, 89px on a 640px phone. It stays below
   the limit-of-detection line - raising it put the line through the "39" -
   and on short windows the numeral scales with height and the copy tightens. */
@media (max-height:880px){
  .fp[data-at="4"] .fp__num{font-size:clamp(2.75rem,8vh,6rem)}
  .fp[data-at="4"] .fp__p{margin-top:.6rem}
  .fp[data-at="4"] .fp__fine{margin-top:.7rem}
}
@media (max-width:820px) and (max-height:760px){
  .fp[data-at="4"] .fp__num{font-size:2.75rem}
  .fp[data-at="4"] .fp__h{font-size:1.6rem}
  .fp[data-at="4"] .fp__p{font-size:.9rem;line-height:1.4}
  .fp[data-at="4"] .fp__fine{font-size:.75rem}
}
@media (max-width:820px) and (max-height:700px){
  .fp[data-at="4"]{top:44%}
  .fp[data-at="4"] .fp__p{font-size:.875rem}
}
.flight__copy{z-index:3}  /* copy above the drawn marks, so a dot never sits on a letter */

/* Flat-By-Default: the film's play button is flat at rest; the shadow is the
   hover state, not its resting look. */
.film__play{box-shadow:none}
.film__play:hover{box-shadow:0 8px 28px rgba(26,25,24,.28)}
/* Measured against the ruler, not the stage's edge: the fine print sat on the
   chapter ruler at 1440x900. The compact numeral covers laptop heights too. */
@media (max-height:980px){
  .fp[data-at="4"] .fp__num{font-size:clamp(2.75rem,8vh,6rem)}
  .fp[data-at="4"] .fp__p{margin-top:.6rem}
  .fp[data-at="4"] .fp__fine{margin-top:.7rem}
}
@media (min-width:821px) and (max-height:800px){
  .fp[data-at="4"] .fp__p{font-size:1rem;line-height:1.45}
}
@media (max-height:780px){ .fp[data-at="4"] .fp__num{font-size:2.5rem} }
@media (min-width:821px) and (max-height:800px){ .fp[data-at="4"] .fp__h{font-size:clamp(1.75rem,4.2vh,2.4rem)} }

/* ---- number hierarchy --------------------------------------------------
   The one Display statement is the H1 (46px). The story's headings ran to
   56px and its "39" to 136px - one simulated 90th-percentile patient set as
   the biggest type on the site, above the cohort result the page is meant to
   lead with. Story type now sits under the H1, and the numeral at headline
   scale. Later than the short-window rules above, so it caps them too. */
.fp__h{font-size:clamp(1.75rem,3.2vw,2.5rem)}
.fp__h--big{font-size:clamp(1.75rem,3.2vw,2.6rem)}
.fp__num,.fp[data-at="4"] .fp__num{font-size:clamp(2.5rem,min(5vw,7vh),3.75rem)}

/* Motion off / reduced: the flowing story column sits on the page's own
   left edge, not the gutter. */
@media (min-width:821px){
  html.motion-off .flight__copy{margin-left:max(var(--gut), calc((100vw - 1180px) / 2 + 28px))}
}
@media (min-width:821px) and (prefers-reduced-motion:reduce){
  .flight__copy{margin-left:max(var(--gut), calc((100vw - 1180px) / 2 + 28px))}
}

/* Short phones: the copy filled the whole hero, leaving no room for the
   limit line above the headline, so it struck through it. The hero keeps a
   band of field above the copy. */
@media (max-width:820px) and (max-height:760px){ .hero--world{min-height:max(580px,82vh)} }
