/* ===========================================================
   Phanim — phanim.com
   Rules: brand/BRAND.md. Tokens below are the only source of colour.
   =========================================================== */

/* ---------- faces, self-hosted ----------
   Google's stylesheet cost 849ms of render-blocking time on a throttled mobile
   connection plus two extra connections, and the swap that followed rewrapped the
   headline: measured CLS. These are same-origin and preloaded, so they arrive with
   the page. Google's own unicode-range split is preserved, so an English page only
   ever downloads the latin file of each family. Files: assets/fonts/. */
@font-face{
  font-family:'Archivo';font-style:normal;font-weight:400 800;font-stretch:75% 112%;font-display:swap;
  src:url('/assets/fonts/archivo-400-800-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'Archivo';font-style:normal;font-weight:400 800;font-stretch:75% 112%;font-display:swap;
  src:url('/assets/fonts/archivo-400-800-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:'Michroma';font-style:normal;font-weight:400;font-display:swap;
  src:url('/assets/fonts/michroma-400-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'Michroma';font-style:normal;font-weight:400;font-display:swap;
  src:url('/assets/fonts/michroma-400-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;
  src:url('/assets/fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;
  src:url('/assets/fonts/ibm-plex-mono-500-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;
  src:url('/assets/fonts/ibm-plex-mono-400-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;
  src:url('/assets/fonts/ibm-plex-mono-500-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root{
  /* surfaces */
  --bg:#080810;
  --surface:#0e0e1b;
  --border:rgba(233,230,255,.09);
  --border-2:rgba(233,230,255,.16);

  /* text */
  --ink:#F1EEE7;
  --muted:#8E88AE;
  --dim:#7D77A6;            /* AA 4.81:1 — replaces #5C5780 (2.97:1, failed) */

  /* signal */
  --signal:#7C5CFF;         /* brand stroke / accent */
  --signal-cta:#7452F5;     /* fill under white text — AA 4.92:1 */
  --signal-bright:#A78BFA;  /* violet text */
  --iris:#5FE6FF;           /* one glint per view */
  --gold:#C9A84C;           /* money only */
  --green:#22C55E;
  --red:#EF4444;

  /* Two faces plus the wordmark. Archivo is a grotesque with flat terminals and
     tight apertures — it sets like shop signage, which is the point. It is loaded
     as a variable font: one file instead of five statics, and the wdth axis is
     what lets the display sizes be optically tightened instead of just scaled. */
  --font-word:'Michroma',sans-serif;
  --font-display:'Archivo',system-ui,sans-serif;
  --font-body:'Archivo',system-ui,sans-serif;
  --font-mono:'IBM Plex Mono',ui-monospace,monospace;

  /* type scale — one place, every page.
     Display is set narrow (wdth 93) because Archivo 800 at 5rem is wide enough to
     lose its rag; condensing it buys a tighter measure without dropping the size. */
  --fs-display:clamp(2.75rem,6.3vw,5.3rem);
  --fs-h1-sm:clamp(2.3rem,5vw,3.6rem);
  --fs-h2:clamp(1.9rem,3.7vw,3rem);
  --fs-h3:1.24rem;
  --fs-lead:clamp(1.05rem,1.35vw,1.22rem);
  --fs-body:1rem;
  --fs-label:11.5px;
  --wdth-display:93;
  --wdth-h2:96;

  --ease:cubic-bezier(.16,1,.3,1);
  --ease-spring:cubic-bezier(.34,1.56,.64,1);
  --dur-fast:.2s;
  --dur-int:.3s;
  --dur-reveal:.75s;

  /* depth. Elevation is shadow plus a rim of light on the top edge — the way a
     machined face catches the shop lights. Never a coloured glow behind a pill. */
  --rim:inset 0 1px 0 rgba(233,230,255,.08);
  --elev-1:0 2px 10px -6px rgba(0,0,0,.7);
  --elev-2:0 18px 44px -26px rgba(0,0,0,.92);
  --elev-3:0 40px 90px -50px rgba(0,0,0,.95);
  --elev-lift:0 34px 90px -34px rgba(124,92,255,.42);

  /* written by app.js, read by CSS. Unitless; every consumer supplies the unit.
     px/py: pointer, -1..1 from centre. hy: hero scroll progress 0..1.
     spin: scroll-velocity fed into the dial. All default to a resting page. */
  --px:0;
  --py:0;
  --hy:0;
  --spin:0;

  --maxw:1320px;   /* includes the .wrap gutter; content area is 1240 */
}

*{box-sizing:border-box;margin:0;padding:0}
/* Any `display` rule outranks the hidden attribute's UA style, so state it once
   here rather than remembering per component. */
[hidden]{display:none!important}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{
  background:var(--bg);color:var(--ink);font-family:var(--font-body);
  font-size:16px;line-height:1.6;-webkit-font-smoothing:antialiased;
  overflow-x:hidden;position:relative;
}
img,svg{max-width:100%}

/* ambient light + precision grid.
   Both bleed past the viewport and drift against the pointer, which is what puts
   the page in front of an atmosphere rather than on top of a flat colour. The
   light moves further than the grid: nearer things move more. */
body::before{
  content:'';position:fixed;inset:-10%;z-index:-2;pointer-events:none;
  background:
    radial-gradient(1200px 700px at 78% -6%, rgba(124,92,255,.16) 0%, transparent 62%),
    radial-gradient(900px 600px at 8% 14%, rgba(95,230,255,.045) 0%, transparent 55%),
    radial-gradient(1000px 900px at 50% 112%, rgba(124,92,255,.08) 0%, transparent 60%),
    var(--bg);
  transform:translate3d(calc(var(--px) * -26px), calc(var(--py) * -18px), 0);
  will-change:transform;
}
body::after{
  content:'';position:fixed;inset:-10%;z-index:-2;pointer-events:none;opacity:.5;
  background-image:
    linear-gradient(rgba(233,230,255,.026) 1px, transparent 1px),
    linear-gradient(90deg, rgba(233,230,255,.026) 1px, transparent 1px);
  background-size:88px 88px;
  -webkit-mask-image:radial-gradient(circle at 62% 16%, #000 0%, transparent 72%);
          mask-image:radial-gradient(circle at 62% 16%, #000 0%, transparent 72%);
  transform:translate3d(calc(var(--px) * -10px), calc(var(--py) * -7px), 0);
  will-change:transform;
}
.grain{
  position:fixed;inset:0;z-index:60;pointer-events:none;opacity:.035;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.wrap{width:100%;max-width:var(--maxw);margin-inline:auto;padding-inline:clamp(20px,4vw,40px)}
/* 6.75rem, not 8: two unbordered sections back to back put 256px of featureless
   black between them at 1440, which read as the page losing its place rather than
   as breathing room. 216px plus the rule below holds the air and keeps the thread. */
section{padding:clamp(4rem,7vw,6.75rem) 0}

/* Connective tissue. The bordered bands (.proof, .facts, .band) already announce
   themselves; these are the sections that floated. The rule draws itself as the
   section arrives, in the same language as every other rule on the page, and it
   is scroll-driven so there is no observer and no class to keep in sync. */
/* The nav is fixed and ~68px tall, so an in-page jump has to stop short of the
   target or the section's own label lands underneath it. */
main[id],section[id],header[id]{scroll-margin-top:96px}

.sect-rule{position:relative}
.sect-rule::before{
  content:'';position:absolute;left:0;right:0;top:0;height:1px;transform-origin:left;
  background:linear-gradient(90deg,var(--border-2),var(--border) 45%,transparent);
}
@supports (animation-timeline:view()){
  .js .sect-rule::before{
    animation:ruleDraw linear both;animation-timeline:view();animation-range:entry 10% entry 85%;
  }
  @keyframes ruleDraw{from{transform:scaleX(0)}to{transform:scaleX(1)}}
}

/* ---------- accessibility ---------- */
:focus-visible{outline:2px solid var(--iris);outline-offset:3px;border-radius:4px}
.skip{
  position:absolute;left:12px;top:-60px;z-index:100;padding:12px 18px;border-radius:8px;
  background:var(--signal-cta);color:#fff;text-decoration:none;font-weight:500;
  transition:top .2s var(--ease);
}
.skip:focus{top:12px}

/* ---------- nav ---------- */
nav{
  position:fixed;top:0;left:0;right:0;z-index:50;padding:15px 0;
  background-color:rgba(9,9,19,0);
  background-image:linear-gradient(to bottom, rgba(8,8,16,.92), rgba(8,8,16,0));
  border-bottom:1px solid transparent;
  backdrop-filter:blur(10px);
}
/* The bar earns a surface once you leave the hero, because the persistent CTA has
   to stay legible over a screenshot. Scroll-driven, so there is no scroll handler
   and no class to keep in sync; browsers without the timeline keep the gradient. */
@supports (animation-timeline:scroll()){
  nav{animation:navSettle linear both;animation-timeline:scroll(root block);animation-range:0 140px}
  @keyframes navSettle{
    to{background-color:rgba(9,9,19,.84);border-bottom-color:var(--border)}
  }
}
/* nav bar is full-bleed; its contents ride the same .wrap gutter as the page,
   so the logo lines up with the headline below it */
nav .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px}
.brand{display:flex;align-items:center;gap:12px;text-decoration:none;color:var(--ink)}
.brand .mark{width:28px;height:28px;flex:none}
/* The mark is an eye, so it looks at you. Travel is capped at 6 user units of a
   100-unit viewBox, which keeps the pupil inside its iris at every mark size. */
.pupil{
  transform-box:fill-box;transform-origin:center;
  transform:translate(calc(var(--px) * 6px), calc(var(--py) * 6px));
}
/* ...and the outer ring reads how far down the page you are. pathLength:100 makes
   the dash arithmetic percentages. Unsupported, it stays at offset 100: invisible. */
.prog{stroke-dasharray:100;stroke-dashoffset:100}
@supports (animation-timeline:scroll()){
  .prog{animation:progRing linear both;animation-timeline:scroll(root block)}
  @keyframes progRing{to{stroke-dashoffset:0}}
}
.brand .word{font-family:var(--font-word);font-size:17px;letter-spacing:.24em;padding-left:.24em}
.navlinks{display:flex;align-items:center;gap:clamp(16px,2.4vw,30px)}
.navlinks a:not(.btn){
  /* 12px of padding, not 10: at 10 the hit box measured 43px on a phone, one
     short of the 44 this audience needs with gloves on */
  color:var(--muted);text-decoration:none;font-size:14.5px;font-weight:500;
  padding:12px 2px;transition:color .25s var(--ease);
}
.navlinks a:not(.btn):hover,
.navlinks a[aria-current="page"]{color:var(--ink)}
.navlinks a[aria-current="page"]{box-shadow:inset 0 -2px 0 var(--signal)}
/* Pricing stays reachable on a phone. This audience lives on one, and the only
   other route to it was scrolling 6,000px to the footer. "What it sees" and
   "About" go: the page itself is the tour, and the footer carries About. */
@media(max-width:760px){.navlinks .hide-sm{display:none}}
/* Keeping Pricing costs ~50px, which pushed "Start free" into two lines. Buy it
   back from the wordmark rather than dropping it: the mark still reads, and the
   headline underneath says PHANIM at 44px anyway. */
@media(max-width:430px){
  .brand .word{font-size:14px;letter-spacing:.16em}
  nav .btn{padding:9px 15px;white-space:nowrap}
  .navlinks{gap:14px}
}

/* ---------- buttons ----------
   Squared-off and flat. The old pill had a 30px violet blur under it, which is
   the one thing every generated landing page shares. */
.btn{
  position:relative;display:inline-flex;align-items:center;justify-content:center;gap:10px;
  font-family:var(--font-body);font-weight:600;font-size:15px;letter-spacing:-.005em;
  min-height:46px;padding:12px 24px;border-radius:8px;cursor:pointer;text-decoration:none;
  border:1px solid transparent;overflow:hidden;isolation:isolate;
  /* magnetism: app.js writes --mx/--my in the -1..1 range while the pointer is
     over the button, and drops them on leave. The spring curve is what makes the
     release feel sprung rather than dragged. */
  transform:translate3d(calc(var(--mx,0) * 5px), calc(var(--my,0) * 4px), 0);
  transition:background var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease),
             color var(--dur-fast) var(--ease),box-shadow var(--dur-int) var(--ease),
             transform .45s var(--ease-spring);
}
.btn:active{transform:translate3d(calc(var(--mx,0) * 5px), calc(var(--my,0) * 4px + 1px), 0) scale(.985)}
.btn .arw{display:inline-block;transition:transform var(--dur-int) var(--ease)}
.btn:hover .arw{transform:translateX(4px)}
/* the key light on the face of the button, following the pointer across it */
.btn::before{
  content:'';position:absolute;inset:0;z-index:-1;opacity:0;pointer-events:none;
  background:radial-gradient(120px 80px at calc(50% + var(--mx,0) * 50%) calc(50% + var(--my,0) * 60%),
             rgba(255,255,255,.20), transparent 70%);
  transition:opacity var(--dur-int) var(--ease);
}
.btn:hover::before{opacity:1}
.btn-primary{
  background:var(--signal-cta);color:#fff;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.22), 0 1px 0 rgba(0,0,0,.4), 0 10px 26px -14px rgba(116,82,245,.9);
}
.btn-primary:hover{background:#8468ff;box-shadow:inset 0 1px 0 rgba(255,255,255,.28), 0 1px 0 rgba(0,0,0,.4), 0 18px 40px -16px rgba(116,82,245,.95)}
.btn-ghost{background:transparent;color:var(--ink);border-color:var(--border-2)}
.btn-ghost:hover{background:rgba(233,230,255,.05);border-color:rgba(233,230,255,.3)}
.btn-ghost::before{background:radial-gradient(120px 80px at calc(50% + var(--mx,0) * 50%) calc(50% + var(--my,0) * 60%), rgba(167,139,250,.16), transparent 70%)}
nav .btn{min-height:44px;padding:11px 18px;font-size:14.5px;border-radius:7px}

/* ---------- type ---------- */
/* Label with a rule, not a pill. */
.eyebrow{
  display:inline-flex;align-items:center;gap:11px;font-family:var(--font-mono);
  font-size:11.5px;font-weight:500;letter-spacing:.22em;text-transform:uppercase;
  color:var(--signal-bright);
}
.eyebrow::after{
  content:'';width:clamp(28px,5vw,64px);height:1px;flex:none;
  background:linear-gradient(90deg,var(--border-2),transparent);
  transform-origin:left;transition:transform .9s var(--ease) .2s;
}
.eyebrow .dot{width:6px;height:6px;border-radius:50%;background:var(--iris);box-shadow:0 0 10px var(--iris);animation:blink 3.4s var(--ease) infinite}
@keyframes blink{0%,92%,100%{opacity:1}96%{opacity:.25}}

h1{
  font-family:var(--font-display);font-weight:800;font-stretch:calc(var(--wdth-display) * 1%);
  font-size:var(--fs-display);line-height:.97;letter-spacing:-.038em;
  margin:24px 0 0;text-wrap:pretty;
}
h1 .glow{color:var(--signal-bright)}
.kicker{
  /* inline-flex, not inline: a reveal transform on an inline box is ignored, and
     the rule below needs to be a real box so it reserves its width up front */
  display:inline-flex;align-items:center;gap:14px;
  font-family:var(--font-word);font-size:var(--fs-label);letter-spacing:.22em;
  text-transform:uppercase;color:var(--ink);opacity:.8;
}
/* the rule draws itself as the section lands, which is what turns a stack of
   fades into a sequence: label, rule, headline, body */
.kicker::after{
  content:'';width:clamp(28px,5vw,72px);height:1px;flex:none;
  background:linear-gradient(90deg,var(--border-2),transparent);
  transform-origin:left;transition:transform .9s var(--ease) .2s;
}
/* scoped to .js, so with scripts off the rule is simply drawn */
.js .kicker::after,.js .eyebrow::after{transform:scaleX(0)}
.js .reveal.in .kicker::after,.js .kicker.reveal.in::after,
.js .reveal.in .eyebrow::after,.js .eyebrow.reveal.in::after{transform:none}
.lead{
  font-family:var(--font-display);font-weight:700;font-stretch:calc(var(--wdth-h2) * 1%);
  font-size:var(--fs-h2);line-height:1.04;letter-spacing:-.03em;
  margin-top:18px;max-width:26ch;text-wrap:balance;
}
.lead .m{color:var(--muted)}
.sub{color:var(--muted);font-size:var(--fs-lead);max-width:38ch;margin-top:22px;line-height:1.55}
.sub b{color:var(--ink);font-weight:600}
.prose p{color:var(--muted);font-size:1.05rem;max-width:62ch;margin-top:18px}
.prose p b,.prose p strong{color:var(--ink);font-weight:600}
.prose a{color:var(--signal-bright);text-decoration-thickness:1px;text-underline-offset:4px}
/* A link that is its own paragraph is a control, not prose, so it gets a control's
   hit box. WCAG 2.2 asks 24px; this audience taps with gloves, so it gets 44.
   :only-child keeps genuine inline links inside sentences untouched. */
.prose p > a:only-child{display:inline-flex;align-items:center;gap:8px;min-height:44px}
.prose a .arw{display:inline-block;transition:transform var(--dur-int) var(--ease)}
.prose a:hover .arw{transform:translateX(4px)}
.prose h3{font-family:var(--font-display);font-weight:700;font-size:1.3rem;margin-top:44px;letter-spacing:-.02em}

/* align-items:center, not the default stretch: stretched, the ghost grew to match
   the primary's new height and the size step disappeared. */
.cta-row{display:flex;align-items:center;flex-wrap:wrap;gap:12px;margin-top:34px}
/* The primary is a size up from the secondary, deliberately. Matched, the ghost
   button was the wider of the two and pulled more attention than the button this
   page exists to get clicked. */
.cta-row .btn-primary{min-height:54px;padding:15px 30px;font-size:16px}
/* Risk reversal is conversion copy, not a footnote: --muted (5.97:1), not --dim. */
.trust{display:flex;align-items:center;flex-wrap:wrap;gap:10px 14px;margin-top:26px;color:var(--muted);font-size:13px;font-family:var(--font-mono);letter-spacing:.02em}
/* separators only earn their place on one line; wrapped, they leave dangling dots */
@media(max-width:560px){.trust [aria-hidden]{display:none}.trust{gap:6px 18px}}

/* ---------- kinetic type ----------
   Words carry their own span so the headline assembles rather than fading in:
   each one hinges up out of depth on the expo curve, 55ms behind the last. Only
   transform, opacity and filter move, so nothing reflows and CLS stays at zero.

   An animation, not an observed class. Above the fold, a JS-gated entrance puts a
   deferred script on the critical path for the page's own headline: Lighthouse
   measured 86% of a 3.3s LCP as render delay waiting on exactly that. A CSS
   animation starts at first render. Scoped to .js — without scripts the headline
   is just a headline, and under reduced motion the block below stops it dead. */
.js .kin .w{
  display:inline-block;transform-origin:50% 100%;
  animation:wordIn .78s var(--ease) both;
  animation-delay:calc(var(--i,0) * 55ms);
}
@keyframes wordIn{
  from{opacity:0;transform:perspective(900px) translate3d(0,.4em,-90px) rotateX(-54deg);filter:blur(9px)}
  to{opacity:1;transform:none;filter:none}
}

/* ---------- hero ---------- */
header.hero{position:relative;min-height:100svh;display:flex;align-items:center;padding-top:118px;padding-bottom:72px;overflow:hidden}
/* no width here — .wrap owns it, and a later width:100% would kill its centring */
.hero-grid{display:grid;grid-template-columns:1.02fr .98fr;gap:clamp(32px,5vw,72px);align-items:center;position:relative;z-index:1}
@media(max-width:1000px){.hero-grid{grid-template-columns:1fr;gap:44px}}

/* ---------- the dial ----------
   The brand mark, built as an instrument rather than a decal. Three ring planes
   sit at different depths inside one perspective: they tilt by different amounts,
   drift by different amounts against the pointer, and the far plane is blurred.
   That combination — not a drop shadow — is what makes it read as an object with
   a back and a front. The whole thing is aria-hidden; it says nothing. */
/* Height is 100svh, not `inset:0`.
   Anchored to the hero's own box, every layer inside sat at a percentage of a
   height that changes the moment the webfont swaps in and the headline rewraps —
   Lighthouse measured 0.213 CLS from the beam alone on a cold mobile load. A
   viewport-sized box does not move, so neither do its children. */
.stage{
  position:absolute;left:0;right:0;top:0;height:100svh;z-index:0;
  pointer-events:none;overflow:hidden;
  perspective:1000px;perspective-origin:72% 46%;
}
.dial{
  position:absolute;top:50%;right:-10%;width:min(920px,78vw);aspect-ratio:1;
  transform:translateY(-50%);transform-style:preserve-3d;
  /* scroll sinks the dial, so the hero visibly has a floor. `translate` composes
     with the transform above rather than replacing it. */
  translate:0 calc(var(--hy) * 9vh);
  -webkit-mask-image:radial-gradient(circle at 50% 50%, #000 52%, transparent 84%);
          mask-image:radial-gradient(circle at 50% 50%, #000 52%, transparent 84%);
}
.lyr{
  position:absolute;inset:0;transform-style:preserve-3d;
  transform:
    translate3d(calc(var(--px) * var(--pd,10px)), calc(var(--py) * var(--pd,10px)), 0)
    rotateX(calc(var(--tx,8deg) + var(--py) * var(--tr,-5deg)))
    rotateZ(calc(var(--px) * var(--rz,2deg)));
  will-change:transform;
}
.lyr svg{position:absolute;inset:0;width:100%;height:100%}
/* far: the outer eye-ring. Small parallax, most tilt, out of focus. */
.lyr-far{--pd:9px;--tx:13deg;--tr:-6deg;--rz:1.4deg;opacity:.46;filter:blur(1.7px)}
/* mid: the working rings. */
.lyr-mid{--pd:19px;--tx:9deg;--tr:-5deg;--rz:2.2deg;opacity:.82}
/* near: ticks, glint, sweep. Moves most, sits in focus, in front of the halo. */
.lyr-near{--pd:34px;--tx:5deg;--tr:-4deg;--rz:3.2deg;opacity:1}

/* the light source. A soft violet key that trails the pointer across the dial —
   the reason the rings have a lit side and a dark side. */
.beam{
  position:absolute;top:50%;right:-10%;width:min(920px,78vw);aspect-ratio:1;
  transform:translate3d(calc(var(--px) * 90px), calc(-50% + var(--py) * 60px), 0);
  background:radial-gradient(closest-side, rgba(124,92,255,.20), rgba(124,92,255,.05) 46%, transparent 72%);
  pointer-events:none;will-change:transform;
}
@media(max-width:1000px){
  .dial,.beam{right:-34%}
  .lyr-far{opacity:.3}.lyr-mid{opacity:.55}.lyr-near{opacity:.7}
}
/* On a phone the dial is atmosphere behind the headline, not an object beside it.
   At mid-height and phone scale it sat in the gap between the CTAs and the panel,
   which read as a smudge. Bigger, higher, quieter: it becomes weather. */
@media(max-width:640px){
  .dial,.beam{top:26%;right:-40%;width:min(580px,132vw)}
  .lyr-far{opacity:.22}.lyr-mid{opacity:.4}.lyr-near{opacity:.5}
}

.spin-slow{transform-box:fill-box;transform-origin:center;animation:spin 90s linear infinite}
.spin-rev{transform-box:fill-box;transform-origin:center;animation:spin 120s linear infinite reverse}
.spin-fast{transform-box:fill-box;transform-origin:center;animation:spin 46s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* Scroll velocity spins the rings. Applied to a wrapper group so it composes with
   the keyframe rotation on the group inside rather than fighting it. */
.vel{transform-box:fill-box;transform-origin:center;rotate:calc(var(--spin) * 1deg)}
.vel-rev{transform-box:fill-box;transform-origin:center;rotate:calc(var(--spin) * -.6deg)}
.pulse{transform-box:fill-box;transform-origin:center;animation:pulse 5.4s var(--ease) infinite}
@keyframes pulse{0%,100%{opacity:.55;transform:scale(1)}50%{opacity:1;transform:scale(1.04)}}
/* the sweep: one arm reading the floor, the way a scope does */
.sweep{transform-box:fill-box;transform-origin:center;animation:spin 14s linear infinite}

/* ---------- the scope: a rendering of the shop view ----------
   The mount owns the reveal, the card owns the tilt. Two elements because a
   transform can only be one thing, and the entrance and the pointer response are
   two different things happening at two different times. */
.scope-mount{perspective:1400px;transform-style:preserve-3d}
.scope{
  position:relative;border:1px solid var(--border-2);border-radius:14px;overflow:hidden;
  background:linear-gradient(180deg,rgba(19,19,34,.92),rgba(10,10,20,.92));
  backdrop-filter:blur(14px);
  box-shadow:var(--elev-3), var(--rim);
  transform:
    rotateY(calc(var(--px) * -5deg))
    rotateX(calc(var(--py) * 3.6deg))
    translate3d(calc(var(--px) * -8px), calc(var(--py) * -5px), 0);
  transition:box-shadow var(--dur-int) var(--ease);
  will-change:transform;
}
/* Glass. A rim of light on the edge nearest the key, which slides around the card
   as it tilts — the single clearest cue that the thing has thickness. */
.scope::before{
  content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:2;
  background:linear-gradient(
    calc(115deg + var(--px) * 40deg),
    rgba(233,230,255,.10) 0%, rgba(233,230,255,0) 34%,
    rgba(233,230,255,0) 66%, rgba(124,92,255,.09) 100%);
}
/* one slow pass of light, the way a scanner reads a floor */
.scope::after{
  content:'';position:absolute;left:0;right:0;height:38%;top:-38%;pointer-events:none;z-index:1;
  background:linear-gradient(180deg,transparent,rgba(167,139,250,.11),transparent);
  animation:sweep 7.5s linear infinite;
}
@keyframes sweep{to{transform:translateY(370%)}}

.scope-bar{
  display:flex;align-items:center;gap:12px;padding:11px 16px;
  border-bottom:1px solid var(--border);background:rgba(233,230,255,.02);
  font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--dim);
}
.scope-bar .live{display:inline-flex;align-items:center;gap:7px;color:var(--green)}
.scope-bar .live i{width:6px;height:6px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:blink 2.6s var(--ease) infinite;font-style:normal}
.scope-bar .shop{color:var(--muted)}
.scope-bar .t{margin-left:auto}

.scope-body{padding:16px}

.alert{
  position:relative;display:flex;align-items:center;gap:12px;padding:13px 14px 13px 15px;border-radius:9px;
  /* tint + label, no side stripe — the app bans status stripes and so does this */
  background:rgba(239,68,68,.09);border:1px solid rgba(239,68,68,.32);
}
/* One pulse when the panel lands, then it stops. An alert that throbs forever is
   wallpaper; an alert that fires once is a thing you look at. */
.js .scope-mount.in .alert::after{
  content:'';position:absolute;inset:-1px;border-radius:inherit;pointer-events:none;
  border:1px solid var(--red);animation:alertPing 1.5s var(--ease) .55s 2 both;
}
@keyframes alertPing{
  0%{opacity:.9;transform:scale(1)}
  70%,100%{opacity:0;transform:scale(1.035)}
}
.alert .k{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:#FCA5A5;flex:none}
.alert .j{font-weight:600;font-size:.95rem;letter-spacing:-.01em}
.alert .m{margin-left:auto;font-family:var(--font-mono);font-size:11.5px;color:var(--muted);flex:none}
@media(max-width:420px){.alert{flex-wrap:wrap;gap:6px 10px}.alert .m{margin-left:0}}

.stage-head{
  display:flex;align-items:baseline;justify-content:space-between;
  margin:22px 2px 12px;font-family:var(--font-word);font-size:10px;
  letter-spacing:.2em;text-transform:uppercase;color:var(--ink);opacity:.75;
}
.stage-head span{font-family:var(--font-mono);letter-spacing:.06em;color:var(--dim);opacity:1;text-transform:none}
.stages{display:grid;grid-template-columns:repeat(auto-fit,minmax(64px,1fr));gap:8px}
.stg{
  padding:11px 10px 10px;border:1px solid var(--border);border-radius:9px;
  background:rgba(233,230,255,.018);
  /* the tiles sit a hair proud of the panel and lift with it */
  box-shadow:var(--rim);
}
.stg .n{font-family:var(--font-display);font-weight:700;font-size:1.28rem;line-height:1;letter-spacing:-.03em}
.stg .l{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);margin-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stg .bar{height:2px;margin-top:9px;border-radius:2px;background:rgba(233,230,255,.1);overflow:hidden}
/* Bars ease to value when the panel arrives, left to right, like an instrument
   taking a reading. Width is the value; scaleX is the animation. */
.stg .bar i{display:block;height:100%;background:var(--signal);border-radius:2px;transform-origin:left}
.js .scope .bar i{transform:scaleX(0);transition:transform 1.05s var(--ease);transition-delay:calc(.35s + var(--i,0) * 70ms)}
.js .scope-mount.in .bar i{transform:none}
.stg.hot{border-color:rgba(239,68,68,.4);background:rgba(239,68,68,.06)}
.stg.hot .n{color:#FCA5A5}
.stg.hot .bar i{background:var(--red)}

.scope-foot{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;margin-top:16px;border-top:1px solid var(--border);padding-top:14px}
.scope-foot .c .v{font-family:var(--font-mono);font-weight:500;font-size:1.02rem;letter-spacing:-.02em}
.scope-foot .c .v.money{color:var(--gold)}
.scope-foot .c .k{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--dim);margin-top:5px}

/* ---------- proof band ----------
   Sits directly under the hero. Quieter than a section, louder than a footnote.
   The grid is one column until a photo exists; :has() means uncommenting the
   <figure> is the only edit needed to go two-up. */
.proof{padding:clamp(2.6rem,5vw,4rem) 0;border-block:1px solid var(--border);background:rgba(12,12,22,.5)}
.proof-grid{display:grid;gap:clamp(26px,4vw,56px);align-items:center}
.proof-grid:has(.proof-shot){grid-template-columns:minmax(0,.72fr) minmax(0,1fr)}
@media(max-width:860px){.proof-grid:has(.proof-shot){grid-template-columns:1fr}}
.proof-shot{border:1px solid var(--border-2);border-radius:12px;overflow:hidden;background:var(--surface)}
.proof-shot img{display:block;width:100%;height:auto}

.proof-line{
  font-family:var(--font-display);font-weight:600;
  font-size:clamp(1.25rem,2.1vw,1.7rem);line-height:1.32;letter-spacing:-.025em;
  color:var(--muted);max-width:46ch;margin-top:18px;text-wrap:pretty;
}
.proof-line b{color:var(--ink);font-weight:700}

.proof-by{display:flex;align-items:center;gap:16px;margin-top:26px}
.proof-by .proof-mark{width:46px;height:46px;flex:none}
.proof-by b{display:block;font-size:1.05rem;font-weight:600;letter-spacing:-.015em;margin-bottom:3px}
.proof-by span{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.04em;color:var(--dim)}

/* ---------- the band, before the photo exists ----------
   Left-hanging in one narrow column, the band left ~430px of the field empty at
   1440 and read as unfinished rather than airy. Until the shop photo lands, the
   statement takes a fuller measure and the person signs it at the right edge, on
   the baseline of the last line — the way an editorial pull quote is attributed.
   That also promotes the attribution, which is the band's actual trust signal and
   was whispering at the bottom left.

   Scoped with :not(:has(.proof-shot)), so the instant /assets/lineage-floor.webp
   lands and the <figure> is uncommented, this stops matching and the body returns
   to a single column beside the photo. Nothing to undo. Browsers without :has()
   get today's single-column band, which is the state this replaces. */
.proof-grid:not(:has(.proof-shot)) .proof-body{
  display:grid;grid-template-columns:minmax(0,1fr) auto;
  column-gap:clamp(32px,5vw,80px);row-gap:26px;align-items:end;
}
.proof-grid:not(:has(.proof-shot)) .proof-body>.kicker,
.proof-grid:not(:has(.proof-shot)) .proof-nums{grid-column:1/-1}
.proof-grid:not(:has(.proof-shot)) .proof-line{max-width:56ch;margin-top:0}
.proof-grid:not(:has(.proof-shot)) .proof-by{margin-top:0}
@media(max-width:860px){
  .proof-grid:not(:has(.proof-shot)) .proof-body{grid-template-columns:1fr;align-items:start}
}

.proof-nums{list-style:none;display:flex;flex-wrap:wrap;gap:14px 44px;margin-top:30px}
.proof-nums li{color:var(--dim);font-size:.9rem;line-height:1.4;max-width:16ch}
.proof-nums b{
  display:block;font-family:var(--font-mono);font-weight:500;font-size:1.55rem;
  letter-spacing:-.03em;color:var(--ink);margin-bottom:5px;
}
.proof-nums b.money{color:var(--gold)}

/* ---------- index of what it sees ----------
   An instrument panel, not a table of contents. This section carries the whole
   product promise, so the numerals are machined at display scale and set the
   rhythm: they are what makes six rows read as an indexed spec sheet rather than
   a bulleted list. Full-strength --dim, never faded — 4.81:1, and it stays there. */
.index{
  display:grid;grid-template-columns:1fr 1fr;column-gap:clamp(30px,4vw,68px);
  margin-top:clamp(32px,4vw,52px);border-top:1px solid var(--border);list-style:none;
}
@media(max-width:860px){.index{grid-template-columns:1fr}}
.index li{
  /* The spec column is a fixed track, not `auto`. Sized to its own content, a row
     with a long label ("STOCK · REORDER") wrapped its copy at a different measure
     than a row with a short one ("BOARD"), so the two columns never lined up. */
  position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) 112px;
  gap:0 clamp(18px,2vw,26px);align-items:start;
  padding:clamp(22px,2.4vw,28px) 16px clamp(22px,2.4vw,28px) 0;
  border-bottom:1px solid var(--border);
  transition:background .35s var(--ease),padding .35s var(--ease),box-shadow .35s var(--ease);
}
/* the rail lights on the row you are reading */
.index li::before{
  content:'';position:absolute;left:0;top:12px;bottom:12px;width:2px;
  background:linear-gradient(180deg,var(--signal),var(--signal-bright));
  transform:scaleY(.3);transform-origin:center;opacity:0;
  transition:opacity .3s var(--ease),transform .45s var(--ease);
}
.index li:hover,.index li:focus-within{
  background:linear-gradient(90deg,rgba(124,92,255,.07),transparent 72%);
  padding-left:16px;box-shadow:var(--elev-1);
}
.index li:hover::before,.index li:focus-within::before{opacity:1;transform:scaleY(1)}
.index .n{
  font-family:var(--font-mono);font-weight:500;
  font-size:clamp(1.45rem,2.1vw,2rem);line-height:1;letter-spacing:-.045em;
  color:var(--dim);padding-top:3px;min-width:2.2ch;
  transition:color .35s var(--ease);
}
.index li:hover .n,.index li:focus-within .n{color:var(--signal-bright)}
.index h3{
  font-family:var(--font-display);font-weight:700;font-stretch:calc(var(--wdth-h2) * 1%);
  font-size:clamp(1.24rem,1.5vw,1.45rem);letter-spacing:-.03em;line-height:1.15;
}
/* No ch cap: the three-column grid already sets the measure, and a 44ch cap inside
   a 594px column left a dead channel between the copy and its spec label. */
.index p{color:var(--muted);font-size:.99rem;line-height:1.5;margin-top:7px}
.index .meta{
  font-family:var(--font-mono);font-size:10.5px;letter-spacing:.12em;
  text-transform:uppercase;color:var(--dim);padding-top:11px;white-space:nowrap;
  text-align:right;
}
@media(max-width:600px){
  .index .meta{display:none}
  .index li{grid-template-columns:auto minmax(0,1fr)}
}

/* ---------- product shots ----------
   Real captures from the app (tools/capture-shots.mjs), framed in CSS.
   The status chip is the app's own — same ghost fill, same mono caps — so the
   page and the product read as one thing. */
.shots{display:flex;flex-direction:column;gap:clamp(60px,9vw,116px);margin-top:clamp(46px,6vw,76px)}
/* The desktop shots run the full column. Beside the copy they were painted at
   0.49x of the viewport they were captured at, which put every label inside the
   product under 7px: a section titled "Inside the app" that showed the visitor
   nothing readable. Full width lands them near 0.86x, and the copy reads fine
   as a lead-in rather than a column. */
.shot-copy{max-width:56ch}
.shot .frame{margin-top:clamp(22px,3vw,34px)}
/* Same machined index as #sees, so the two mid-page sections read as one system. */
.shot-n{
  display:block;font-family:var(--font-mono);font-weight:500;
  font-size:clamp(1.45rem,2.1vw,2rem);line-height:1;letter-spacing:-.045em;
  color:var(--dim);margin-bottom:16px;
}

/* A phone is a narrow object, so it keeps the two-up layout: at full width a
   390px capture would be blown up well past life size. */
.shot-phone{
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.62fr);
  gap:clamp(28px,5vw,64px);align-items:center;max-width:1040px;margin-inline:auto;
  /* .shots is a column flex, and margin-inline:auto cancels the default stretch,
     leaving this row shrink-wrapped to its content. Say the width out loud. */
  width:100%;
}
.shot-phone .shot-copy{max-width:44ch}
.shot-copy h3{
  font-family:var(--font-display);font-weight:700;letter-spacing:-.035em;line-height:1.08;
  font-size:clamp(1.5rem,2.7vw,2.1rem);margin-top:16px;text-wrap:balance;
}
.shot-copy p{color:var(--muted);font-size:1.03rem;line-height:1.55;margin-top:14px;max-width:44ch}

.chip{
  display:inline-flex;align-items:center;gap:7px;padding:4px 11px;border-radius:6px;border:1px solid;
  font-family:var(--font-mono);font-size:11px;letter-spacing:.09em;text-transform:uppercase;
}
.chip i{width:5px;height:5px;border-radius:50%;background:currentColor;flex:none}
.chip-red{color:#FCA5A5;background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.26)}
.chip-gold{color:var(--gold);background:rgba(201,168,76,.1);border-color:rgba(201,168,76,.28)}
.chip-violet{color:var(--signal-bright);background:rgba(124,92,255,.1);border-color:rgba(124,92,255,.3)}

/* Devices, photographed rather than pasted. The mount holds the perspective and
   the tilt tracks the pointer, so the screenshot reads as a panel on a desk under
   a light instead of a PNG on a page. */
.frame{
  position:relative;border:1px solid var(--border-2);border-radius:12px;overflow:hidden;background:var(--surface);
  box-shadow:var(--elev-lift), var(--rim);
  transform:perspective(1800px)
    rotateY(calc(var(--px) * -2.4deg))
    rotateX(calc(var(--py) * 1.6deg))
    translate3d(calc(var(--px) * -5px), calc(var(--py) * -3px), 0);
  transition:box-shadow .5s var(--ease);
}
/* edge light: the top-left corner catches the same key as everything else */
.frame::after{
  content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:2;
  background:linear-gradient(calc(118deg + var(--px) * 34deg),
    rgba(233,230,255,.09) 0%, rgba(233,230,255,0) 30%,
    rgba(233,230,255,0) 72%, rgba(124,92,255,.08) 100%);
}
.shot:hover .frame{box-shadow:0 46px 110px -34px rgba(124,92,255,.55), var(--rim)}

/* Scroll parallax on the devices, driven by the scroll position rather than a
   scroll handler: the frame rides past its own copy at a different rate, which is
   what makes it read as an object at a distance instead of a block in the flow.
   `translate` composes with the pointer tilt on `transform`. Browsers without the
   view timeline get a static, correctly-placed frame. */
@supports (animation-timeline:view()){
  .shot .frame,.shot .phone{
    animation:deviceDrift linear both;
    animation-timeline:view();
    animation-range:entry 0% exit 100%;
  }
  @keyframes deviceDrift{from{translate:0 30px}to{translate:0 -30px}}
}
.frame-bar{
  display:flex;align-items:center;gap:14px;padding:9px 13px;
  border-bottom:1px solid var(--border);background:rgba(233,230,255,.03);
}
.dots{display:flex;gap:6px;flex:none}
.dots i{width:9px;height:9px;border-radius:50%;background:rgba(233,230,255,.13)}
.frame .url{
  font-family:var(--font-mono);font-size:11px;letter-spacing:.02em;color:var(--dim);
  background:rgba(8,8,16,.55);border:1px solid var(--border);border-radius:5px;padding:3px 11px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.frame img{display:block;width:100%;height:auto}

/* ---------- the scan ----------
   The section is called "inside the app" and the brand is an eye, so the screens
   resolve under a pass of light rather than appearing whole: a violet bar travels
   down the glass and the capture materialises behind its leading edge. Same light
   as the hero scope's sweep, so the page has one vocabulary rather than two.

   clip-path is the one property here outside the transform/opacity/filter rule.
   It composites, it runs once per screen, and nothing else reveals a rectangle
   top-down without either squashing the image or animating a box. Worth it. */
.frame-screen,.phone-screen{position:relative;overflow:hidden;line-height:0}
.phone-screen{border-radius:30px}
.scan{
  position:absolute;left:0;right:0;top:0;height:14%;pointer-events:none;z-index:2;
  opacity:0;
  background:linear-gradient(180deg,
    transparent, rgba(167,139,250,.10) 46%, rgba(167,139,250,.34) 84%, var(--signal-bright));
}
/* No resting clip: the from-state lives in the keyframes and arrives with the
   animation. A base `clip-path` here would mean an image clipped to nothing for
   good if the observer ever failed to fire, and a screenshot that might not show
   is a worse bug than a reveal that might not play. The parent `.reveal` is
   already at opacity 0 until `.in`, so there is nothing to flash. */
.js .shot.in .frame-screen img,
.js .shot.in .phone-screen img{animation:screenIn 1.05s var(--ease) .1s both}
.js .shot.in .scan{animation:scanBar 1.05s var(--ease) .1s both}
@keyframes screenIn{
  from{clip-path:inset(0 0 100% 0)}
  to{clip-path:inset(0 0 0 0)}
}
@keyframes scanBar{
  0%{opacity:0;transform:translateY(-100%)}
  10%{opacity:1}
  86%{opacity:1}
  100%{opacity:0;transform:translateY(614%)}
}

/* 356px, not 286px: the capture is a 390px viewport, and 266px of paint made it
   0.68x. This puts it at 0.86x, matching the desktop shots. */
.phone{
  position:relative;max-width:356px;margin-inline:auto;padding:10px;border-radius:40px;
  border:1px solid var(--border-2);
  background:linear-gradient(180deg,rgba(233,230,255,.075),rgba(233,230,255,.015));
  box-shadow:0 40px 96px -34px rgba(124,92,255,.46), var(--rim);
  transform:perspective(1400px)
    rotateY(calc(var(--px) * -4deg))
    rotateX(calc(var(--py) * 2.6deg))
    translate3d(calc(var(--px) * -9px), calc(var(--py) * -5px), 0);
}
/* the aluminium catching the key light down one side of the case */
.phone::after{
  content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(calc(120deg + var(--px) * 46deg),
    rgba(233,230,255,.16) 0%, rgba(233,230,255,0) 26%,
    rgba(233,230,255,0) 74%, rgba(124,92,255,.12) 100%);
}
/* No notch: the bezel is 10px, so a pill drawn there lands inside the captured
   viewport and reads as part of the app's own header. The radius moved to the
   screen wrapper so the scan is clipped to the glass, not the image box. */
.phone img{display:block;width:100%;height:auto}

@media (max-width:900px){
  .shot-phone{grid-template-columns:1fr;gap:26px}
  .shot-copy,.shot-copy p,.shot-phone .shot-copy{max-width:none}
  /* Below 900 the frames carry the 390px capture. Uncapped, a 768px tablet would
     paint it at 1.8x life size. 420 keeps every shot inside 0.85x to 1.1x. */
  .frame{max-width:420px;margin-inline:auto}
}

/* ---------- facts strip ---------- */
.facts{border-block:1px solid var(--border);background:rgba(12,12,22,.55);padding:0}
.facts ul{list-style:none;display:grid;grid-template-columns:repeat(3,1fr)}
@media(max-width:760px){.facts ul{grid-template-columns:1fr}}
.facts li{
  padding:30px clamp(20px,3vw,36px);border-left:1px solid var(--border);
  color:var(--muted);font-size:.97rem;
}
.facts li:first-child{border-left:0}
@media(max-width:760px){.facts li{border-left:0;border-top:1px solid var(--border);padding:22px 0}.facts li:first-child{border-top:0}}
/* Up from 1.7rem: with the index and the shots both carrying display-scale
   numerals, a thin strip of small figures between them read as a gap in the page
   rather than a band. These are the three numbers that answer "what does it cost". */
.facts b{
  display:block;font-family:var(--font-mono);font-weight:500;
  font-size:clamp(1.9rem,3vw,2.6rem);line-height:1;
  letter-spacing:-.04em;color:var(--ink);margin-bottom:10px;
}
.facts b.money{color:var(--gold)}

/* ---------- editorial split ---------- */
.split{display:grid;grid-template-columns:minmax(0,.34fr) minmax(0,1fr);gap:clamp(24px,5vw,72px);align-items:start}
@media(max-width:860px){.split{grid-template-columns:1fr;gap:20px}}
.split .rule{margin-top:14px;height:1px;background:linear-gradient(90deg,var(--border-2),transparent)}
.pull{
  font-family:var(--font-display);font-weight:600;font-size:clamp(1.15rem,1.7vw,1.4rem);
  line-height:1.35;letter-spacing:-.02em;color:var(--ink);
  margin-top:28px;padding-left:20px;box-shadow:inset 2px 0 0 var(--signal);max-width:50ch;
}

/* ---------- final CTA ---------- */
.final{position:relative;text-align:center;overflow:hidden}
.final h2{
  font-family:var(--font-display);font-weight:800;font-stretch:calc(var(--wdth-display) * 1%);
  font-size:clamp(2.2rem,5.2vw,3.9rem);letter-spacing:-.04em;line-height:1;text-wrap:balance;
}
.final p{color:var(--muted);margin:20px auto 0;max-width:50ch;font-size:1.08rem}
.final .cta-row{justify-content:center}
/* only the crown of the wheel clears the fold — a horizon, not a logo */
.horizon{
  position:absolute;left:50%;top:56%;width:min(1400px,150vw);aspect-ratio:1;
  transform:translateX(-50%);pointer-events:none;opacity:.55;
  -webkit-mask-image:linear-gradient(180deg,#000 0 22%,transparent 48%);
          mask-image:linear-gradient(180deg,#000 0 22%,transparent 48%);
}

/* ---------- footer ---------- */
footer{border-top:1px solid var(--border);padding:38px 0 30px;margin-top:20px}
.foot{display:flex;align-items:flex-start;justify-content:space-between;gap:28px;flex-wrap:wrap;color:var(--dim);font-size:13.5px}
.foot .brand .word{font-size:15px}
.foot nav{position:static;padding:0;background:none;backdrop-filter:none;display:flex;gap:22px;flex-wrap:wrap}
/* 22px tall was under the WCAG 2.2 target-size minimum, on the device this
   audience actually browses from. Padding, not font size. */
.foot nav a{display:inline-block;padding:11px 0}
/* inline-block with padding so the credit link clears the 24px target minimum */
.foot a{color:var(--muted);text-decoration:none;display:inline-block;padding-block:5px}
.foot a:hover{color:var(--ink)}
.foot .tag{font-family:var(--font-mono);letter-spacing:.04em}

/* ---------- reveal ----------
   Scoped to .js so the page is fully readable when scripts don't run. Content
   rises *out of* the page rather than sliding up it: the self-contained
   perspective plus a rotateX hinge is what separates this from a fade. */
.js .reveal{
  opacity:0;
  transform:perspective(1200px) translate3d(0,30px,-70px) rotateX(5deg);
  transform-origin:50% 100%;filter:blur(7px);
  transition:opacity var(--dur-reveal) var(--ease),transform .85s var(--ease),filter var(--dur-reveal) var(--ease);
}
.js .reveal.in{opacity:1;transform:none;filter:none}
[data-d="1"]{transition-delay:.08s}[data-d="2"]{transition-delay:.18s}
[data-d="3"]{transition-delay:.28s}[data-d="4"]{transition-delay:.38s}[data-d="5"]{transition-delay:.48s}

/* Above the fold there is nothing to observe — it is already on screen — so the
   entrance runs as an animation from first render instead of waiting for the
   deferred script to add a class. Same keyframes as the transition above, so the
   two paths are visually identical. */
@keyframes revealIn{
  from{opacity:0;transform:perspective(1200px) translate3d(0,30px,-70px) rotateX(5deg);filter:blur(7px)}
  to{opacity:1;transform:none;filter:none}
}
.js header .reveal{animation:revealIn .85s var(--ease) both}
.js header [data-d="1"]{animation-delay:.08s}
.js header [data-d="2"]{animation-delay:.18s}
.js header [data-d="3"]{animation-delay:.28s}
.js header [data-d="4"]{animation-delay:.38s}
.js header [data-d="5"]{animation-delay:.48s}
/* Lists cascade off their own index instead of a fixed set of delay steps, so the
   rule holds however many rows there are. Must sit after `.js .reveal`, whose
   transition shorthand resets the delay. */
.js .index li.reveal,.js .faq>div.reveal{transition-delay:calc(var(--i,0) * 70ms)}

/* ---------- reduced motion ----------
   Everything above degrades to its finished state. The page keeps its depth —
   shadows, rim light, layered rings — and loses only the movement. */
@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation:none!important;transition:none!important}
  .js .reveal,.js .kin .w{opacity:1;transform:none;filter:none}
  .js .scope .bar i{transform:none}
  /* Frozen, these become a permanent scan bar and a permanent second border on the
     alert, which is worse than nothing. */
  .scope::after,.js .scope-mount.in .alert::after,.scan{display:none}
  /* the pointer and scroll channels are pinned shut, so nothing tracks anything */
  :root{--px:0;--py:0;--hy:0;--spin:0}
}

/* ---------- inner page header ----------
   Same instrument as the hero, sized and quietened for a shorter box. Before it,
   the right half of every inner page head was empty field: the pages shared the
   tokens and the nav but not the atmosphere, which is exactly how a site starts
   feeling like two sites. */
.page-head{position:relative;overflow:hidden;padding:clamp(7.5rem,13vw,10rem) 0 0}
.page-head .wrap{position:relative;z-index:1}
/* The head box is ~320-450px and the dial is 620px, so `overflow:hidden` cut the
   outer ring off on a hard horizontal edge. Dissolve the instrument into the page
   instead of slicing it. */
.stage-head{
  height:100%;
  -webkit-mask-image:linear-gradient(180deg,#000 0 58%,transparent 96%);
          mask-image:linear-gradient(180deg,#000 0 58%,transparent 96%);
}
.stage-head .dial,.stage-head .beam{width:min(620px,64vw);right:-12%;top:52%}
.stage-head .lyr-mid{opacity:.5}
.stage-head .lyr-near{opacity:.62}
@media(max-width:900px){
  .stage-head .dial,.stage-head .beam{right:-34%;top:44%;width:min(520px,112vw)}
  .stage-head .lyr-mid{opacity:.28}
  .stage-head .lyr-near{opacity:.36}
}
/* Inner-page headlines are two sentences in two tones. Breaking between them is
   the only correct break, so state it rather than hoping the wrapper agrees. */
.page-head h1{margin-top:18px;max-width:18ch}
.page-head h1 .glow{display:block}
.page-head .sub{max-width:52ch}

/* ---------- pricing ---------- */
/* stretch so the three CTAs line up on one baseline regardless of list length */
.plans{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:52px;align-items:stretch}
@media(max-width:940px){.plans{grid-template-columns:1fr;max-width:520px;margin-inline:auto}}
.plan{
  position:relative;background:linear-gradient(180deg,var(--surface),rgba(14,14,27,.4));
  border:1px solid var(--border);border-radius:14px;padding:28px 26px 30px;
  display:flex;flex-direction:column;gap:18px;
  box-shadow:var(--rim);
  transition:border-color var(--dur-int) var(--ease),transform .4s var(--ease),box-shadow .4s var(--ease);
}
.plan:hover{border-color:var(--border-2);transform:translateY(-4px);box-shadow:var(--elev-2),var(--rim)}
/* The plan most shops buy stands a step forward of the other two, at rest. */
.plan.featured{
  border-color:rgba(124,92,255,.45);background:linear-gradient(180deg,rgba(26,20,52,.9),rgba(14,14,27,.5));
  box-shadow:0 26px 60px -34px rgba(124,92,255,.6),var(--rim);
}
.plan.featured:hover{box-shadow:0 40px 84px -34px rgba(124,92,255,.72),var(--rim)}
.plan .tag{
  position:absolute;top:-11px;left:26px;font-family:var(--font-mono);font-size:10.5px;
  letter-spacing:.16em;text-transform:uppercase;color:#fff;background:var(--signal-cta);
  padding:4px 12px;border-radius:5px;
}
.plan .name{font-family:var(--font-word);font-size:12px;letter-spacing:.2em;text-transform:uppercase;color:var(--ink)}
.plan .price{font-family:var(--font-mono);font-weight:500;font-size:2.9rem;line-height:1;color:var(--gold);letter-spacing:-.045em}
.plan .price .per{font-family:var(--font-body);font-size:1rem;font-weight:400;color:var(--muted);letter-spacing:0}
.plan .annual{font-family:var(--font-mono);font-size:12px;color:var(--dim);letter-spacing:.02em;margin-top:-10px}
.plan .seats{color:var(--ink);font-weight:600;font-size:.98rem;padding-bottom:18px;border-bottom:1px solid var(--border)}
.plan ul{list-style:none;display:flex;flex-direction:column;gap:11px;flex:1}
.plan li{display:flex;gap:11px;color:var(--muted);font-size:.95rem;line-height:1.45}
.plan li svg{width:17px;height:17px;flex:none;margin-top:3px;color:var(--signal-bright)}
.plan li.inherits{color:var(--dim);font-style:normal}
.plan .btn{width:100%}

.fine{margin-top:30px;color:var(--dim);font-size:13px;font-family:var(--font-mono);letter-spacing:.02em;text-align:center}

/* ---------- FAQ / definition list ---------- */
.band{border-block:1px solid var(--border);background:rgba(12,12,22,.5)}
.faq{display:grid;grid-template-columns:1fr 1fr;gap:14px 40px;margin-top:44px}
@media(max-width:820px){.faq{grid-template-columns:1fr}}
.faq h3{font-family:var(--font-display);font-weight:600;font-size:1.05rem;margin-bottom:6px;letter-spacing:-.02em}
.faq p{color:var(--muted);font-size:.95rem}

/* ---------- legal / long-form ---------- */
/* .legal rides on .wrap. Capping the wrap itself re-centred the whole column and
   left it out of line with the headline above, so cap the measure instead. */
.legal>*{max-width:74ch}
.legal h2{font-family:var(--font-display);font-weight:700;font-size:1.3rem;margin-top:44px;letter-spacing:-.025em}
.legal p,.legal li{color:var(--muted);font-size:1rem;margin-top:14px}
.legal ul{margin-left:20px}
.legal li{margin-top:8px}
.legal b,.legal strong{color:var(--ink);font-weight:600}
.legal a{color:var(--signal-bright)}
.legal .meta{font-family:var(--font-mono);font-size:12px;color:var(--dim);letter-spacing:.03em;margin-top:8px}
