/* =====================================================================
   Saphion landing page — replicated from reference capture
   Measured against a 1440 × 1080 viewport @ DPR 2
   ===================================================================== */

:root{
  /* palette (sampled from the reference) */
  --black:        #01040E;
  --mint:         #A26AF6;   /* Sign Up / Accept / Explore buttons        */
  --mint-btn-fill:#6B3FC0;   /* marketing filled-button bg — darker than
                                --mint so white button text clears 4.5:1;
                                not for icon/accent use                   */
  --mint-logo:    #A26AF6;   /* logo square                               */
  --mint-bright:  #B98BF8;   /* focus-visible outline colour              */
  --green-ink:    #A26AF6;   /* accents on white                          */
  --nav-link:     #C2C6D2;
  --hero-dim:     #4E5464;
  --hero-sub:     #8A92A0;
  --cookie-bg:    #11151F;
  --decline:      #2A3040;
  --ink:          #11151F;
  --ink-soft:     #5B6270;
  --white:        #FFFFFF;

  --wide:   1208px;   /* measured content column, all lower sections */

  --ease: cubic-bezier(.22,.61,.36,1);
  /* entrance curves — decisive deceleration, no bounce */
  --ease-out-quart: cubic-bezier(.25,1,.50,1);
  --ease-out-quint: cubic-bezier(.22,1,.36,1);
}

/* ---------- font ----------------------------------------------------
   The reference is set in GT Walsheim (the reference capture's brand face).
   Outfit is the closest freely-available match: single-storey 'a',
   geometric skeleton, sheared 't' terminal, tall x-height, and — measured
   against the capture — the tightest width agreement of any candidate
   (+3.5% mean, corrected back with tracking below).
   -------------------------------------------------------------------- */
html[data-font="poppins"]{ --sans:'Poppins'; }
html[data-font="outfit"] { --sans:'Outfit'; }

*,*::before,*::after{ box-sizing:border-box; }
html{
  -webkit-text-size-adjust:100%;
  scroll-padding-top:120px; /* matches the sticky nav's fixed height so an
                                anchor target never lands underneath it   */
}
body{
  margin:0;
  background:var(--black);
  color:var(--white);
  font-family:'GT Walsheim Pro','GT Walsheim',var(--sans,'Outfit'),
              -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  font-weight:400;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  overflow-x:hidden;
}
img{ display:block; max-width:100%; }
a{ color:inherit; text-decoration:none; }
h1,h2,h3,p,ul{ margin:0; }
ul{ padding:0; list-style:none; }

/* =====================================================================
   NAV — 120px tall, 30px gutters
   ===================================================================== */
.nav{
  position:sticky; top:0; z-index:40;
  height:120px;
  background:var(--black);
  border-bottom:1px solid rgba(255,255,255,.085);
}
/* the reference centres nav content on y≈72 inside the 120px bar,
   i.e. it sits low rather than optically centred                        */
.nav__inner{
  height:100%;
  padding:25.5px 30px 0;
  display:flex; align-items:center;
}

/* Saphion horizontal logo (purple symbol + white wordmark), shown at the
   brand-approved 160 × 38 display size from the asset library            */
.logo{
  font-size:29.5px; font-weight:700; letter-spacing:.015em;
  line-height:1; white-space:nowrap;
  display:inline-flex; align-items:center; gap:2px;
  /* Without this, a tight flex row (narrow phone: logo + Talk to Saphion +
     burger) lets the browser's default flex-shrink squash the brand mark
     itself before it touches anything else, since img{max-width:100%}
     below opts images into shrinking. Protect it; let the ≤400px tier
     handle making room instead. */
  flex-shrink:0;
}
.logo__img{
  height:38px; width:auto;
  display:block;
}

.nav__links{ margin-left:85px; }
.nav__list{ display:flex; align-items:center; gap:34px; }
.nav__item{ position:relative; }
.nav__item--mobile-only{ display:none; }

.nav__link{
  display:inline-flex; align-items:center; gap:7px;
  font-size:18px; font-weight:400; color:var(--nav-link);
  letter-spacing:-.012em;
  font-family:inherit;
  background:none; border:0; padding:0; margin:0; cursor:pointer;
  transition:color .25s var(--ease);
}
.nav__link:hover{ color:#fff; }
.nav__link:focus-visible{ outline:2px solid var(--mint); outline-offset:4px; border-radius:4px; }

.nav__chev{ width:10px; height:6px; flex:0 0 auto; transition:transform .25s var(--ease); }
.nav__trigger[aria-expanded="true"] .nav__chev{ transform:rotate(180deg); }

/* ---------- dropdown panels ---------- */
.nav__panel{
  position:absolute; top:calc(100% + 18px); left:50%;
  transform:translate(-50%,6px);
  min-width:280px;
  background:var(--cookie-bg);
  border:1px solid rgba(255,255,255,.085);
  border-radius:16px;
  padding:22px;
  box-shadow:0 26px 54px rgba(0,0,0,.5);
  opacity:0; visibility:hidden;
  transition:opacity .2s var(--ease), transform .2s var(--ease), visibility 0s linear .2s;
  z-index:50;
}
.nav__panel.is-open{
  opacity:1; visibility:visible; transform:translate(-50%,0);
  transition:opacity .2s var(--ease), transform .2s var(--ease), visibility 0s linear 0s;
}
.nav__panel--wide{ min-width:520px; left:0; transform:translateY(6px); }
.nav__panel--wide.is-open{ transform:translateY(0); }

.nav__panel-overview{
  display:block;
  padding:14px 16px;
  margin-bottom:16px;
  border-radius:10px;
  background:rgba(255,255,255,.05);
  transition:background .2s var(--ease);
}
.nav__panel-overview:hover{ background:rgba(255,255,255,.09); }
.nav__panel-overview{ font-size:16px; font-weight:500; color:#fff; }
.nav__panel-overview-sub{
  display:block; margin-top:3px;
  font-size:13px; font-weight:400; color:var(--hero-sub);
}

.nav__panel-cols{ display:flex; gap:32px; }
.nav__panel-col{ display:flex; flex-direction:column; gap:2px; min-width:220px; }
.nav__panel-col + .nav__panel-col{ border-left:1px solid rgba(255,255,255,.085); padding-left:32px; }

.nav__panel-label{
  font-size:12.5px; font-weight:500; color:#6F7280;
  margin-bottom:8px; padding:0 10px;
}
.nav__panel-link{
  display:flex; align-items:center; gap:8px;
  padding:9px 10px; border-radius:8px;
  font-size:15px; font-weight:400; color:var(--nav-link);
  letter-spacing:-.008em;
  transition:background .2s var(--ease), color .2s var(--ease);
}
.nav__panel-link:hover{ background:rgba(255,255,255,.06); color:#fff; }
.nav__panel-link:focus-visible{ outline:2px solid var(--mint); outline-offset:-2px; }

.nav__pill{
  display:inline-flex; align-items:center;
  height:20px; padding:0 8px; border-radius:5px;
  background:rgba(255,255,255,.08);
  font-size:11px; font-weight:500; color:#9DA0AC;
  white-space:nowrap;
}

.nav__right{ margin-left:auto; display:flex; align-items:center; gap:16px; }

.nav__login{
  font-size:16px; font-weight:500; color:#fff;
  transition:color .25s var(--ease);
}
.nav__login:hover{ color:var(--nav-link); }
.nav__login:focus-visible{ outline:2px solid var(--mint); outline-offset:4px; border-radius:4px; }

.nav__burger{
  position:relative; /* anchors the ::before hit-area expansion below */
  display:none;
  width:34px; height:34px;
  align-items:center; justify-content:center;
  background:none; border:0; padding:0; cursor:pointer;
  color:#fff;
}
/* The visual icon stays 34px so the tight top bar's fit (see the 400px
   tier) doesn't have to budget for it growing, but a 34px tap target
   still falls short of the 44px touch-target minimum. Expand the
   clickable area only, via an invisible pseudo-element overflowing the
   box, rather than growing the box itself. */
.nav__burger::before{
  content:''; position:absolute; inset:-5px;
}
.nav__burger svg{ width:20px; height:16px; }
.nav__burger:focus-visible{ outline:2px solid var(--mint); outline-offset:2px; border-radius:4px; }

.btn{
  display:inline-flex; align-items:center; justify-content:center;
  height:34px; border-radius:8px;
  font-size:15px; font-weight:500; letter-spacing:-.005em;
  white-space:nowrap;
  transition:transform .25s var(--ease), filter .25s var(--ease);
}
.btn:hover{ transform:translateY(-1px); filter:brightness(1.08); }
.btn:active{ transform:translateY(0); filter:brightness(.96); }
.btn:focus-visible{ outline:2px solid var(--mint-bright); outline-offset:3px; }
.btn--mint{ padding:0 15px; }

/* Shared marketing "filled primary" treatment — the single owner of the
   purple-fill pairing. Every filled-purple action lists itself here
   instead of redeclaring background+color, so a future variant can't
   independently drift back into dark-on-purple the way .cookie__btn--accept
   and .acard__btn--mint previously did.                                  */
.btn--mint,
.cookie__btn--accept,
.acard__btn--mint{
  background:var(--mint-btn-fill);
  color:#fff;
}

/* =====================================================================
   HERO
   ===================================================================== */
.hero{
  position:relative;
  height:817.5px;           /* panel edge lands on y = 937.5 */
  padding-top:85px;
  text-align:center;
}

/* radial bloom behind the headline (matches the centre-weighted falloff) */
.hero::before{
  content:''; position:absolute; inset:0; pointer-events:none;
  background:radial-gradient(46% 34% at 50% 44%, rgba(126,74,214,.13), transparent 70%);
}

.hero__h1{
  position:relative;
  font-size:112px;
  line-height:100px;
  font-weight:400;
  letter-spacing:-.022em;
}
.hero__h1 > span{ display:block; }

/* each line carries its own centre-peaked gradient — measured from the
   reference, where brightness rises toward the middle of every line     */
.hero__l1,.hero__l2{
  background-image:linear-gradient(90deg,#2A3040 0%,#4E5464 50%,#36363F 100%);
  -webkit-background-clip:text; background-clip:text;
  color:transparent;
}
/* line 1 sits ~19px left of the optical centre in the reference —
   reproduced here rather than "corrected"                              */
.hero__l1{ padding-right:41px; }
.hero__l2{ letter-spacing:-.013em; }
.hero__l2{ background-image:linear-gradient(90deg,#24242E 0%,#4F4F5C 50%,#28282F 100%); }

/* line 3 is a flex row so the two icons sit inline with the words.
   Gaps below are the measured ink-to-ink distances from the capture.   */
.hero__h1 > .hero__l3{
  display:flex;
  align-items:center; justify-content:center;
}
.hero__is  { color:#6F7280; }
.hero__plus{ color:#C2C2CE; font-size:1.25em; line-height:0; margin:0 17.5px 0 22px; }
.hero__human{
  background-image:linear-gradient(90deg,#C8C8C8 0%,#FFFFFF 45%,#EFEFEF 100%);
  -webkit-background-clip:text; background-clip:text;
  color:transparent;
  letter-spacing:.005em;
}
.hero__ai{
  background-image:linear-gradient(90deg,#B2B2C0 0%,#767684 100%);
  -webkit-background-clip:text; background-clip:text;
  color:transparent;
  letter-spacing:-.009em;
  margin-left:18.5px;
}

/* fingerprint — ridges draw in once and stay drawn (no repeat).
   The mark is the traced thumbprint.jpg reference: near-circular rather
   than the tall oval it replaced (104.16 x 112.72 viewBox, ratio .924), so
   the box is sized to hold roughly the previous optical mass rather than
   the previous height — squeezing the new proportions back into a 70x84
   frame would letterbox and shrink it. */
.ico-print{
  width:74px; height:80px;
  margin:0 18px 0 42px;
  flex:0 0 auto;
  filter:drop-shadow(0 0 10px rgba(162,106,246,.55));
  /* Optical nudge only — the mark sits fractionally low against the
     baseline of the words either side of it. */
  transform:translateY(3px);
}
/* The stagger is what carries the outside-in read, so it is spaced wide
   enough to be legible as a sequence (8 x 90ms = 630ms of lead between the
   outermost ridge and the core) rather than reading as one simultaneous
   draw. Paths are authored outermost -> innermost, so nth-child order is
   the reveal order — see the note on the artwork in marketing/design.md. */
.ico-print .pr{
  stroke-dasharray:var(--len,150);
  stroke-dashoffset:var(--len,150);
  animation:drawRidge .82s var(--ease-out-quart) forwards;
}
.ico-print .pr:nth-child(1){ animation-delay:0s }
.ico-print .pr:nth-child(2){ animation-delay:.09s }
.ico-print .pr:nth-child(3){ animation-delay:.18s }
.ico-print .pr:nth-child(4){ animation-delay:.27s }
.ico-print .pr:nth-child(5){ animation-delay:.36s }
.ico-print .pr:nth-child(6){ animation-delay:.45s }
.ico-print .pr:nth-child(7){ animation-delay:.54s }
.ico-print .pr:nth-child(8){ animation-delay:.63s }
/* 0% is deliberately omitted: the animation starts from the base rule's own
   stroke-dashoffset:var(--len,150), so a single "to" keyframe is enough for
   a one-shot draw. animation-fill-mode:forwards (set above) then holds the
   ridge at fully-drawn once this completes, rather than snapping back. */
@keyframes drawRidge{
  to{ stroke-dashoffset:0; }
}

/* sparkle star — twinkles and breathes continuously */
.ico-star{
  width:74px; height:86px;
  flex:0 0 auto;
  margin:0 9px 0 9px;
  display:inline-flex;
  filter:drop-shadow(0 0 18px rgba(162,106,246,.6));
}
.ico-star svg{ width:100%; height:100%; overflow:visible; }
/* The breathe belongs to the whole optic, not to the outline — it is on
   .star__stack so the facets, filaments, grain and core all move with the
   body as one object. Same contract as before: 0% is the resting pose the
   entrance hands over to. */
.star__stack{ transform-origin:60px 70px; animation:starBreathe 3.4s ease-in-out infinite; }
@keyframes starBreathe{
  0%,100%{ transform:scale(.92) rotate(0deg);   opacity:.92; }
  50%    { transform:scale(1.06) rotate(4deg);  opacity:1;   }
}

/* ---- the glyph's interior -------------------------------------------
   Every rule below follows the same contract as .star__body above: the
   0% frame of each loop IS that layer's resting pose, so the entrance's
   final keyframe can match it exactly and hand back invisibly, and a
   paused loop (before .is-landed, or while the hero is off screen) sits
   at the resting pose rather than somewhere mid-cycle.

   The layers are normal-blended tinted glass, not additive light — screen
   blending only ever lightens, and three screen layers plus a white core
   plus white filaments is what pushed the whole mark toward pale lavender
   instead of the brand purple. Each facet's own gradient (in index.html)
   carries saturated, darker-than-base stops, so stacking them adds shadow
   and depth rather than wash. Screen blending, combined with the clip-path
   on the filaments group and an SVG filter, was also the direct cause of a
   real load-time freeze: filter + blend-mode + clip-path on the same
   animated subtree forces the browser off the GPU and onto software
   compositing every frame. Don't reintroduce mix-blend-mode here.        */
.star__facet{
  transform-origin:60px 70px;
  animation:facetDrift 17s ease-in-out infinite;
}
/* Different scales, different phases, opposite directions — the drift is
   ±2deg over 17s, which is movement you notice only if you look for it. */
.star__facet--1{ animation-duration:17s; }
.star__facet--2{ animation-name:facetDriftB; animation-duration:21s; }
.star__facet--3{ animation-name:facetDriftC; animation-duration:26s; }
/* Opacity carries real weight now that blending is normal, not screen — the
   gradient's own stop-opacity already tapers each plane, so the layer
   opacity is the plane's overall presence, not a wash intensity. These 0%
   values are mirrored in script.js's facet stage; keep them in sync. */
@keyframes facetDrift{
  0%,100%{ transform:scale(.985) rotate(0deg); opacity:.85; }
  50%    { transform:scale(1.005) rotate(1.9deg); opacity:.95; }
}
@keyframes facetDriftB{
  0%,100%{ transform:scale(.90) rotate(0deg); opacity:.68; }
  50%    { transform:scale(.925) rotate(-2.4deg); opacity:.80; }
}
@keyframes facetDriftC{
  0%,100%{ transform:scale(.80) rotate(0deg); opacity:.52; }
  50%    { transform:scale(.775) rotate(2.8deg); opacity:.64; }
}

/* Edge refraction: a warm and a cool copy of the outline, offset under a
   pixel each way at render size. Stroke only — the silhouette itself is
   untouched. Kept faint on purpose: pushed any harder it stops reading as
   light bending and starts reading as a misregistered outline. */
.star__chroma{ fill:none; stroke-width:1.2; }
.star__chroma--a{ stroke:#D88CFF; opacity:.32; transform:translate(1.1px,-1.1px); }
.star__chroma--b{ stroke:#4B237C; opacity:.38; transform:translate(-1.1px,1.1px); }

.star__filaments{ animation:filBreathe 11s ease-in-out infinite; }
/* Two strands run on their own clock, so the bundle never pulses as one
   block — that is the difference between "alive" and "blinking".        */
.star__filaments .fil:nth-child(3){ animation:filBreathe 7.5s ease-in-out infinite; }
.star__filaments .fil:nth-child(7){ animation:filBreathe 9.5s ease-in-out infinite -3s; }
@keyframes filBreathe{ 0%,100%{ opacity:.85 } 50%{ opacity:1 } }

.star__core{ transform-origin:60px 70px; animation:coreBreathe 5.2s ease-in-out infinite; }
@keyframes coreBreathe{
  0%,100%{ transform:scale(1);    opacity:.94; }
  50%    { transform:scale(1.07); opacity:1;   }
}
.star__spec{ opacity:.9; }

/* The glint is a short dash travelling a long gap, so a highlight slides
   the length of the edge and then there is a long dark interval before the
   next one — restrained, not a repeating streak. */
.star__glint{
  fill:none; stroke:#FFFFFF; stroke-width:1.5; stroke-linecap:round;
  stroke-dasharray:14 150; opacity:.45;
  animation:glintRun 9s linear infinite;
}
.star__glint--b{ animation-duration:11s; animation-delay:-5s; }
@keyframes glintRun{
  0%  { stroke-dashoffset:150; }
  100%{ stroke-dashoffset:-14; }
}

.star__sparks circle{ animation:twinkle 1.9s ease-in-out infinite; transform-origin:center; }
.star__sparks circle:nth-child(2n){  animation-delay:.30s; }
.star__sparks circle:nth-child(3n){  animation-delay:.62s; }
.star__sparks circle:nth-child(4n){  animation-delay:.95s; }
.star__sparks circle:nth-child(5n){  animation-delay:1.28s; }
@keyframes twinkle{ 0%,100%{opacity:.15}  50%{opacity:1} }

.hero__sub{
  margin-top:40px;
  font-size:24.9px; line-height:30px; font-weight:400;
  color:var(--hero-sub);
  letter-spacing:-.01em;
}

/* Carries the CTA's top margin so the entrance has something to stage
   that isn't .cta-glow itself — see the note in index.html.            */
.hero__cta{ margin-top:31px; }

/* CTA with the soft green bloom underneath */
.cta-glow{
  position:relative;
  display:inline-flex; align-items:center; justify-content:center;
  width:237px; height:62px;
  border-radius:10px;
  border:1px solid rgba(178,138,250,.5);
  background:var(--black);
  font-size:22px; font-weight:400; color:#fff;
  letter-spacing:-.012em;
  box-shadow:0 5px 26px rgba(162,106,246,.16), inset 0 -14px 24px -18px rgba(162,106,246,.6);
  transition:box-shadow .3s var(--ease), border-color .3s var(--ease);
}
/* The hero entrance primes the button dim, then lets script.js remove
   this class ~150ms after the button has already landed — the glow
   catches a beat after the shape does, instead of both arriving at once.
   transition:none on the primed state itself means the dim value snaps
   in silently; the button's own transition above is what animates the
   *release* back to full brightness. Never added outside the entrance,
   so a page with no JS, reduced motion, or after landing is always full
   brightness by default — nothing here can leave the CTA dim.          */
.cta-glow.is-priming{
  box-shadow:0 5px 26px rgba(162,106,246,.06), inset 0 -14px 24px -18px rgba(162,106,246,.25);
  transition:none;
}
.cta-glow::after{
  content:''; position:absolute; left:50%; top:56%;
  width:360px; height:180px; transform:translateX(-50%);
  background:radial-gradient(50% 50% at 50% 50%, rgba(162,106,246,.22), transparent 70%);
  pointer-events:none; z-index:-1;
}
.cta-glow:hover{
  border-color:rgba(178,138,250,.9);
  box-shadow:0 6px 46px rgba(162,106,246,.42), inset 0 -14px 24px -18px rgba(162,106,246,.9);
}
.cta-glow:active{
  border-color:rgba(178,138,250,.7);
  box-shadow:0 3px 16px rgba(162,106,246,.22), inset 0 -10px 18px -16px rgba(162,106,246,.7);
}
.cta-glow:focus-visible{ outline:2px solid var(--mint-bright); outline-offset:3px; }

/* Two light strokes travel continuously around the button's border, half a
   turn apart, each carrying its own bloom. One unbroken circuit: linear
   timing and a full 360deg per iteration, so it never pulses, never eases
   and has no visible seam where the loop restarts.

   Built as a rotating conic gradient masked down to a 1px ring — inset:-1px
   puts the ring exactly on the button's own border, padding:1px sets its
   thickness, and the two-layer mask xor punches the middle out. Rotating the
   gradient's angle rather than the element means the light tracks the
   rounded corners properly instead of an unrelated square sweeping past.

   The travel needs @property to interpolate an angle; without it --cta-ang
   holds its initial value and the two strokes simply sit still at the
   button's sides — quiet and correct, never broken. The bloom is a
   drop-shadow on the masked result, so it hugs the moving strokes.

   No staging of its own: the whole CTA is staged via .hero__cta, so the
   strokes fade up with the button. Parked when the hero scrolls out of
   view, alongside the star's loop.                                        */
@property --cta-ang{ syntax:'<angle>'; initial-value:0deg; inherits:false; }

.cta-glow::before{
  content:''; position:absolute; inset:-1px;
  border-radius:inherit; padding:1px; pointer-events:none;
  background:conic-gradient(from var(--cta-ang),
    transparent 0deg,   transparent 24deg,
    rgba(216,194,255,.95) 46deg,
    transparent 68deg,  transparent 204deg,
    rgba(216,194,255,.95) 226deg,
    transparent 248deg, transparent 360deg);
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
          mask-composite:exclude;
  filter:drop-shadow(0 0 6px rgba(178,138,250,.8));
  animation:ctaOrbit 9s linear infinite;
}
@keyframes ctaOrbit{ to{ --cta-ang:360deg; } }

/* ---------------------------------------------------------------------
   DEPTH ASSEMBLY — the hero's one authored entrance
   ---------------------------------------------------------------------
   The staged (hidden) state is scoped to .hero-anim on <html>, which the
   inline <head> script adds only when scripting is live and reduced
   motion is not requested. Without that class — no JS, reduced motion, a
   headless render, or the failsafe timer having fired — every rule in
   this block is inert and the hero is simply the finished hero. Content
   is never gated on an animation completing.

   The moving parts themselves live in script.js: perspective, Z travel
   and tilt are per-element rather than a shared perspective on .hero,
   because making .hero a stacking context would bury .cta-glow's
   z-index:-1 bloom behind its own opaque fill.
   ------------------------------------------------------------------- */
.hero-anim [data-hero-stage]{
  opacity:0;
  will-change:transform, opacity, filter;
}
/* Released the instant an element's own arrival resolves (.is-settled,
   added by script.js off that element's own animation, not the whole
   hero) — no reason to keep something that has already visually landed
   promoted to its own compositor layer for however much longer the
   slowest remaining stage (the CTA, last at ~1120ms) takes to catch up. */
.hero-anim [data-hero-stage].is-settled{ will-change:auto; }

/* The fingerprint's one-shot draw and the star's looping breathe/twinkle
   are not replaced by the entrance — they are held while it plays and
   released via .is-landed, added by script.js, so the fingerprint starts
   drawing and the star starts breathing without both waiting on the CTA
   to finish. The two icons add it at different moments for different
   reasons: the fingerprint's draw is CSS-only and nothing else touches it,
   so its .is-landed fires partway through its own outer arrival, purely
   for pacing. The star's breathe/drift/twinkle loops share elements and
   properties with its own interior formation (formStar, in script.js), so
   its .is-landed fires only once that formation's WAAPI animations have
   actually finished and released those properties — any earlier, and the
   loop would be unpaused underneath a still-active script animation that
   outranks it, running invisibly until something finally cancels the
   script side, which is what used to read as the glyph forming and then
   freezing before its sparkle/breathe suddenly cut in.
   The same pause parks the star's loop whenever the hero is scrolled out
   of view, since a filtered 18-circle twinkle loop is not worth
   compositing for someone reading further down the page — the fingerprint
   rule alongside it is just a safety net for a draw interrupted mid-way by
   a fast scroll, since a completed one-shot has nothing left to pause.  */
.hero-anim .ico-print:not(.is-landed) .pr,
.hero-anim .ico-star:not(.is-landed) :is(.star__stack,.star__facet,.star__filaments,
  .star__filaments .fil,.star__core,.star__glint,.star__sparks circle),
.hero.is-idle .ico-print .pr,
.hero.is-idle :is(.star__stack,.star__facet,.star__filaments,
  .star__filaments .fil,.star__core,.star__glint,.star__sparks circle),
.hero.is-idle .cta-glow::before{ animation-play-state:paused; }

/* cookie consent — sits near the bottom of the hero */
.cookie{
  position:absolute; left:50%; transform:translateX(-50%);
  bottom:26px;
  width:803px; min-height:84px;
  display:flex; align-items:center;
  padding:0 23px 0 20px;
  background:var(--cookie-bg);
  border-radius:12px;
  text-align:left;
}
.cookie.is-gone{ display:none; }
.cookie__text{
  font-size:14px; line-height:18px; color:#9DA0AC; font-weight:300;
}
.cookie__text a{ color:var(--mint); }
.cookie__actions{ margin-left:auto; display:flex; gap:5px; }
.cookie__btn{
  height:34px; border:0; border-radius:8px; cursor:pointer;
  font-family:inherit; font-size:14px; font-weight:500;
  transition:filter .25s var(--ease);
}
.cookie__btn:hover{ filter:brightness(1.1); }
.cookie__btn:active{ filter:brightness(.94); }
.cookie__btn:focus-visible{ outline:2px solid var(--mint-bright); outline-offset:2px; }
.cookie__btn--decline{ background:var(--decline); color:#fff; width:79px; }
.cookie__btn--accept{ width:76px; }

/* =====================================================================
   CONTINUOUS PAGE COMPOSITION
   Everything below the hero is one uninterrupted scroll. The rounded
   inset panel is a static visual motif — it never pins, never enters a
   "full page mode", never collapses back. Panels simply alternate mood
   (light / dark) down the page, and all motion comes from content
   entering view. Nothing here changes layout on scroll, so there is no
   reflow cost and no possibility of sections colliding.
   ===================================================================== */
.page{ position:relative; padding-bottom:40px; }

.panel{
  position:relative;
  margin:40px 8px 0;
  border-radius:64px;
  overflow:hidden;
}
.panel--light{ background:var(--white); color:var(--ink); }
.panel--dark { background:var(--black); }

/* ---------- Choose Your Adventure ---------- */
.adventure{ padding:243px 0 0; text-align:center; }

.adventure__h2{
  font-size:53px; font-weight:400; letter-spacing:.008em;
  color:#11151F; line-height:1.1;
}
.adventure__h2 em{ font-style:normal; color:var(--green-ink); }

.adventure__sub{
  margin-top:33px;
  font-size:19.9px; line-height:25.6px; font-weight:400;
  color:#767684;
}

.adventure__cards{
  width:var(--wide); max-width:calc(100% - 60px);
  margin:58px auto 0;
  display:grid; grid-template-columns:1fr 1fr; gap:42px;
}

.acard{
  position:relative;
  height:419px;
  border-radius:22px;
  overflow:hidden;
  text-align:left;
  color:#fff;
  display:flex;
}
/* card internals are pinned to the card box so both cards share the same
   vertical rhythm regardless of how many lines their lede runs to        */
.acard__body{ position:relative; z-index:3; padding:50px 0 0 41px; width:74%; }
.acard__title{ font-size:37.6px; font-weight:400; letter-spacing:-.015em; line-height:1.1; }
.acard__lede{
  margin-top:8px; font-size:17.5px; line-height:26px; font-weight:300;
  color:#B5B5C2;
}
.acard__list{
  position:absolute; z-index:3; left:41px; top:158px; width:74%;
  display:grid; gap:10px;
}
.acard__list li{
  position:relative; padding-left:24px;
  font-size:16.6px; line-height:26.3px; font-weight:300; color:#E4E4EC;
}
.spark{
  position:absolute; left:0; top:7px;
  width:13px; height:13px;
  background:var(--mint);
  -webkit-mask:var(--sparkMask) center/contain no-repeat;
          mask:var(--sparkMask) center/contain no-repeat;
}
:root{
  --sparkMask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 0c1 7.6 3.4 10 11 11-7.6 1-10 3.4-11 11-1-7.6-3.4-10-11-11 7.6-1 10-3.4 11-11z'/%3E%3C/svg%3E");
}
.acard__btn{
  position:absolute; left:41px; top:316px;
  display:inline-flex; align-items:center; justify-content:center;
  width:293px; height:53px;
  border-radius:10px;
  font-size:15.8px; font-weight:500;
  transition:transform .25s var(--ease), filter .25s var(--ease);
}
.acard__btn:hover{ transform:translateY(-2px); filter:brightness(1.07); }
.acard__btn:active{ transform:translateY(0); filter:brightness(.95); }
.acard__btn:focus-visible{ outline:2px solid var(--mint-bright); outline-offset:3px; }
.acard__btn--mint{
  box-shadow:0 6px 26px rgba(162,106,246,.32);
}
.acard__btn--dark{ background:#262233; color:#fff; }

.acard__art{ position:absolute; inset:0; z-index:1; }

/* — developers card artwork — */
.acard--dev{
  background:
    radial-gradient(70% 90% at 8% 0%, rgba(96,54,168,.32), transparent 62%),
    #08060F;
}
.acard__art--ide{ left:447px; right:0; width:auto; }
.ide{
  position:absolute; left:0; top:42px; width:430px; height:400px;
  background:#100E16;
  border-radius:12px 0 0 0;
  box-shadow:-24px 0 60px rgba(0,0,0,.55);
  overflow:hidden;
  font-family:ui-monospace,'SF Mono',Menlo,Consolas,monospace;
}
.ide__bar{ height:28px; display:flex; align-items:center; gap:6px; padding-left:13px; background:#17161D; }
.ide__bar i{ width:9px; height:9px; border-radius:50%; }
.ide__bar i:nth-child(1){ background:#FF5F57 }
.ide__bar i:nth-child(2){ background:#FEBC2E }
.ide__bar i:nth-child(3){ background:#28C840 }
.ide__head{
  margin:8px; padding:10px 12px 13px;
  border-radius:9px;
  background:linear-gradient(180deg,#1A1030,#130B24);
}
.ide__crumb{ font-size:9px; color:#726E7A; }
.ide__title{ margin-top:7px; font-size:23px; color:#F2F5F3; font-family:var(--sans),sans-serif; font-weight:400; letter-spacing:-.02em; }
.ide__pill{
  margin-top:9px; display:inline-flex; align-items:center; gap:5px;
  padding:3px 8px; border-radius:5px;
  background:rgba(162,106,246,.16);
  font-size:9.5px; font-weight:700; color:#EFE3FF;
  font-family:var(--sans),sans-serif;
}
.ide__check{
  width:10px; height:10px; border-radius:2.5px;
  background:var(--mint); color:#0B0518;
  font-size:8px; line-height:10px; text-align:center;
}
.ide__code{ margin:0 8px; padding:11px 12px; border-radius:9px 9px 0 0; background:#15121F; }
.ide__q{ font-size:11px; color:#8E88A8; font-family:var(--sans),sans-serif; }
.ide pre{ margin:11px 0 0; font-size:8px; line-height:14px; color:#C9C6D6; white-space:pre; }
.ide__sel{ margin-top:7px; padding:6px 8px; border:1px solid rgba(162,106,246,.75); border-radius:6px; background:rgba(60,36,110,.28); }
.ide__sel pre{ margin:0; }
.ln{ display:inline-block; width:17px; color:#4A4560; }
.c-cm{ color:#5C5870 } .c-kw{ color:#C678DD } .c-fn{ color:#61AFEF }
.c-s{ color:#C9A6FB }  .c-p{ color:#8B8799 } .c-v{ color:#E06C9F } .c-n{ color:#D19A66 }

/* — technology-services card artwork —
   A compact route/timeline replaces the old starfield + wireframe cube +
   "GenAI" badge: the point is the services process (tell us what you need
   -> we investigate -> you approve -> we deliver), not "AI" as a category.
   One soft corner glow keeps the same sense of depth the dev card's own
   radial-gradient background carries, without stars or a floating cube. */
/* Two blooms, because this card has two things to light. The products card
   only needs one: its bloom sits over the copy and its editor panel is
   dark, so the card is rich where you read it. Here the panel is a lifted
   surface on the right, so a single bloom either hides behind the panel
   (leaving the copy on flat black — visibly duller than its neighbour) or
   sits over the copy and leaves the panel in a void. So: a softer one over
   the copy, mirroring the products card, and a stronger one behind the
   panel so the object sits *in* light rather than on top of nothing. */
.acard--biz{
  background:
    radial-gradient(64% 78% at 14% 0%, rgba(96,54,168,.26), transparent 64%),
    radial-gradient(70% 86% at 78% 4%, rgba(120,74,214,.30), transparent 66%),
    var(--black);
}
.acard__art--route{ left:auto; right:0; width:62%; }

/* The route sits on its own surface, at the same depth as the products
   card's editor panel. That pairing is the point: both cards carry a
   Saphion object, so neither reads as the thin one. It deliberately shares
   NONE of the editor's application chrome — no window dots, no monospace,
   no syntax colour — because this is a document, not a product screen, and
   technology services is not delivered inside the platform. What it does
   share with the editor is its anchoring and its crop — see below. */
.rpanel{
  /* Anchored and cropped exactly like .ide on the products card: pinned
     42px from the card's top, running off BOTH the right and the bottom
     edge, so the two cards' objects sit at the same height and are cut
     the same way. Only the top-left corner is rounded — it's the only
     corner still on the card. Matching the crop is what makes the pair
     read as one composition; centring it here (as it was) left the two
     panels visibly out of step. */
  position:absolute; right:-30px; top:42px; bottom:-24px;
  width:232px;
  padding:22px 24px 26px;
  background:linear-gradient(180deg,#1B1628 0%,#131020 46%,#0C0A13 100%);
  border:1px solid rgba(255,255,255,.09);
  border-right:0; border-bottom:0;     /* those edges are off-card */
  border-radius:14px 0 0 0;
  /* leftward only, like .ide — a downward shadow would be cropped away */
  box-shadow:-38px 0 76px rgba(0,0,0,.6),
             inset 0 1px 0 rgba(255,255,255,.075);
}
/* One soft brand wash where the route begins, rather than a glow on the
   rail itself — the light belongs to the surface, not to the diagram. */
.rpanel::before{
  content:''; position:absolute; inset:0;
  border-radius:inherit; pointer-events:none;
  background:radial-gradient(58% 44% at 16% 18%, rgba(162,106,246,.11), transparent 70%);
}
/* Positioned so it paints above ::before — an unpositioned block would sit
   in the background layer and the wash would render over the heading. */
/* Margin is tight on purpose: the rail's lead-in starts just under the
   hairline so the route reads as descending from the heading. It also
   balances the panel optically — ~37px from hairline to the first node,
   ~36px from the last node to the panel floor. */
.rpanel__head{
  position:relative;
  margin:0 0 10px; padding-bottom:14px;
  border-bottom:1px solid rgba(255,255,255,.07);
  font-size:12.5px; line-height:1.4; font-weight:500;
  color:#9C96AE;                       /* 6.5:1 on the panel surface */
}

/* ---- geometry: one model, no per-node offsets -----------------------
   Every node centre and every rail segment is derived from the four
   values below, so the rail cannot drift off the dots at any width:

   - the rail's x is (dot - line) / 2, making rail and dot concentric by
     construction rather than by a hand-tuned left value;
   - --route-centre is the distance from a step's top edge to its node
     centre, and is the ONLY vertical anchor — the dot's margin and the
     segment's top both read it, so they cannot disagree;
   - --route-centre is half a LINE BOX, not half the step, so a label
     that wraps to two lines grows the step downward and leaves its node
     sitting on the axis instead of sliding to the step's new middle;
   - a segment is exactly (this step + the gap) tall, which lands it on
     the next node's centre whatever that step's own height turns out to
     be. That is why the steps use a fixed gap rather than
     space-between: an even rhythm here is also a computable one.     */
.route{
  --route-dot:14px;
  --route-line:2px;
  --route-lh:20px;                              /* label line box       */
  --route-centre:calc(var(--route-lh) / 2);     /* step top -> node centre */
  --route-gap:52px;
  /* A segment spans one step plus one gap. The lead-in is a fixed FRACTION
     of that rather than a fixed px, so the rail is cut into proportional
     lengths at every breakpoint — which is what lets one duration ratio
     below give the same travel speed everywhere. */
  --route-seg-len:calc(var(--route-lh) + var(--route-gap));
  --route-lead-frac:.55;
  --route-lead:calc(var(--route-seg-len) * var(--route-lead-frac)
                    - var(--route-centre));     /* rail above node 1    */
  --route-glow:8px;
  --route-rail:rgba(162,106,246,.2);
  --route-label-off:#A29DB2;                    /* 7.6:1 on --black     */
  --route-label-on:#DDD9E6;
  /* fills the panel's content box — the panel owns the sizing now */
  position:relative;
  padding-top:var(--route-lead);
}

/* The lead-in: rail from the top of the route down to node 1's centre,
   so the sequence has somewhere to travel FROM and node 1 is arrived at
   rather than simply switched on. ::before is the dim rail, ::after the
   bright fill that travels along it — same box, same geometry.       */
.route::before,.route::after{
  content:''; position:absolute; top:0;
  left:calc((var(--route-dot) - var(--route-line)) / 2);
  width:var(--route-line);
  height:calc(var(--route-lead) + var(--route-centre));
}
/* Each step owns the segment leaving it for the next node. The last
   node is the destination, so it has nothing leaving it.             */
.route__step::before,.route__step::after{
  content:''; position:absolute; top:var(--route-centre);
  left:calc((var(--route-dot) - var(--route-line)) / 2);
  width:var(--route-line);
  height:calc(100% + var(--route-gap));
}
.route__step:last-child::before,.route__step:last-child::after{ content:none; }
.route::before,.route__step::before{ background:var(--route-rail); }
.route::after,.route__step::after{
  background:var(--mint);
  transform-origin:top center;
}

.route__steps{
  /* above the rails, so each segment terminates *under* its node rather
     than stopping short of one — the join is hidden, never a visible gap */
  position:relative; z-index:1;
  list-style:none; margin:0; padding:0;
  display:flex; flex-direction:column; gap:var(--route-gap);
}
.route__step{ position:relative; display:flex; align-items:flex-start; gap:12px; }
.route__dot{
  flex:0 0 auto; box-sizing:border-box;
  margin-top:calc(var(--route-centre) - var(--route-dot) / 2);
  width:var(--route-dot); height:var(--route-dot); border-radius:50%;
  border:2px solid var(--mint);
  background:var(--mint);
  box-shadow:0 0 var(--route-glow) rgba(162,106,246,.5);
}
.route__label{
  font-size:14px; line-height:var(--route-lh); font-weight:500;
  color:var(--route-label-on); letter-spacing:-.005em;
}
/* The destination carries a permanent typographic weight (never animated
   — a weight change mid-sequence would reflow the label and shift the
   card), and a slightly wider glow + pure white as its resolved state,
   which it only reaches once its own node is reached.                */
.route__step:last-child{ --route-glow:12px; --route-label-on:#fff; }
.route__step:last-child .route__label{ font-weight:600; }

/* ---- the sequence --------------------------------------------------
   Everything above is the FINISHED route: no JS, no observer support,
   or a script failure all leave a complete, readable diagram rather
   than an empty rail. The unplayed state and the animation are both
   scoped to .reveal-ready (added by script.js on boot) and to
   prefers-reduced-motion:no-preference — so reduced motion never needs
   to override anything, it simply never opts in.

   The rail is ONE continuous draw at ONE constant speed, not four
   animations in a row. Two things are load-bearing for that:

   - **every travel animation is `linear`.** An ease-out per segment
     decelerates the line to a stop at each node and accelerates it away
     again, which reads as slow-fast-slow-fast pulsing rather than a line
     being drawn. Easing belongs on the nodes (discrete state changes),
     never on the travel.
   - **there is no dwell at a node.** Segment N starts on the exact frame
     segment N-1 ends, so the line never pauses. A node lights as the
     line passes through it; it does not stop to light it.

   Speed is constant by construction, not by hand-tuned numbers: the
   lead-in is --route-lead-frac of a segment's LENGTH and runs for the
   same fraction of a segment's DURATION, so px-per-ms is identical for
   every part of the rail at every breakpoint.

     lead   400 -> 631
     node 0 631            seg 0  631 -> 1051
     node 1 1051           seg 1 1051 -> 1471
     node 2 1471           seg 2 1471 -> 1891
     node 3 1891 -> 2131 (destination, holds)

   --route-start clears the card's own reveal travel before the route
   begins, so the two entrances read as one settling motion, not two
   competing ones. Nothing loops; every animation is one-shot forwards
   and .is-in is added once and never removed, so scrolling away and
   back — or a resize — cannot replay or reset it.                    */
@media (prefers-reduced-motion:no-preference){
  .reveal-ready .route::after,
  .reveal-ready .route__step::after{ transform:scaleY(0); }
  .reveal-ready .route__dot{
    background:var(--black); border-color:rgba(162,106,246,.42);
    box-shadow:0 0 0 rgba(162,106,246,0);
  }
  .reveal-ready .route__label{ color:var(--route-label-off); }

  .route{
    --route-start:400ms;
    --route-seg-dur:420ms;
    /* same fraction of the duration as --route-lead-frac is of the length,
       so the lead-in travels at exactly the cruise speed                  */
    --route-lead-dur:calc(var(--route-seg-dur) * var(--route-lead-frac));
    --route-node-dur:240ms;
  }
  /* --i and the delay derived from it are declared on the SAME element.
     A custom property is substituted where it is declared, not where it is
     consumed: deriving --route-node-at up on .route would resolve var(--i)
     against .route (unset -> 0) and inherit that one computed value to every
     step, which is exactly how all four nodes ended up sharing a delay. */
  .route__step{
    --route-node-at:calc(var(--route-start) + var(--route-lead-dur)
                         + var(--i) * var(--route-seg-dur));
  }
  .route__step:nth-child(1){ --i:0 } .route__step:nth-child(2){ --i:1 }
  .route__step:nth-child(3){ --i:2 } .route__step:nth-child(4){ --i:3 }

  .reveal-ready .acard--biz.is-in .route::after{
    animation:routeDraw var(--route-lead-dur) linear var(--route-start) both;
  }
  /* the segment leaving node N starts exactly when node N lights, so the
     travel is unbroken end to end — one line, one speed, no dwell */
  .reveal-ready .acard--biz.is-in .route__step::after{
    animation:routeDraw var(--route-seg-dur) linear var(--route-node-at) both;
  }
  .reveal-ready .acard--biz.is-in .route__dot{
    animation:routeNodeOn var(--route-node-dur) var(--ease-out-quart)
              var(--route-node-at) both;
  }
  .reveal-ready .acard--biz.is-in .route__label{
    animation:routeLabelOn var(--route-node-dur) var(--ease-out-quart)
              var(--route-node-at) both;
  }
}
@keyframes routeDraw{ from{ transform:scaleY(0) } to{ transform:scaleY(1) } }
@keyframes routeNodeOn{
  from{ background:var(--black); border-color:rgba(162,106,246,.42);
        box-shadow:0 0 0 rgba(162,106,246,0); }
  to  { background:var(--mint);  border-color:var(--mint);
        box-shadow:0 0 var(--route-glow) rgba(162,106,246,.5); }
}
@keyframes routeLabelOn{
  from{ color:var(--route-label-off) } to{ color:var(--route-label-on) }
}

/* ---------- Tell Saphion AI ---------- */
.change{ padding:279px 0 153px; }

.change__h2{
  text-align:center;
  font-size:53px; line-height:52.2px; font-weight:400; letter-spacing:0;
}
.change__h2 em{ font-style:normal; color:var(--green-ink); }
.change__h2 .hard{ color:var(--ink); font-weight:600; }

.change__sub{
  margin:26px auto 0;
  max-width:700px;
  text-align:center;
  font-size:18.4px; line-height:27.6px; font-weight:400;
  color:#767684;
}

.change__grid{
  position:relative;
  width:var(--wide); max-width:calc(100% - 60px);
  margin:98px auto 0;
  min-height:820px;
}
.change__col{ padding-left:123px; width:520px; }

.stmt + .stmt{ margin-top:57px; }
.stmt__t{ font-weight:400; letter-spacing:-.012em; }
.stmt--on .stmt__t{ font-size:28.6px; line-height:1.2; font-weight:600; color:#11151F; }
.stmt--on .stmt__t em{ font-style:normal; color:var(--green-ink); }
.stmt__d{ margin-top:16px; font-size:17.7px; line-height:27.1px; font-weight:400; color:#7E7E8A; }
.stmt--dim1 .stmt__t{ font-size:26.8px; line-height:32px; color:#B0B0BC; }
.stmt--dim1 .stmt__d{ color:#9C97A6; }
.stmt--dim2 .stmt__t{ font-size:26.8px; line-height:32px; color:#E1E1E8; }
.stmt--dim2 .stmt__d{ color:#B7B2C0; }

.change__media{ position:absolute; left:0; top:0; width:100%; height:100%; pointer-events:none; }
.change__media > *{ pointer-events:auto; }
.change__photo{
  position:absolute; left:525.7px; top:7px;
  width:537px; height:537px; object-fit:cover;
  border-radius:24px;
}

/* ---------- opseq: a scene-by-scene product story, not a pipeline card
   ---------------------------------------------------------------------
   Corrects an earlier build of this card that was a persistent vertical
   timeline (four stages, a rail, dots — all visible at once). That read
   as a process diagram, not a story, and is removed entirely. This is
   FOUR SCENES stacked in the same frame, one visible at a time: spoken
   request -> Saphion AI operates -> generated outputs -> human
   signature. Each scene fully occupies `.opseq__viewport` and crossfades
   into the next; nothing is ever a connected multi-node chart.

   Same underlying mechanism as the "Proposed route" sequence above,
   reused rather than reinvented: viewport-triggered via the page's
   existing one-shot [data-reveal] observer (.opseq.is-in — no new JS),
   plays once, settles on the last scene, never loops or replays.

   Accessibility differs from the route pattern in one deliberate way:
   there, the base/no-motion CSS *is* the finished animated state (a
   fully-drawn rail). Here the finished animated state is only the LAST
   scene, which alone can't communicate "spoken request" or "generated
   outputs" in text — so the base/no-motion render is instead all four
   scenes stacked in plain reading order (no rail, no dots, just spacing
   and dividers), a genuinely different composition from the motion
   version, not merely its end frame. The motion layering (absolute
   stacking, crossfades) exists only inside
   prefers-reduced-motion:no-preference. ------------------------------- */
.opseq{
  position:absolute; left:576.5px; top:224.7px;
  width:594px; padding:32px 34px 28px;
  border-radius:20px;
  background:linear-gradient(180deg, rgba(32,26,55,.80) 0%, rgba(25,20,43,.95) 18%, rgba(19,16,31,.99) 40%, #12101F 100%);
  backdrop-filter:blur(6px);
  box-shadow:0 34px 70px rgba(14,10,30,.42);
  color:#EDE8F6;
}

.opseq__viewport{ position:relative; }
.opseq__scene{ position:relative; padding:15px 0; }
.opseq__scene + .opseq__scene{ border-top:1px solid rgba(255,255,255,.09); }
.opseq__label{ font-size:13.5px; font-weight:500; letter-spacing:-.005em; color:rgba(255,255,255,.82); display:flex; align-items:center; gap:8px; flex:0 0 auto; }

/* -------- scene 1: a spoken request, captured and transcribed --------
   Someone is talking and Saphion AI is transcribing it — that's the
   whole scene. Not a chatbot bubble, not a dashboard, not an equaliser.

   Replaces an earlier build whose waveform was a single hand-drawn
   polyline scrolled sideways, which settled to a flat dead rule and read
   as a decorative line rather than a voice. The form here is taken from
   the reference capture in the repository, measured frame by frame:

     · thin bars MIRRORED about a centre line, ~5.6px apart, 3.2px wide
       (the reference runs 65:35 bar-to-gap; this holds that ratio);
     · every bar pulses IN PLACE. The field does not scroll — in the
       reference, a bar correlates far better with itself one frame later
       (.73) than with either neighbour (.65/.64), so the energy is
       standing, not travelling;
     · neighbours move together. Reference correlation falls .83 → .58 →
       .18 across one, two and five bars, which is what makes bursts read
       as syllables rather than as noise; the runtime noise field is
       tuned to .88 / .67 / .17 against those numbers;
     · quiet never means empty — the reference floors bars at a dot about
       one bar-width tall, so silence still looks like an open mic;
     · energy and colour are hottest at the centre and fall away to the
       edges, with a soft bloom over the whole field.

   The reference's own ramp travels indigo → magenta → warm rose. The
   mechanic is kept exactly and the hue is remapped onto Saphion's violet
   family, so nothing borrows a colour that appears nowhere else on the
   site.

   The bar field is a settled completed capture in plain SVG; motion is
   layered over it by script.js on a canvas, so no-JS, a failed script
   and prefers-reduced-motion all still show a real voice-capture state.
   No confidence score, no transcription metadata, no capture chrome.

   The quotation commits WORD BY WORD rather than in a few large phrase
   chunks. Each word lands as its own moment in the authored speech
   envelope finishes — the same envelope that drives the bar amplitude —
   so the text reads as fast, live recognition of the speech on screen
   rather than a slow reveal or a quote already written on a card. */

/* Top-left identity: the reusable Saphion AI glyph (defined once near the
   top of index.html as #saphion-ai-glyph, mirroring the hero's .ico-star
   artwork) plus the listening label. The glyph carries no animation
   classes of its own; the static pose below matches the settled frame the
   hero's own reduced-motion override uses, so it reads as the same mark,
   just still rather than breathing. */
.opseq__glyph{ width:15px; height:17.5px; flex:0 0 auto; display:inline-flex; }
.opseq__glyph svg{ width:100%; height:100%; overflow:visible; }
.aig__stack{ transform-origin:60px 70px; transform:scale(.92); opacity:.92; }
.aig__facet1{ transform-origin:60px 70px; transform:scale(.985); opacity:.85; }
.aig__facet2{ transform-origin:60px 70px; transform:scale(.9); opacity:.68; }
.aig__core{ opacity:.94; }

/* Waveform + transcript, composed as one group independent of the label
   above. In the animated render (.reveal-ready .opseq__scene--voice, see
   below) this column fills the space left under the label and centres the
   group within it; in the plain stacked fallback it is simply block flow,
   so no min-height/flex is declared here. */
.opseq__capture{ margin-top:14px; }

/* Shorter here than in the animated render on purpose. Whenever the
   sequence is not playing — no JS, or reduced motion — all four scenes
   stack in reading order instead of sharing one frame, and the card
   grows to roughly two and a half times its animated height, already
   overspilling the panel it sits in before this scene is counted. The
   band is scaled down vertically there so the stack costs no more room
   than the treatment it replaces; it still reads as the same completed
   capture, just a quieter one. */
.opseq__voice{
  position:relative;
  height:44px;
  filter:drop-shadow(0 0 5px rgba(162,106,246,.42)) drop-shadow(0 0 14px rgba(162,106,246,.16));
}
.opseq__wavebed,
.opseq__wavelive{ position:absolute; inset:0; display:block; width:100%; height:100%; }
.opseq__wavebed path{
  fill:none; stroke:url(#opseqVoiceRamp); stroke-width:3.2; stroke-linecap:round;
}
/* Once the live canvas is in place it is the only thing drawn; the bed
   stays in the DOM as the settled state the canvas is replacing. */
.opseq__voice--live .opseq__wavebed{ visibility:hidden; }
.opseq__quote{
  margin:14px 0 0;
  font-size:15.3px; line-height:22px; font-style:italic; color:#DDD6EA;
}
.opseq__chunk{ display:inline; }

/* -------- scene 2: Saphion AI thinks, then responds --------
   Mirrors the real Saphion AI interaction pattern: customer-facing
   reasoning-SUMMARY paragraph accumulates — never the private chain-of-
   thought, never a tool name, internal prompt or raw log, the same kind
   of surfaced "working" summary already shown in the product — then
   collapses into a plain "Thought for Xs" line once resolved. The
   assistant's response follows, styled like scene 1's transcript quote
   since both are AI-produced text presented the same way. "Generating"
   (a shimmering in-progress label, not a bold status line) and the
   resulting document settle the scene before it hands off to sign-off. */
.opseq__think{ margin-top:14px; }
/* The expanded reasoning summary is a genuine paragraph, and a genuine
   accordion — the one place this card deliberately animates a layout
   property, because a real collapse (height, not just opacity) is the
   whole point of the beat: 0fr (collapsed — the resolved/no-motion
   state, and where this sits before the scene plays) -> 1fr (expanded,
   while the paragraph accumulates) -> 0fr again once the response
   begins (opseqReasonCollapse). `min-height:0` on the grid item is
   required for a grid row to shrink below its content's intrinsic
   height at all — without it the collapse simply doesn't happen. */
.opseq__reason-wrap{ display:grid; grid-template-rows:0fr; overflow:hidden; }
.opseq__reason-wrap > .opseq__reason{ min-height:0; }
.opseq__reason{
  margin:0; font-size:13px; line-height:19px; font-style:italic;
  color:rgba(234,230,242,.78);
}
/* Each chunk fades in and stays — genuinely additive text, never a
   replace and never a rotating burst. aria-hidden throughout: this
   summary is a played performance, not part of the accessible content
   (see the reduced-motion/no-JS fallback below, which never shows it). */
.opseq__rchunk{ opacity:0; }
/* Plain text, matching the live product's own collapsed-summary row —
   no bullet, no dot, no bold treatment. The reopen chevron trails the
   text (as it does live) rather than leading it. */
.opseq__thought{
  margin-top:12px;
  display:flex; align-items:center; gap:6px;
  font-size:12.5px; font-weight:500; color:#B7B0C4;
}
/* The reopen affordance the live product carries on the collapsed row
   during a turn — reused as-is from .nav__chev rather than a new shape,
   just resized and recoloured for this dark card. Static here: the
   marketing card is a played scene, not an interactive control. */
.opseq__thought-chevron{ width:8px; height:5px; flex:0 0 auto; color:#8E86A0; }
.opseq__response{
  margin:14px 0 0; font-size:15.3px; line-height:22px; font-style:italic; color:#DDD6EA;
}
/* "Generating": the same restrained, plain-text register as the
   .opseq__label heading above it, not a bold conclusion line. Solid
   white by default (the resolved/no-motion state); only inside the
   motion query does it pick up the gradient needed to shimmer. */
.opseq__generating{
  margin-top:14px;
  font-size:13.5px; font-weight:500; letter-spacing:-.005em; color:#fff;
}
.opseq__docrow{
  margin-top:12px; display:flex; align-items:center; gap:9px;
  padding:9px 12px; border-radius:9px; background:rgba(255,255,255,.045);
}
.opseq__docrow-icon{ width:13px; height:15px; flex:0 0 auto; color:#8E86A0; }
.opseq__docrow-name{ font-size:13.4px; font-weight:500; color:#EAE6F2; flex:1 1 auto; }
/* The document's own lightweight generating -> ready state: a small
   spinner while the file is being produced, crossfading once into a
   settled mint status dot — the same dot language as the rest of the
   site's "ready/live" indicators — once it's done. Never a progress bar
   or percentage; this is a glance-length affordance, not a status panel. */
.opseq__docrow-status{ position:relative; width:10px; height:10px; flex:0 0 auto; }
.opseq__docrow-spinner{
  position:absolute; inset:0; border-radius:50%;
  border:1.5px solid rgba(255,255,255,.18); border-top-color:#C9A6FB;
  opacity:0;
}
.opseq__docrow-ready{
  position:absolute; inset:0; margin:auto; width:7px; height:7px; border-radius:50%;
  background:var(--mint); box-shadow:0 0 8px rgba(162,106,246,.7); opacity:1;
}

/* -------- scene 3: human review and signature --------
   A simple card only: the document scene 2 produced, and a signature
   that draws once (authored artwork — never generated or imitated by
   Saphion AI, representing the authorised person's own act). The
   interaction carries the meaning — no pill, divider or caption restates
   it — so the card settles once the ink and its glow have resolved. */
.opseq__doc{
  margin-top:13px; padding:15px 16px 15px; border-radius:12px;
  background:rgba(255,255,255,.045);
}
.opseq__docname{ display:block; font-size:13.8px; font-weight:500; color:#EAE6F2; }
/* Fixed, compact width rather than 100%: a signature is a small mark, not
   a line stretched to fill the document's full measure — letting it span
   edge-to-edge is what made the earlier placeholder read as decoration
   rather than a person's hand. Left-aligned under the document name,
   matching where a real signature block sits. */
.opseq__signature{ display:block; width:150px; height:34px; margin-top:10px; }
.opseq__signature path{ fill:none; stroke:#fff; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round; }

/* ---- the sequence ----------------------------------------------------
   Unplayed state and whole animation live inside
   prefers-reduced-motion:no-preference, scoped to .reveal-ready and to
   .opseq.is-in (added once, never removed, by the shared [data-reveal]
   observer) — so reduced motion never needs an override, it simply
   never opts in.

   Three scenes, one crossfade transition apiece: at each transition
   point the outgoing scene fades + slides up while the incoming one
   fades + slides up into its place, in the SAME window (opseqSceneOut /
   opseqSceneIn, both --opseq-trdur, both starting at the same delay) —
   a true layered swap, not a cut. Scene 3 gets an entrance and no exit:
   it is the sequence's settled state.                                 */
@media (prefers-reduced-motion:no-preference){
  .opseq{
    /* Slowed down across the board so the sequence reads as deliberate
       rather than raced through — see the dedicated .opseq
       IntersectionObserver in script.js for the separate fix to how early
       this now starts. Scene entrances and the crossfade between scenes
       both stretched (600ms → 750ms);
       scene 1's word-by-word transcript stretched (~1.85s → ~3.0s) so
       each word stays readable as it lands instead of flashing past; and
       every scene now gets a longer hold once its content has fully
       landed, before it transitions to the next. Scenes 2-4 keep their
       own beat-to-beat rhythm (how far apart their own list items land)
       untouched — only the entrance/transition/hold structure changed.

       Scene 1's nine words, as spoken (ms from --opseq-capture-at):
         "Generate"            0 → 270
         "the"               350 → 480
         "documents"         540 → 830
         "for"               960 → 1090
         "tomorrow's"       1170 → 1470
         "working-at-height" 1570 → 2080
         "job"              2140 → 2340
         "at"               2400 → 2530
         "Riverside."       2590 → 2960, quiet by 3260

       Once the last word has committed and the waveform has gone quiet —
       both settle by ~4010ms — the scene holds for ~1.7s (--opseq-tr1 at
       5700ms) before crossfading to scene 2, long enough to actually read
       the finished sentence.

       Scene 2 was rebuilt twice to mirror the real product's reasoning-
       summary pattern (see its own variable group below); scene 3 (the
       old RAMS-output list) was removed entirely and folded into scene
       2's own ending, leaving three scenes in total. Full sequence total
       ≈16.6s, each scene holding long enough to read before it
       crossfades to the next. */
    --opseq-start:300ms;
    --opseq-open-dur:750ms;
    --opseq-trdur:750ms;
    --opseq-total:15000ms;
    --opseq-capture-at:700ms; --opseq-capture-dur:3260ms;
    --opseq-chunk1:1060ms; --opseq-chunk2:1270ms; --opseq-chunk3:1620ms;
    --opseq-chunk4:1880ms; --opseq-chunk5:2260ms; --opseq-chunk6:2870ms;
    --opseq-chunk7:3130ms; --opseq-chunk8:3320ms; --opseq-chunk9:3750ms;
    --opseq-chunk-dur:260ms;
    --opseq-tr1:5700ms;

    /* Scene 2 — retimed to feel like a quick working phase rather than a
       performance: the reasoning paragraph still expands and accumulates
       (opseqReasonCollapse: 0fr -> 1fr), but chunks land closer together
       (~300ms apart, was ~380ms) and the wrap holds only long enough to
       actually read the finished sentence before collapsing into
       "Thought for 8s". The thought -> response -> generating handoffs
       that used to carry a 500ms dead pause each now run back-to-back
       (~200-250ms) so nothing reads as a stall, while each beat still
       fully lands (opacity 1) before the next begins. "Generating" and
       the document row appear together — the row's own icon runs a brief
       spin-to-ready crossfade (opseq__docrow-spinner -> -ready) so the
       document visibly progresses to a settled state rather than just
       appearing finished. */
    --opseq-reason-at:5800ms; --opseq-reason-collapse-dur:3100ms;
    --opseq-rchunk1-at:6150ms; --opseq-rchunk2-at:6450ms; --opseq-rchunk3-at:6750ms;
    --opseq-rchunk4-at:7050ms; --opseq-rchunk5-at:7350ms; --opseq-rchunk6-at:7650ms;
    --opseq-rchunk7-at:7950ms; --opseq-rchunk-dur:300ms;
    --opseq-thought-at:8570ms; --opseq-thought-dur:350ms;
    --opseq-response-at:9170ms; --opseq-response-dur:400ms;
    --opseq-generating-at:9770ms; --opseq-generating-dur:350ms;
    --opseq-docrow-at:9970ms; --opseq-docrow-dur:400ms;
    --opseq-docready-at:10670ms; --opseq-docready-dur:280ms;
    --opseq-tr2:11600ms;

    /* Scene 3 — sign-off: a natural signature draws once, then a
       restrained glow settles along the ink before the scene holds. */
    --opseq-sign-at:12100ms;   --opseq-sign-dur:1300ms;
    --opseq-glow-at:13250ms;   --opseq-glow-dur:1400ms;
  }

  /* Sized to comfortably fit scene 2's settled state (thought + response
     + generating + document), now the tallest of the three. The shorter
     scenes simply have spare room above/below their content within the
     same fixed frame — see the flex-centred balance rule below, which is
     what turns that spare room into an even margin instead of a dead gap
     under the content. */
  .opseq__viewport{ min-height:248px; }
  /* Only the playing sequence gets the full band: there the scenes share
     one frame, so the extra height costs the card nothing. */
  .reveal-ready .opseq__voice{ height:78px; }
  .reveal-ready .opseq__scene{
    position:absolute; top:0; left:0; right:0; bottom:0; padding:0; border-top:0;
    opacity:0;
  }
  /* Both scenes 1 and 2 share the same balance mechanism: the label stays
     pinned top (flex:0 0 auto, set on .opseq__label above) while the
     content beneath it grows to fill what's left and centres itself in
     that space. Scene 2's content shrinks as the reasoning paragraph
     collapses (opseqReasonCollapse), so without this the settled state
     (thought + response + generating + document) would sit pinned to the
     top with a growing dead gap below it — the "top-heavy and
     unfinished" read this replaces. Centring the whole block, rather
     than centring each line inside it, is what keeps this from reading
     as mechanically centred: the internal rhythm (margin-top on each
     element below) is untouched, only the block's position within the
     frame moves as its own height changes. */
  .reveal-ready .opseq__scene--voice,
  .reveal-ready .opseq__scene--operate{ display:flex; flex-direction:column; }
  .reveal-ready .opseq__scene--voice .opseq__capture,
  .reveal-ready .opseq__scene--operate .opseq__think{
    flex:1 1 auto; min-height:0;
    display:flex; flex-direction:column; justify-content:center;
  }
  .reveal-ready .opseq__chunk,
  .reveal-ready .opseq__thought,
  .reveal-ready .opseq__response,
  .reveal-ready .opseq__generating,
  .reveal-ready .opseq__docrow{ opacity:0; }
  .reveal-ready .opseq__thought{ transform:translate3d(0,4px,0); }
  .reveal-ready .opseq__response,
  .reveal-ready .opseq__generating,
  .reveal-ready .opseq__docrow{ transform:translate3d(0,6px,0); }
  /* Pre-play: spinner visible (hidden behind the row's own opacity:0 until
     it rises in), ready dot not yet shown — reverse of each one's
     unconditional/resolved default above. */
  .reveal-ready .opseq__docrow-spinner{ opacity:1; }
  .reveal-ready .opseq__docrow-ready{ opacity:0; }
  .reveal-ready .opseq__signature path{ stroke-dasharray:300; stroke-dashoffset:300; }
  .reveal-ready .opseq__doc{ box-shadow:0 0 0 0 rgba(162,106,246,0); }

  /* scene 1 — spoken request, opens the sequence and exits at transition 1 */
  .reveal-ready .opseq.is-in .opseq__scene--voice{
    animation:opseqSceneIn var(--opseq-open-dur) var(--ease-out-quart) var(--opseq-start) both,
              opseqSceneOut var(--opseq-trdur) var(--ease-out-quart) var(--opseq-tr1) forwards;
  }
  /* The clock the live waveform reads. It animates a registered custom
     property that nothing is painted from: its only job is to be a real
     CSS animation on the canvas, so the canvas shares one timeline with
     the rest of the sequence and pauses, scrubs and resumes with it
     instead of running off its own wall clock. */
  .reveal-ready .opseq.is-in .opseq__wavelive{
    animation:opseqClock var(--opseq-total) linear 0ms both;
  }
  /* live transcription: each chunk commits (bright, interim) then settles
     to the quote's ink colour (confirmed) — speech in, text out, never a
     quote that was already written */
  .reveal-ready .opseq.is-in .opseq__quote .opseq__chunk:nth-child(1){
    animation:opseqChunkIn var(--opseq-chunk-dur) var(--ease-out-quart) var(--opseq-chunk1) both;
  }
  .reveal-ready .opseq.is-in .opseq__quote .opseq__chunk:nth-child(2){
    animation:opseqChunkIn var(--opseq-chunk-dur) var(--ease-out-quart) var(--opseq-chunk2) both;
  }
  .reveal-ready .opseq.is-in .opseq__quote .opseq__chunk:nth-child(3){
    animation:opseqChunkIn var(--opseq-chunk-dur) var(--ease-out-quart) var(--opseq-chunk3) both;
  }
  .reveal-ready .opseq.is-in .opseq__quote .opseq__chunk:nth-child(4){
    animation:opseqChunkIn var(--opseq-chunk-dur) var(--ease-out-quart) var(--opseq-chunk4) both;
  }
  .reveal-ready .opseq.is-in .opseq__quote .opseq__chunk:nth-child(5){
    animation:opseqChunkIn var(--opseq-chunk-dur) var(--ease-out-quart) var(--opseq-chunk5) both;
  }
  .reveal-ready .opseq.is-in .opseq__quote .opseq__chunk:nth-child(6){
    animation:opseqChunkIn var(--opseq-chunk-dur) var(--ease-out-quart) var(--opseq-chunk6) both;
  }
  .reveal-ready .opseq.is-in .opseq__quote .opseq__chunk:nth-child(7){
    animation:opseqChunkIn var(--opseq-chunk-dur) var(--ease-out-quart) var(--opseq-chunk7) both;
  }
  .reveal-ready .opseq.is-in .opseq__quote .opseq__chunk:nth-child(8){
    animation:opseqChunkIn var(--opseq-chunk-dur) var(--ease-out-quart) var(--opseq-chunk8) both;
  }
  .reveal-ready .opseq.is-in .opseq__quote .opseq__chunk:nth-child(9){
    animation:opseqChunkIn var(--opseq-chunk-dur) var(--ease-out-quart) var(--opseq-chunk9) both;
  }

  /* transition 1 — voice out, operate in (operate exits at transition 2,
     the only remaining transition — the sequence is three scenes now) */
  .reveal-ready .opseq.is-in .opseq__scene--operate{
    animation:opseqSceneIn var(--opseq-trdur) var(--ease-out-quart) var(--opseq-tr1) both,
              opseqSceneOut var(--opseq-trdur) var(--ease-out-quart) var(--opseq-tr2) forwards;
  }
  /* the reasoning paragraph: the wrap expands, holds open while all
     seven chunks land (each one fading in and staying — additive text,
     never a replace), then collapses again as "Thought for 8s" fades in
     just ahead of the collapse finishing */
  .reveal-ready .opseq.is-in .opseq__reason-wrap{
    animation:opseqReasonCollapse var(--opseq-reason-collapse-dur) linear var(--opseq-reason-at) both;
  }
  .reveal-ready .opseq.is-in .opseq__rchunk:nth-child(1){
    animation:opseqRise var(--opseq-rchunk-dur) var(--ease-out-quart) var(--opseq-rchunk1-at) both;
  }
  .reveal-ready .opseq.is-in .opseq__rchunk:nth-child(2){
    animation:opseqRise var(--opseq-rchunk-dur) var(--ease-out-quart) var(--opseq-rchunk2-at) both;
  }
  .reveal-ready .opseq.is-in .opseq__rchunk:nth-child(3){
    animation:opseqRise var(--opseq-rchunk-dur) var(--ease-out-quart) var(--opseq-rchunk3-at) both;
  }
  .reveal-ready .opseq.is-in .opseq__rchunk:nth-child(4){
    animation:opseqRise var(--opseq-rchunk-dur) var(--ease-out-quart) var(--opseq-rchunk4-at) both;
  }
  .reveal-ready .opseq.is-in .opseq__rchunk:nth-child(5){
    animation:opseqRise var(--opseq-rchunk-dur) var(--ease-out-quart) var(--opseq-rchunk5-at) both;
  }
  .reveal-ready .opseq.is-in .opseq__rchunk:nth-child(6){
    animation:opseqRise var(--opseq-rchunk-dur) var(--ease-out-quart) var(--opseq-rchunk6-at) both;
  }
  .reveal-ready .opseq.is-in .opseq__rchunk:nth-child(7){
    animation:opseqRise var(--opseq-rchunk-dur) var(--ease-out-quart) var(--opseq-rchunk7-at) both;
  }
  /* the collapse settles here: "Thought for 8s" fades in as the wrap
     above finishes closing, so the compact row is what's left once the
     paragraph has tucked itself away */
  .reveal-ready .opseq.is-in .opseq__thought{
    animation:opseqRise var(--opseq-thought-dur) var(--ease-out-quart) var(--opseq-thought-at) both;
  }
  .reveal-ready .opseq.is-in .opseq__response{
    animation:opseqRise var(--opseq-response-dur) var(--ease-out-quart) var(--opseq-response-at) both;
  }
  /* "Generating" shimmers briefly (bounded to 2 iterations — comfortably
     covers the window before the scene hands off, so it never keeps
     looping pointlessly after the card has moved on) while the document
     row rises in beneath it. */
  .reveal-ready .opseq.is-in .opseq__generating{
    background-image:linear-gradient(90deg, rgba(255,255,255,.38) 0%, #fff 50%, rgba(255,255,255,.38) 100%);
    background-size:200% 100%; -webkit-background-clip:text; background-clip:text; color:transparent;
    animation:opseqRise var(--opseq-generating-dur) var(--ease-out-quart) var(--opseq-generating-at) both,
              opseqShimmer 1.4s linear var(--opseq-generating-at) 2;
  }
  .reveal-ready .opseq.is-in .opseq__docrow{
    animation:opseqRise var(--opseq-docrow-dur) var(--ease-out-quart) var(--opseq-docrow-at) both;
  }
  /* The document's own generating -> ready beat: the spinner turns twice
     (bounded, never a pointless indefinite loop) then fades as a settled
     mint dot fades in behind it — a believable, lightweight sign the
     document has finished rather than just appearing complete. */
  .reveal-ready .opseq.is-in .opseq__docrow-spinner{
    animation:opseqSpin 480ms linear var(--opseq-docrow-at) 2,
              opseqFadeOut 220ms var(--ease-out-quart) var(--opseq-docready-at) forwards;
  }
  .reveal-ready .opseq.is-in .opseq__docrow-ready{
    animation:opseqRise var(--opseq-docready-dur) var(--ease-out-quart) var(--opseq-docready-at) both;
  }

  /* transition 2 — operate out, sign in (settles; no exit) */
  .reveal-ready .opseq.is-in .opseq__scene--sign{
    animation:opseqSceneIn var(--opseq-trdur) var(--ease-out-quart) var(--opseq-tr2) both;
  }
  .reveal-ready .opseq.is-in .opseq__signature path{
    animation:opseqSign var(--opseq-sign-dur) var(--ease-out-quart) var(--opseq-sign-at) both;
  }
  /* The restrained premium glow: a soft mint bloom rises around the
     signed document once the ink finishes, peaks briefly, then settles
     to a faint resting halo — one flare, never a permanent pulse. Layered
     box-shadow (a tight inner catch-light + a wider soft bloom) rather
     than a filter, so it stays cheap and never touches layout. */
  .reveal-ready .opseq.is-in .opseq__doc{
    animation:opseqGlow var(--opseq-glow-dur) var(--ease-out-quart) var(--opseq-glow-at) both;
  }
}
@keyframes opseqSceneIn{
  from{ opacity:0; transform:translate3d(0,16px,0); }
  to  { opacity:1; transform:none; }
}
@keyframes opseqSceneOut{
  from{ opacity:1; transform:none; }
  to  { opacity:0; transform:translate3d(0,-16px,0); }
}
@keyframes opseqRise{
  from{ opacity:0; transform:translate3d(0,8px,0); }
  to  { opacity:1; transform:none; }
}
@property --opseq-clock{ syntax:'<number>'; inherits:false; initial-value:0; }
@keyframes opseqClock{ from{ --opseq-clock:0 } to{ --opseq-clock:1 } }
@keyframes opseqChunkIn{
  0%  { opacity:0; transform:translate3d(0,5px,0); color:var(--mint-bright); }
  55% { opacity:1; transform:none; color:var(--mint-bright); }
  100%{ opacity:1; transform:none; color:#DDD6EA; }
}
@keyframes opseqSign{ from{ stroke-dashoffset:300 } to{ stroke-dashoffset:0 } }
@keyframes opseqSpin{ to{ transform:rotate(360deg); } }
@keyframes opseqFadeOut{ to{ opacity:0; } }
/* One flare, then rest — never a loop. Rises quickly to a soft double
   bloom around the signed document, eases back to a much fainter halo by
   the end and holds there (the "calm completed state" the ink settles
   into), rather than fading out to nothing or pulsing indefinitely. */
@keyframes opseqGlow{
  0%  { box-shadow:0 0 0 0 rgba(201,166,251,0), 0 0 0 0 rgba(162,106,246,0); }
  38% { box-shadow:0 0 20px 3px rgba(201,166,251,.38), 0 0 46px 10px rgba(162,106,246,.22); }
  100%{ box-shadow:0 0 12px 1px rgba(201,166,251,.14), 0 0 28px 4px rgba(162,106,246,.08); }
}
/* The "Generating" shimmer: a bright band sweeps across the gradient-
   clipped text. Purposeful, not decorative — it's the in-progress
   signal itself, the same idiom as a skeleton-loader shimmer. */
@keyframes opseqShimmer{
  from{ background-position:200% 0; }
  to  { background-position:-200% 0; }
}
/* A real accordion: the row track animates 0fr -> 1fr (quick expand,
   ease-out) -> holds open while the paragraph accumulates (linear, since
   nothing is moving) -> 0fr again (ease-out collapse). Per-keyframe
   `animation-timing-function` governs each segment independently — the
   animation's own declared timing-function (linear, where it's applied)
   is just the neutral default for whichever segment doesn't set one. */
@keyframes opseqReasonCollapse{
  0%  { grid-template-rows:0fr; animation-timing-function:var(--ease-out-quart); }
  8%  { grid-template-rows:1fr; animation-timing-function:linear; }
  88% { grid-template-rows:1fr; animation-timing-function:var(--ease-out-quart); }
  100%{ grid-template-rows:0fr; }
}

/* =====================================================================
   MUNDANE — black full-bleed section
   Editorial, asymmetrical composition, pinned to the same 1440px
   reference frame as the rest of the page's layered art (hero, .change,
   opseq): a tall photo bleeding above the section as the dominant
   right-hand anchor, with two premium note panels layered over the
   photo and the space beside it — never a flat row beneath it — and one
   hand-drawn sweep tying the handwritten "First customer" mark to both.
   ===================================================================== */
.mundane{ background:var(--black); padding:320px 0 240px; }
.mundane__inner{
  position:relative;
  width:var(--wide); max-width:calc(100% - 60px);
  margin:0 auto;
  min-height:800px;
}
.mundane__copy{ margin-left:110.5px; width:440px; }
.mundane__h2{
  font-size:50.7px; line-height:54.2px; font-weight:400; letter-spacing:-.012em;
  text-wrap:balance;
  background-image:linear-gradient(90deg,#ADADB8 0%,#FFFFFF 46%,#C0C0CB 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
}
.mundane__sub{
  margin-top:26px;
  font-size:19px; line-height:25.3px; font-weight:300; color:#B6B6C2;
  text-wrap:pretty;
}
.mundane__partner{
  margin-top:20px;
  font-size:17px; font-weight:500; color:#fff;
}
.proof__link{
  display:inline-flex; align-items:center; gap:8px;
  margin-top:28px;
  font-size:16px; font-weight:600; color:#fff;
  border-bottom:1px solid rgba(255,255,255,.35);
  padding-bottom:2px;
  transition:border-color .2s var(--ease), color .2s var(--ease);
}
.proof__link svg{ width:14px; height:10px; }
.proof__link:hover{ border-color:#fff; }
.proof__link:focus-visible{ outline:2px solid var(--mint-bright); outline-offset:4px; }

/* The dominant anchor: tall, bleeding above the section's own top edge
   (negative top), deliberately more image than the flat strip treatment
   it replaces. A soft shadow seats it as a lifted surface rather than a
   flat photo pasted on black — the same "small Saphion surface with real
   depth" language the rest of the page uses. */
.mundane__photo{
  position:absolute; left:579px; top:-96px;
  width:540px; height:820px; object-fit:cover;
  border-radius:24px;
  box-shadow:0 50px 100px rgba(0,0,0,.55);
}

/* ---- the two note panels: premium editorial callouts, not dashboard
   tiles. A label and one line of copy each — no header bar, no status
   dot, no rule stack. Frosted so either backdrop (photo or black) stays
   legible underneath, which is what lets them sit confidently layered
   over the image instead of needing a flat, opaque strip beneath it. */
.mundane__note{
  position:absolute; width:280px; padding:24px 26px;
  border-radius:16px;
  /* High enough opacity that contrast holds even over the brightest part
     of the photo underneath (verified against a white-backdrop worst
     case, not assumed from the token) — .62 measured ~3:1 for the desc
     text there, short of the 4.5:1 floor; .84 clears it regardless of
     what the panel happens to sit over. */
  background:rgba(15,12,26,.84);
  backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  border:1px solid rgba(255,255,255,.09);
  box-shadow:0 30px 60px rgba(0,0,0,.45);
}
.mundane__note-label{ font-size:15.5px; font-weight:600; letter-spacing:-.005em; color:#fff; }
.mundane__note-desc{ margin-top:10px; font-size:13.5px; line-height:19px; color:#D6D2E2; }
/* Layered directly over the photo's lower-middle third — the panel that
   gets to sit on the dominant image. */
.mundane__note--products{ left:640px; top:460px; }
/* Staggered lower and to the left, in the gap between the copy column
   and the photo rather than on it — the asymmetry (one panel on the
   image, one beside it) is deliberate, not an oversight. */
.mundane__note--services{ left:120px; top:640px; width:300px; }

/* ---- the handwritten connector: one continuous hand-drawn sweep from
   the tag near the upper (image-layered) panel down to the lower one,
   replacing the earlier two separate curved arrows. Sized and positioned
   as its own small local box so the path's coordinates never need to
   track the panels' exact runtime positions — see the responsive notes
   below for why it simply retires at narrow widths instead of trying to
   re-aim itself. */
.mundane__annot{ position:absolute; left:120px; top:410px; width:520px; height:250px; }
.mundane__tag{
  position:absolute; left:430px; top:-34px;
  font-family:'Caveat',cursive; font-size:32px; font-weight:700; color:#fff;
  transform:rotate(-8deg); white-space:nowrap;
  text-shadow:0 2px 14px rgba(0,0,0,.5);
}
.mundane__connector{ position:absolute; inset:0; width:100%; height:100%; overflow:visible; }
.mundane__connector path{
  fill:none; stroke:#A26AF6; stroke-width:2.4; stroke-linecap:round; stroke-linejoin:round;
}

/* =====================================================================
   COMPACT FINAL CTA
   ===================================================================== */
.closing{ padding:140px 0; text-align:center; }
.closing__h2{
  font-size:53px; font-weight:400; letter-spacing:.008em;
  color:#11151F; line-height:1.1;
}
.closing__sub{
  margin:24px auto 0; max-width:640px;
  font-size:19px; line-height:27px; font-weight:400; color:#767684;
}
.closing .btn--lg{ margin-top:40px; }
.btn--lg{ height:52px; padding:0 30px; border-radius:10px; font-size:17px; }

/* =====================================================================
   FOOTER
   ===================================================================== */
.site-footer{
  background:var(--black);
  padding:96px 30px 40px;
  border-top:1px solid rgba(255,255,255,.085);
}
.site-footer__inner{ width:var(--wide); max-width:calc(100% - 60px); margin:0 auto; }
.site-footer__top{
  display:grid;
  grid-template-columns:1.4fr 1fr 1fr 1fr 1fr;
  gap:48px;
}
.site-footer__brand{ padding-right:20px; }
.site-footer__logo{ display:inline-block; }
.site-footer__logo img{ height:34px; width:auto; }
.site-footer__tag{
  margin-top:20px; max-width:280px;
  font-size:15px; line-height:23px; color:var(--hero-sub);
}
.site-footer__col{ display:flex; flex-direction:column; gap:2px; }
.site-footer__label{
  font-size:12.5px; font-weight:500; color:#6F7280;
  margin-bottom:12px;
}
.site-footer__col a{
  display:flex; align-items:center; gap:8px;
  padding:7px 0;
  font-size:15px; color:var(--nav-link);
  transition:color .2s var(--ease);
}
.site-footer__col a:hover{ color:#fff; }
.site-footer__col a:focus-visible{ outline:2px solid var(--mint-bright); outline-offset:2px; border-radius:4px; }

.site-footer__bottom{
  margin-top:72px; padding-top:28px;
  border-top:1px solid rgba(255,255,255,.085);
  display:flex; align-items:center; justify-content:space-between; gap:24px;
  flex-wrap:wrap;
}
.site-footer__legal{ display:flex; gap:24px; }
.site-footer__legal a{ font-size:14px; color:var(--hero-sub); transition:color .2s var(--ease); }
.site-footer__legal a:hover{ color:#fff; }
.site-footer__legal a:focus-visible{ outline:2px solid var(--mint-bright); outline-offset:2px; border-radius:4px; }
.site-footer__copy{ font-size:13.5px; color:#6F7280; }

@media (max-width:1040px){
  .site-footer__top{ grid-template-columns:1fr 1fr 1fr; row-gap:36px; }
  .site-footer__brand{ grid-column:1 / -1; padding-right:0; margin-bottom:8px; }
}
@media (max-width:760px){
  .closing{ padding:90px 0; }
  .closing__h2{ font-size:32px; line-height:1.15; }
  .closing__sub{ font-size:16px; line-height:23px; }
  .site-footer{ padding:64px 18px 32px; }
  .site-footer__top{ grid-template-columns:1fr 1fr; gap:36px 24px; }
  .site-footer__bottom{ flex-direction:column; align-items:flex-start; gap:16px; margin-top:48px; }
}
@media (max-width:480px){
  .site-footer__top{ grid-template-columns:1fr; }
}

/* =====================================================================
   SECTION ENTRANCES
   =====================================================================
   Every block enters once as it scrolls into view, then holds. Three
   values come from the markup — data-reveal="rise,delay,scale" — so
   each block gets its own weight: headings travel a little and arrive
   first, cards travel further in a stagger, and the big media objects
   (photos, the profile card, the kanban boards) travel furthest with a
   slight zoom, which reads as depth against the copy beside them.

   IMPORTANT: the hidden state is scoped to .reveal-ready, which JS adds
   on boot. Without JS — or if the script fails — nothing is ever
   hidden and the page ships complete and fully readable. The entrance
   enhances a working default rather than gating content on it.
   ===================================================================== */
.reveal-ready [data-reveal]{
  opacity:0;
  transform:translate3d(0,var(--rise,40px),0) scale(var(--rz,1));
  will-change:transform,opacity;
}
.reveal-ready [data-reveal].is-in{
  opacity:1;
  transform:none;
  will-change:auto;
  /* opacity resolves a little ahead of the travel so copy becomes readable
     early, while the movement keeps easing out underneath it              */
  transition:opacity .60s var(--ease-out-quart) var(--delay,0s),
             transform .92s var(--ease-out-quint) var(--delay,0s);
}

/* Panels drift up gently as they come into view. Pure CSS scroll-driven
   motion: it runs on the compositor, tracks scroll in both directions,
   and settles at its end state instead of ever reversing back out. In
   browsers without support the @supports block simply never applies. */
@supports (animation-timeline: view()){
  @media (prefers-reduced-motion:no-preference){
    @keyframes panelSettle{
      from{ transform:translate3d(0,34px,0); }
      to  { transform:none; }
    }
    .panel{
      animation:panelSettle linear both;
      animation-timeline:view();
      animation-range:entry 0% entry 24%;
    }
  }
}

@media (prefers-reduced-motion:reduce){
  .reveal-ready [data-reveal]{ opacity:1; transform:none; }
  /* Removing the animations is not enough on its own. Every layer's resting
     appearance is authored in its loop's 0% frame, so `animation:none`
     alone drops each one back to its *base* value — full opacity, unscaled
     — and the glyph renders as the brightest, flattest version of itself
     with all 18 spark dots lit at once. The reduced-motion state has to be
     the composition someone designed, so each layer is pinned to the pose
     its loop rests at. */
  .star__stack,.star__facet,.star__filaments,.star__filaments .fil,.star__core,
  .star__sparks circle,.ico-print .pr{ animation:none; stroke-dashoffset:0; }
  .star__stack{ transform:scale(.92); opacity:.92; }
  .star__facet--1{ transform:scale(.985); opacity:.85; }
  .star__facet--2{ transform:scale(.90);  opacity:.68; }
  .star__facet--3{ transform:scale(.80);  opacity:.52; }
  .star__filaments{ opacity:.9; }
  .star__core{ opacity:.94; }
  /* Mid-twinkle rather than all-lit: the dots read as motes suspended in
     the mark instead of a field of hard white points. */
  .star__sparks circle{ opacity:.5; }
  /* The glint is motion by definition. Rather than freeze a dash somewhere
     arbitrary along the edge, drop the dash entirely: what remains is a
     continuous hairline specular down two edges, which is a deliberate
     static treatment rather than a stopped animation. */
  .star__glint{ animation:none; stroke-dasharray:none; opacity:.32; }
  /* The route needs nothing here: its unplayed state and its whole
     sequence live inside a prefers-reduced-motion:no-preference block,
     so reduced motion simply never opts in and the base rules — the
     completed, fully-drawn route — are what render. */
  .nav__panel,.nav__panel.is-open,.nav__links,.nav.is-mobile-open .nav__links,.nav__chev{ transition:none; }
  /* Belt and braces: the inline <head> script already declines to add
     .hero-anim here, so this only matters if the preference is switched
     on mid-session. Drift is disabled in JS, not by zeroing the custom
     properties, so the icons keep their original resting transforms.  */
  .hero-anim [data-hero-stage]{ opacity:1; will-change:auto; }
  /* The travelling edge strokes are motion and nothing else — with the
     travel removed they would be two static hairlines pinned to the button's
     corners, which is not a design decision anyone made. Remove them. */
  .cta-glow::before{ display:none; }
}

/* =====================================================================
   RESPONSIVE
   =====================================================================
   The desktop layout above is pinned to the reference's 1440px frame
   (fixed section heights, absolutely-placed art). Below that the same
   design relaxes into a flow layout.
   ===================================================================== */
@media (max-width:1400px){
  /* The desktop base rule's max-width:calc(100% - 60px) is a leftover of
     the --wide/60px reference-frame sizing; below this breakpoint centring
     is owned by margin-inline instead. Left in place, the stale max-width
     stays harmless while margin-inline is 40px (60 - 80 < 0, never binds)
     but starts clamping the box narrower than the margins account for once
     margin-inline drops to 16px at 760px (60 - 32 > 0) — and because the
     margins are explicit, not auto, the shortfall lands entirely on the
     right instead of re-centring. Clearing it here is the single fix for
     all shared containers at every width this rule covers — .container
     (the sub-page content measure) joins the same rule for the same
     reason, rather than declaring its own copy that could drift. */
  .adventure__cards,.change__grid,.mundane__inner,.container{ width:auto; max-width:none; margin-inline:40px; }
  .change__col{ padding-left:0; width:auto; max-width:520px; }
  .change__photo{ left:44%; }
  .opseq{ left:52%; width:min(594px,48vw); }
  /* .mundane__inner drops its fixed 1208px width above (shared rule) and
     goes fluid, so the composition re-anchors to the container's own
     edges (right for the photo, small fixed left offsets for the rest)
     instead of trying to hold the desktop's absolute pixel values —
     same technique the section used before this rebuild. */
  .mundane__copy{ margin-left:0; }
  .mundane__photo{ left:auto; right:0; }
  .mundane__note--products{ left:auto; right:170px; top:420px; }
  .mundane__note--services{ left:40px; top:600px; }
  .mundane__annot{ left:auto; right:390px; top:350px; width:460px; }
}
@media (max-width:1280px){
  .nav__burger{ display:inline-flex; }

  .nav__links{
    position:absolute; left:0; right:0; top:100%;
    max-height:calc(100vh - 120px); overflow-y:auto;
    padding:8px 30px 24px;
    background:var(--black);
    border-bottom:1px solid rgba(255,255,255,.085);
    box-shadow:0 26px 54px rgba(0,0,0,.5);
    opacity:0; visibility:hidden; transform:translateY(-8px);
    transition:opacity .2s var(--ease), transform .2s var(--ease), visibility 0s linear .2s;
    margin-left:0;
  }
  .nav.is-mobile-open .nav__links{
    opacity:1; visibility:visible; transform:translateY(0);
    transition:opacity .2s var(--ease), transform .2s var(--ease), visibility 0s linear 0s;
  }

  .nav__list{ flex-direction:column; align-items:stretch; gap:0; }
  .nav__item{ border-bottom:1px solid rgba(255,255,255,.06); }
  .nav__item--mobile-only{ border-bottom:0; }
  .nav__link{ width:100%; padding:16px 4px; justify-content:space-between; font-size:19px; }
  .nav__trigger{ justify-content:space-between; }

  .nav__panel{
    position:static; transform:none; min-width:0;
    background:none; border:0; box-shadow:none; overflow:hidden;
    max-height:0; opacity:0; visibility:hidden; padding:0 4px;
    transition:max-height .25s var(--ease), opacity .2s var(--ease),
               padding .25s var(--ease), visibility 0s linear .25s;
  }
  .nav__panel.is-open{
    max-height:600px; opacity:1; visibility:visible; padding:0 4px 16px;
    transition:max-height .25s var(--ease), opacity .2s var(--ease),
               padding .25s var(--ease), visibility 0s linear 0s;
  }
  .nav__panel--wide{ left:auto; }
  .nav__panel-cols{ flex-direction:column; gap:4px; }
  .nav__panel-col + .nav__panel-col{ border-left:0; padding-left:0; margin-top:12px; padding-top:12px; border-top:1px solid rgba(255,255,255,.06); }
  .nav__panel-overview{ margin-bottom:8px; }

  .nav__right{ gap:12px; }

  .hero{ height:auto; padding-bottom:150px; }
  .hero__h1{ font-size:86px; line-height:82px; }
  .hero__l1{ padding-right:31px; }
  .ico-print{ width:60px; height:65px; margin:0 14px 0 32px; }
  .ico-star{ width:57px; height:66px; margin:0 7px; }
  .hero__sub{ font-size:20px; line-height:26px; }
  .cookie{ position:relative; left:auto; bottom:auto; transform:none;
           width:min(803px, calc(100% - 60px)); margin:70px auto 0; }
  .adventure{ padding-top:150px; }
  .change{ padding:190px 0 120px; }
  .change__grid{ min-height:0; }
  .change__media{ position:relative; height:660px; margin-top:80px; }
  .change__photo{ left:0; width:min(440px,42vw); height:min(440px,42vw); }
  .opseq{ left:14%; top:34%; width:min(520px,58vw); }
  .mundane{ padding:160px 0; }
  .mundane__h2{ font-size:42px; line-height:1.14; }
}
/* The "Choose Your Path" cards are a fixed 419px box with the list and the
   CTA absolutely pinned to it, so their vertical rhythm only holds while the
   card is at its full 583px width — i.e. once the 1208px content column is
   no longer capped by the viewport. Below that the two columns squeeze the
   body to ~345px, the lede runs long and the list grows straight through the
   pinned CTA. So the card drops to its own flow layout at the same 1280px
   threshold the nav collapses at, rather than at 1040px where the rest of the
   lower page reflows. Only the card rules move up; everything else below
   stays on 1040. */
@media (max-width:1280px){
  .adventure__cards{ grid-template-columns:1fr; gap:28px; }
  .acard{ height:auto; }
  .acard__body{ width:100%; padding:38px 38px 0; }
  /* The list and the CTA are children of .acard__body, which already carries
     the 38px gutter — their own 38px inset was doubling it, pushing both 38px
     right of the title and running the 293px CTA past the card edge (clipped
     by .acard's overflow:hidden at phone widths). Inset comes from the body
     only; the CTA keeps its 293px but yields on a narrow card. */
  .acard__list{ position:static; width:auto; margin:34px 0 0; padding:0; }
  .acard__btn{ position:static; width:100%; max-width:293px; margin:34px 0 38px; }
  /* The route is absolutely positioned against the right-hand 62% of a
     fixed-height card at desktop; once .acard__body goes full-width above,
     that same overlay would sit directly behind the CTA and bullet list
     instead of beside them. So it leaves the overlay and joins the flow.
     .route itself stays position:relative throughout, so its own
     absolutely-positioned rail never re-anchors against a different
     ancestor when the wrapper around it changes from absolute to static. */
  .acard__art--route{ position:static; width:auto; }
  .acard--biz{ align-items:center; }
  /* Geometry tokens only — never animation properties. Re-declaring an
     animation inside a media query restarts it, which is exactly the
     "replays on responsive reflow" failure this has to avoid. The rail
     is scaleY, so a changed gap needs no timing change either — and the
     lead-in is derived from the gap, so the whole rail stays cut into the
     same proportions and keeps one constant travel speed here too. */
  .route{ --route-gap:24px; }
  /* The card's own line breaks were measured against a full-width desktop
     column. From here down the copy column is narrower and variable, so
     they stop helping and start breaking lines in the wrong place ("the
     priority or" stranded on its own line). Natural wrapping from here.
     Each has a space before it in the markup, so removing the break can't
     fuse the words either side. */
  .acard__lede br,.acard__list br{ display:none; }
  /* Nothing to run off here — the panel rejoins the flow, closes the two
     cropped edges, rounds all four corners and drops the bleed. */
  .rpanel{
    position:relative; right:auto; top:auto; bottom:auto;
    border-right:1px solid rgba(255,255,255,.09);
    border-bottom:1px solid rgba(255,255,255,.09);
    border-radius:14px;
    box-shadow:0 20px 44px rgba(0,0,0,.4),
               inset 0 1px 0 rgba(255,255,255,.075);
  }
}
/* Between the two existing thresholds the card is a single full-width
   column but still 700-1220px wide — far too wide to stack the panel under
   the CTA, which stranded the whole right half of the card empty. The card
   keeps its row axis here and the panel simply becomes a flow sibling
   beside the copy, which is what puts that width back to work. */
@media (max-width:1280px) and (min-width:761px){
  .acard--biz .acard__body{ width:auto; flex:1 1 0; }
  .acard__art--route{ flex:0 0 auto; display:flex; align-items:center;
                      padding:38px 38px 38px 8px; }
  .rpanel{ width:320px; }
}
@media (max-width:760px){
  /* Genuinely narrow: one column, panel below the CTA. Capped rather than
     full-width so a rail and four short labels never stretch into a band. */
  .acard--biz{ flex-direction:column; align-items:stretch; }
  .acard__art--route{ display:block; padding:0 38px 38px; }
  .rpanel{ width:100%; max-width:340px; }
}
@media (max-width:1040px){
  .acard__art--ide{ left:60%; }
  .change__media{ height:auto; }
  .change__photo,.opseq{ position:relative; left:auto; top:auto; }
  .change__photo{ width:100%; height:340px; }
  .opseq{ width:min(100%,560px); margin:-90px 0 0 auto; }
  /* Genuinely narrow: the layered composition needs real estate the
     column no longer has, so it rejoins ordinary flow — photo, then
     each note in turn — rather than trying to hold an overlap that
     would just collide. The connector's fork points are meaningless
     once its two targets are no longer side by side, so it retires; the
     plain "Lawtech is Saphion's first customer and design partner."
     sentence in the copy column still carries the connection in words. */
  .mundane__inner{ min-height:0; }
  .mundane__copy{ width:100%; }
  .mundane__photo{
    position:relative; left:auto; right:auto; top:auto;
    width:100%; height:min(85vw,520px); margin-top:56px;
  }
  .mundane__note{ position:relative; left:auto; right:auto; top:auto; width:100%; max-width:380px; margin-top:22px; }
  .mundane__annot{ display:none; }
}
@media (max-width:760px){
  .nav__inner{ padding:18px 18px 0; }
  .nav__links{ padding-left:18px; padding-right:18px; }
  .hero{ padding-top:56px; padding-bottom:90px; }
  .hero__h1{ font-size:44px; line-height:44px; letter-spacing:-.02em; }
  .hero__l1{ padding-right:0; }
  .hero__h1 > .hero__l3{ flex-wrap:wrap; }
  .ico-print{ width:32px; height:35px; margin:0 8px 0 12px; }
  .ico-star{ width:32px; height:37px; margin:0 5px; }
  /* Detail tier. At 32px the filaments are well under a device pixel, so
     they are not drawn at all rather than drawn and lost. The facet stack
     drops to two planes and the refraction offset thickens, so what
     survives is the part that still reads at this size: depth at the
     edges and a bright core. */
  .star__filaments,.star__facet--3{ display:none; }
  .star__chroma{ stroke-width:2.6; }
  .star__glint{ stroke-width:2; }
  .hero__plus{ margin:0 8px; }
  .hero__ai{ margin-left:8px; }
  .hero__sub{ font-size:16px; line-height:24px; padding:0 22px; margin-top:24px; }
  .hero__sub br,.adventure__sub br{ display:none; }
  .cta-glow{ width:210px; height:54px; font-size:18px; }
  .cookie{ width:calc(100% - 32px); flex-direction:column; align-items:flex-start;
           gap:14px; padding:18px; margin-top:52px; }
  .cookie__text br{ display:none; }
  .cookie__actions{ margin-left:0; }
  .panel{ border-radius:28px; margin:24px 8px 0; }
  .adventure{ padding-top:70px; }
  .adventure__cards,.change__grid,.mundane__inner,.container{ margin-inline:16px; }
  .adventure__h2,.change__h2{ font-size:32px; line-height:1.15; }
  .adventure__sub{ font-size:16px; line-height:23px; }
  .change__sub{ font-size:16px; line-height:23px; }
  .acard__title{ font-size:28px; }
  .acard__art--ide{ left:auto; right:-46%; width:96%; opacity:.34; }
  .change{ padding:90px 0 70px; }
  .stmt--on .stmt__t{ font-size:24px; }
  .stmt--dim1 .stmt__t,.stmt--dim2 .stmt__t{ font-size:24px; line-height:27px; }
  .stmt + .stmt{ margin-top:40px; }
  .opseq{ padding:24px 20px 20px; }
  .opseq__quote{ font-size:14.5px; line-height:21px; }
  .mundane{ padding:90px 0 80px; }
  .mundane__h2{ font-size:32px; line-height:1.16; }
  .mundane__sub{ font-size:16px; line-height:23px; }
  .mundane__note{ max-width:none; padding:20px 22px; }
}
@media (max-width:620px){
  .acard__art--ide{ display:none; }
  /* Log in moves from the top bar into the hamburger panel at the same
     threshold it disappears from the top bar — one visible copy at any
     given width, never both (it previously showed in both places between
     620-1280px, since these two rules used to live at different
     breakpoints). */
  .nav__login{ display:none; }
  .nav__item--mobile-only{ display:block; }
}
@media (max-width:400px){
  /* Below ~394px, logo(160) + "Talk to Saphion"(130) + burger(34) at their
     natural sizes no longer fit the 18px-gutter nav bar — measured as low
     as a 0px gap between the logo and the button at 360px wide (iPhone SE
     and many Android phones), and .logo's new flex-shrink:0 above means
     that stops being silently absorbed by squashing the brand mark and
     starts being a real, visible collision instead. Recover room without
     touching nav__inner's padding, which .nav__links intentionally
     mirrors for the mobile panel's own edge alignment. Values chosen to
     leave a genuine (not just non-negative) gap down to 320px.         */
  .logo__img{ height:28px; }
  .btn--mint{ padding:0 8px; }
  .nav__right{ gap:8px; }
}

/* =====================================================================
   PLACEHOLDER PAGES
   Minimal, on-brand "coming soon" pattern for destinations that don't
   have real content yet. Reuses the same header chrome and tokens as
   the landing page — no parallel visual system.
   ===================================================================== */
.placeholder{ min-height:60vh; padding:110px 30px 160px; }
.placeholder__inner{ max-width:640px; margin:0 auto; }

.placeholder__back{
  display:inline-flex; align-items:center; gap:8px;
  font-size:15px; color:var(--hero-sub);
  transition:color .25s var(--ease);
  margin-bottom:52px;
}
.placeholder__back svg{ width:11px; height:11px; transition:transform .25s var(--ease); }
.placeholder__back:hover{ color:#fff; }
.placeholder__back:hover svg{ transform:translateX(-2px); }

.placeholder__h1{
  font-size:53px; font-weight:400; letter-spacing:-.015em; line-height:1.1;
  color:#fff;
}
.placeholder__badge{
  display:inline-flex; align-items:center; height:28px; padding:0 12px;
  margin-top:22px; border-radius:7px;
  background:rgba(255,255,255,.08);
  font-size:13px; font-weight:500; color:#9DA0AC;
}
.placeholder__lede{
  margin-top:24px; font-size:19px; line-height:29px; color:var(--hero-sub);
}

.placeholder__section{ margin-top:56px; }
.placeholder__section-label{
  font-size:12.5px; font-weight:500; color:#6F7280; margin-bottom:14px;
}
.placeholder__list{ display:grid; gap:12px; }
.placeholder__list li{
  position:relative; padding-left:22px;
  font-size:16px; line-height:26px; color:#C2C6D2;
}
.placeholder__list li::before{
  content:''; position:absolute; left:0; top:10px;
  width:6px; height:6px; border-radius:50%;
  background:var(--mint);
}
.placeholder__list a{ color:inherit; transition:color .2s var(--ease); }
.placeholder__list a:hover{ color:#fff; }

@media (max-width:760px){
  .placeholder{ padding:70px 22px 100px; }
  .placeholder__h1{ font-size:36px; }
  .placeholder__lede{ font-size:17px; line-height:26px; }
}

/* =====================================================================
   SUB-PAGE PRIMITIVES
   Shared building blocks for every real (non-"coming soon") marketing
   page below the landing page: a dark intro band, a content measure, an
   ordered-process list, a human-control callout, a responsive card grid
   and a reusable proof strip. Each page composes these differently
   (order, count, which ones appear at all) rather than repeating one
   fixed template — see marketing/design.md, "Sub-page system".
   ===================================================================== */

/* .container is the same shared responsive measure as .adventure__cards /
   .change__grid / .mundane__inner / .site-footer__inner (--wide, 60px cap
   at desktop, cleared max-width + margin-inline below 1400px) — added to
   those same shared breakpoint rules below rather than declared alone, so
   it can never drift out of sync with the fix already proven there. */
.container{ width:var(--wide); max-width:calc(100% - 60px); margin:0 auto; }

.pagehero{ background:var(--black); padding:200px 0 100px; }
.pagehero__back{
  display:inline-flex; align-items:center; gap:8px;
  font-size:15px; color:var(--hero-sub);
  transition:color .25s var(--ease);
  margin-bottom:40px;
}
.pagehero__back svg{ width:11px; height:11px; transition:transform .25s var(--ease); }
.pagehero__back:hover{ color:#fff; }
.pagehero__back:hover svg{ transform:translateX(-2px); }
.pagehero__back:focus-visible{ outline:2px solid var(--mint-bright); outline-offset:3px; border-radius:4px; }
.pagehero__pill{
  display:inline-flex; align-items:center; height:28px; padding:0 12px;
  margin-bottom:22px; border-radius:7px;
  background:rgba(255,255,255,.08);
  font-size:13px; font-weight:500; color:#9DA0AC;
}
.pagehero__pill--roadmap{ background:rgba(162,106,246,.14); color:#DCC3FF; }
.pagehero__h1{
  max-width:820px;
  font-size:clamp(36px,4.6vw,60px); font-weight:400; letter-spacing:-.015em; line-height:1.08;
  color:#fff;
}
.pagehero__lede{
  max-width:640px; margin-top:22px;
  font-size:19px; line-height:29px; color:var(--hero-sub);
}
.pagehero__lede em{ font-style:italic; color:#fff; }
.pagehero__cta{ margin-top:36px; }
@media (max-width:760px){
  .pagehero{ padding:130px 0 64px; }
  .pagehero__lede{ font-size:17px; line-height:26px; }
}

/* ---------- Ordered process (genuine sequences only — see the
   numbered-markers ban: earn the number, don't scaffold with it) ---------- */
.steps{ display:grid; gap:36px; counter-reset:step; }
.sectionhead + .steps,.sectionhead + .cardgrid{ margin-top:48px; }
.steps--cols{ grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:32px 40px; }
.step{ position:relative; padding-left:56px; }
.step__num{
  position:absolute; left:0; top:0;
  display:flex; align-items:center; justify-content:center;
  width:38px; height:38px; border-radius:10px;
  background:rgba(162,106,246,.14);
  font-size:15px; font-weight:600; color:var(--green-ink);
  font-variant-numeric:tabular-nums;
}
.step__title{ font-size:19px; font-weight:500; letter-spacing:-.01em; color:#11151F; }
.step__body{ margin-top:8px; font-size:16px; line-height:25px; color:#5B6270; }
.panel--dark .step__title{ color:#fff; }
.panel--dark .step__body{ color:var(--hero-sub); }

/* ---------- Human-control / boundary callout — full border + tint +
   leading icon, never a colour side-stripe (absolute ban) ---------- */
.callout{
  display:flex; gap:16px;
  padding:26px 28px; border-radius:16px;
  border:1px solid rgba(162,106,246,.28);
  background:rgba(162,106,246,.06);
}
.panel--dark .callout{ border-color:rgba(162,106,246,.32); background:rgba(162,106,246,.08); }
.callout__icon{
  flex:0 0 auto; width:28px; height:28px; margin-top:2px;
  -webkit-mask:var(--sparkMask) center/contain no-repeat;
          mask:var(--sparkMask) center/contain no-repeat;
  background:var(--mint);
}
.callout__title{ font-size:16px; font-weight:500; color:#11151F; }
.panel--dark .callout__title{ color:#fff; }
.callout__body{ margin-top:6px; font-size:15.5px; line-height:24px; color:#5B6270; }
.panel--dark .callout__body{ color:var(--hero-sub); }

/* ---------- Credible empty state — a genuine "nothing here yet",
   never a fake list of placeholder items ---------- */
.emptystate{ text-align:center; padding:40px 0; }
.emptystate__icon{
  display:inline-block; width:36px; height:36px; margin-bottom:20px;
  -webkit-mask:var(--sparkMask) center/contain no-repeat;
          mask:var(--sparkMask) center/contain no-repeat;
  background:var(--mint);
}
.emptystate__title{ font-size:clamp(24px,3vw,32px); font-weight:400; letter-spacing:-.01em; color:#11151F; }
.emptystate__body{ margin:14px auto 0; max-width:480px; font-size:17px; line-height:26px; color:#5B6270; }
.emptystate__cta{ margin-top:32px; }

/* ---------- Responsive card grid — column count and card content vary
   per page; this owns only spacing/sizing, never a fixed 3-up template ---------- */
.cardgrid{ display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:24px; }
.cardgrid__card{
  padding:32px; border-radius:18px;
  background:#0C0A13; border:1px solid rgba(255,255,255,.085);
}
.cardgrid__card-title{ font-size:19px; font-weight:500; color:#fff; letter-spacing:-.01em; }
.cardgrid__card-body{ margin-top:10px; font-size:15.5px; line-height:24px; color:var(--hero-sub); }
.cardgrid__card-link{
  display:inline-flex; align-items:center; gap:6px; margin-top:18px;
  font-size:14.5px; font-weight:500; color:var(--green-ink);
  transition:gap .2s var(--ease);
}
.cardgrid__card-link svg{ width:11px; height:11px; }
.cardgrid__card:hover .cardgrid__card-link{ gap:10px; }
.cardgrid__card-link:focus-visible{ outline:2px solid var(--mint-bright); outline-offset:3px; border-radius:4px; }
.cardgrid__card--light{ background:var(--white); border-color:rgba(17,21,31,.08); }
.cardgrid__card--light .cardgrid__card-title{ color:#11151F; }
.cardgrid__card--light .cardgrid__card-body{ color:#5B6270; }

/* ---------- Section heading, reused at sub-page scale (smaller than the
   landing page's .adventure__h2 — sub-pages are denser, more numerous) ---------- */
.sectionhead{ max-width:640px; }
.sectionhead__h2{
  font-size:clamp(28px,3.2vw,38px); font-weight:400; letter-spacing:-.01em; line-height:1.15;
  color:#11151F;
}
.panel--dark .sectionhead__h2{ color:#fff; }
.sectionhead__sub{ margin-top:14px; font-size:17px; line-height:26px; color:#5B6270; }
.inline-link{ color:inherit; text-decoration:underline; text-underline-offset:2px; transition:color .2s var(--ease); }
.inline-link:hover{ color:var(--mint-bright); }
.inline-link:focus-visible{ outline:2px solid var(--mint-bright); outline-offset:2px; border-radius:2px; }
.panel--dark .sectionhead__sub{ color:var(--hero-sub); }

.pagesection{ padding:96px 0; }
.pagesection + .pagesection{ padding-top:0; }
@media (max-width:760px){
  .pagesection{ padding:64px 0; }
  .sectionhead__sub{ font-size:16px; line-height:24px; }
}
