/* No transition on --light. The script interpolates it continuously
   against scroll position, so a CSS transition on top would smear
   and lag behind the scrub. Section-local overrides below are the
   only place --light changes discretely. */

/* ============================================================
   Reset and base
   ============================================================ */

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

/* clip, not hidden: hidden on the root turns html into a scroll
   container and breaks ScrollTrigger pinning. */
html {
  -webkit-text-size-adjust: 100%;
  overflow-x: clip;
}

body {
  margin: 0;
  background: var(--obsidian);
  color: var(--on-obsidian-70);
  font-family: var(--font-text);
  font-variation-settings: "opsz" 14, "wght" 300;
  font-size: var(--t-body);
  line-height: 1.68;
  overflow-x: clip;
}

html.lenis { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-stopped { overflow: hidden; }

img { max-width: 100%; display: block; }

::selection { background: var(--light); color: var(--obsidian); }

:focus-visible {
  outline: 2px solid var(--light);
  outline-offset: 3px;
}

.u-visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Above the preload gate's z-index, so it is still reachable if a
   keyboard user tabs while the sequence is loading. */
.skip {
  position: fixed;
  top: var(--s4);
  left: var(--s4);
  z-index: 100;
  padding: var(--s3) var(--s5);
  border-radius: var(--r-s);
  background: var(--on-obsidian);
  color: var(--obsidian);
  font-family: var(--font-num);
  font-size: var(--t-meta);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  /* Clipped, not translated. A transform transition on a fixed element
     left the link stuck on screen after its first focus/blur cycle, and
     a skip link that will not go away again is worse than one that does
     not animate. clip-path and opacity both stay off the layout path. */
  clip-path: inset(50%);
  opacity: 0;
  pointer-events: none;
}

.skip:focus {
  clip-path: none;
  opacity: 1;
  pointer-events: auto;
}

/* The target of the skip link. tabindex -1 makes it a focus target; the
   outline is suppressed because the visible result of skipping is the
   viewport moving, not a ring around the whole document. */
main:focus { outline: none; }

/* ============================================================
   Refraction 1: the caustic wash

   Light bleeding across the dark. Fixed and pointer-events:none
   so scrolling content never repaints because of it.
   ============================================================ */

/* inset 0, not a negative inset: the sphere's position is authored
   in viewport percentages and the canvas reflection has to project
   the exact same point. A larger-than-viewport box would put the
   visual sphere somewhere other than where the script thinks it is.

   contain: strict pins that promise down and, more importantly, walls
   the layer off: the sphere's colour changes on every scroll frame, and
   without containment that repaint was being taken as a full-viewport
   repaint underneath the whole scrolling page. */
.caustics {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  contain: strict;
}

/* The sphere is a real element, centred on its own origin, so the script
   moves it with a transform instead of re-authoring a gradient position.
   margin pulls it back by half its size, which puts its centre exactly on
   the translated point: the same viewport coordinate the canvas
   reflection projects from. */
.caustics__sphere {
  position: absolute;
  top: 0;
  left: 0;
  width: 68vmax;
  height: 68vmax;
  margin: -34vmax 0 0 -34vmax;
  border-radius: 50%;
  background: radial-gradient(circle closest-side,
    color-mix(in oklch, var(--light) 34%, transparent) 0%,
    color-mix(in oklch, var(--light) 15%, transparent) 36%,
    transparent 66%);
  /* Was 30px. The gradient's own 66% falloff is already soft, so most of
     that blur was buying nothing; what is left is there to kill banding
     on a near-black ground, which is the only job it ever actually did. */
  filter: blur(16px);
  will-change: transform;
  /* Starts high and right of its resting point. updateLight overwrites
     this with the real scroll-derived position on the first frame after
     the gate lifts, and the transition added by .is-arriving carries it
     there. */
  transform: translate3d(88vw, -10vh, 0);
}

/* The dimmer second sphere low on the opposite side. Fixed position, so
   it never moves and never needs its own layer. */
.caustics__floor {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle 52vmax at 10% 88%,
    color-mix(in oklch, var(--light) 9%, transparent) 0%,
    transparent 72%);
}

main, .header, .footer { position: relative; z-index: 1; }

/* ============================================================
   Type primitives
   ============================================================ */

.t-hero, .t-h2, .t-h3 {
  font-family: var(--font-display);
  color: var(--on-obsidian);
  margin: 0;
  text-wrap: balance;
}

.t-hero {
  font-size: var(--t-hero);
  font-variation-settings: "opsz" 96, "wght" 700;
  line-height: 0.92;
  letter-spacing: -0.025em;
}

.t-h2 {
  font-size: var(--t-h2);
  font-variation-settings: "opsz" 72, "wght" 700;
  line-height: 0.98;
  letter-spacing: -0.02em;
}

.t-h3 {
  font-size: var(--t-h3);
  font-variation-settings: "opsz" 32, "wght" 600;
  line-height: 1.15;
}

.t-lede {
  font-size: var(--t-lede);
  font-variation-settings: "opsz" 16, "wght" 300;
  line-height: 1.6;
  max-width: 52ch;
  margin: 0;
}

.t-body { max-width: 66ch; margin: 0; }
.t-body + .t-body { margin-top: var(--s5); }

/* Refraction 4: bloom behind figures. The number reads as lit
   from behind rather than painted a color. */
.t-num {
  font-family: var(--font-num);
  font-size: var(--t-num);
  font-variation-settings: "wght" 400;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--on-obsidian);
  text-shadow: 0 0 42px color-mix(in oklch, var(--light) 45%, transparent);
  transition: text-shadow var(--d-slow) linear;
  margin: 0;
}

.t-meta {
  font-family: var(--font-num);
  font-size: var(--t-meta);
  font-variation-settings: "wght" 400;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--on-obsidian-70);
}

/* ============================================================
   Layout
   ============================================================ */

.shell {
  width: var(--max);
  margin-inline: auto;
}

.grid-12 {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--col-gap);
}

section { padding-block: var(--section-y); }

/* DESIGN.md asks for section rhythm to vary rather than run uniform, and
   it was running uniform. The three destinations are one sequence, not
   three separate arguments, so they close up against each other and the
   full measure is spent on the boundaries either side of them. */
#split { padding-block: var(--section-y) var(--s8); }

/* The three destinations are one sequence, so they sit closer to each
   other than to the sections either side. Closer, not cramped: at --s7
   they read as a single dense block rather than three beats. */
section[data-light] { padding-block: clamp(140px, 11vw, 200px); }
section[data-light="spend"] { padding-bottom: var(--section-y); }
#trust { padding-block: var(--section-y) var(--s9); }
#open { padding-block: var(--s9) calc(var(--section-y) * 1.25); }

/* ============================================================
   Facet: a large rounded field cut from the block.
   Not a card: these are page-scale surfaces, never stacked,
   never nested, never translucent.
   ============================================================ */

.facet {
  position: relative;
  border-radius: var(--r-xl);
  background: linear-gradient(168deg,
    var(--obsidian-2) 0%,
    var(--obsidian) 68%);
  padding-block: clamp(96px, 10vw, 180px);
  padding-inline: clamp(var(--s6), 5vw, var(--s10));
  overflow: hidden;
}

/* Refraction 2: exit-edge glow. Light escaping the cut edge.
   Top edge only. A colored left or right stripe is banned and
   this must not become one. */
.facet::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg,
    transparent 0%,
    color-mix(in oklch, var(--light) 55%, transparent) 22%,
    color-mix(in oklch, var(--light) 55%, transparent) 78%,
    transparent 100%);
  transition: background var(--d-slow) linear;
}

/* Refraction 3: specular sweep. A steep highlight travelling
   across the facet. Transform only, so it stays on the GPU. */
.facet::after {
  content: "";
  position: absolute;
  inset: -40% -10%;
  pointer-events: none;
  background: linear-gradient(104deg,
    transparent 42%,
    color-mix(in oklch, var(--light) 14%, transparent) 50%,
    transparent 58%);
  transform: translate3d(-60%, 0, 0);
  transition: transform 1400ms var(--e-out);
}

.facet.is-lit::after { transform: translate3d(60%, 0, 0); }

/* The conchoidal fracture cut. Sparingly: at most twice a page,
   or it stops being an accent and becomes a motif. */
.facet--fractured {
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 88px), calc(100% - 120px) 100%, 0 100%);
}

/* ============================================================
   Header
   ============================================================ */

.header {
  position: fixed;
  inset: 0 0 auto 0;
  height: var(--header-h);
  display: flex;
  align-items: center;
  z-index: 10;
  pointer-events: none;
}

.header__inner {
  width: var(--max);
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  pointer-events: auto;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
  font-family: var(--font-display);
  font-variation-settings: "opsz" 24, "wght" 700;
  font-size: 1.125rem;
  letter-spacing: -0.01em;
  color: var(--on-obsidian);
  text-decoration: none;
  margin: 0;
}

/* Optically sized against the cap height rather than the em box:
   the mark's artwork sits inside a 64 unit square with padding, so
   matching it to the type's cap height leaves it looking small. */
.wordmark__mark { width: 1.45em; height: 1.45em; flex: none; }

/* Not a .cta. The page has exactly one call to action and this is not
   it; sharing the class made the header read as a second one and made
   "one button on the page" false in the code as well as on screen. */
.navlink {
  display: inline-block;
  font-family: var(--font-num);
  font-size: var(--t-meta);
  font-variation-settings: "wght" 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: var(--s3) var(--s5);
  border: 1px solid color-mix(in oklch, var(--facet-edge) 80%, transparent);
  border-radius: var(--r-m);
  background: transparent;
  color: var(--on-obsidian);
  text-decoration: none;
  white-space: nowrap;
  transition: border-color var(--d-fast) var(--e-out),
              box-shadow var(--d-base) var(--e-out);
}

.navlink:hover {
  border-color: var(--light);
  box-shadow: 0 0 24px -6px color-mix(in oklch, var(--light) 60%, transparent);
}

/* ============================================================
   CTA: the only button on the page
   ============================================================ */

.cta {
  display: inline-block;
  font-family: var(--font-num);
  font-size: var(--t-meta);
  font-variation-settings: "wght" 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: var(--s4) var(--s7);
  border: 1px solid var(--on-obsidian);
  border-radius: var(--r-m);
  background: var(--on-obsidian);
  color: var(--obsidian);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--d-fast) var(--e-out),
              color var(--d-fast) var(--e-out),
              border-color var(--d-fast) var(--e-out),
              box-shadow var(--d-base) var(--e-out),
              transform var(--d-fast) var(--e-out);
}

.cta:hover {
  background: transparent;
  color: var(--on-obsidian);
  border-color: var(--light);
  box-shadow: 0 0 32px -8px color-mix(in oklch, var(--light) 70%, transparent);
}

.cta:active { transform: translateY(1px); }

/* ============================================================
   Hero
   ============================================================ */

.hero {
  position: relative;
  min-height: 100dvh;
  display: grid;
  grid-template-columns: 7fr 5fr;
  align-items: center;
  gap: var(--col-gap);
  width: var(--max);
  margin-inline: auto;
  padding-block: var(--header-h) var(--s9);
}

/* Measures are set per element, never on the wrapper. A ch cap on
   the wrapper resolves against the body font and strangles the
   lede while the display type overflows it regardless. */
.hero__copy { grid-column: 1; }
.hero__copy .t-hero { max-width: 11ch; }
/* s7 not s6: the hero sits at line-height .92, so its descenders
   eat most of the gap a smaller step would leave. */
.hero__lede { margin-top: var(--s7); max-width: 38ch; }
.hero__actions { margin-top: var(--s7); display: flex; gap: var(--s5); align-items: center; }

.hero__stage {
  grid-column: 2;
  position: relative;
  justify-self: end;
  width: min(58vw, 820px);
  margin-right: calc(var(--gut) * -1);
}

.card-canvas {
  display: block;
  width: 100%;
  height: auto;
  /* No background. The alpha channel does the work. */
}

/* ============================================================
   Preload gate
   ============================================================ */

.preloader {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-content: center;
  gap: var(--s5);
  background: var(--obsidian);
  color: var(--on-obsidian-70);
  transition: opacity var(--d-slow) var(--e-out);
}

.preloader[hidden] { display: none; }
.preloader.is-done { opacity: 0; pointer-events: none; }

.preloader__bar {
  width: min(240px, 40vw);
  height: 1px;
  background: color-mix(in oklch, var(--facet-edge) 45%, transparent);
}

.preloader__fill {
  height: 100%;
  width: 0%;
  background: var(--light);
  box-shadow: 0 0 12px color-mix(in oklch, var(--light) 80%, transparent);
  transition: width var(--d-fast) linear;
}

/* ============================================================
   Destinations

   Each section overrides --light locally, so everything inside it
   (label, bloom, exit edge, sweep) carries that destination's own
   color regardless of which light is globally active. Without this
   the Tax label renders in whatever light the scroll happens to be
   in, which destroys the save/tax/spend mapping that the palette
   depends on for colorblind redundancy.
   ============================================================ */

/* Scoped to section: the root element also carries data-light,
   and matching it here would fight the JS-driven global light. */
section[data-light="save"]  { --light: var(--light-save); }
section[data-light="tax"]   { --light: var(--light-tax); }
section[data-light="spend"] { --light: var(--light-spend); }

.dest__label {
  font-family: var(--font-display);
  font-size: var(--t-hero);
  font-variation-settings: "opsz" 96, "wght" 700;
  line-height: 0.92;
  letter-spacing: -0.025em;
  color: var(--light);
  text-shadow: 0 0 64px color-mix(in oklch, var(--light) 40%, transparent);
  transition: color var(--d-slow) linear, text-shadow var(--d-slow) linear;
  margin: 0;
}

.dest__figure { margin-top: var(--s5); }
.dest__label-wrap { grid-column: 1 / 7; }

.dest__note {
  grid-column: 8 / -1;
  align-self: end;
  margin-top: var(--s9);
}

/* ============================================================
   Ledger: replaces the banned three-card feature row
   ============================================================ */

.ledger {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid color-mix(in oklch, var(--facet-edge) 40%, transparent);
}

.ledger__row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: var(--s6);
  align-items: baseline;
  padding-block: var(--s5);
  border-bottom: 1px solid color-mix(in oklch, var(--facet-edge) 40%, transparent);
}

.ledger__source { font-variation-settings: "opsz" 14, "wght" 400; color: var(--on-obsidian); }
.ledger__date { font-family: var(--font-num); font-size: var(--t-meta); color: var(--on-obsidian-70); }

.ledger__amount {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-variation-settings: "wght" 400;
  color: var(--on-obsidian);
}

/* ============================================================
   Waitlist form
   ============================================================ */

.account { display: grid; gap: var(--s2); max-width: 420px; }

.account__label {
  font-family: var(--font-num);
  font-size: var(--t-meta);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--on-obsidian-70);
}

.account__input {
  font-family: var(--font-text);
  font-size: var(--t-body);
  padding: var(--s4) var(--s5);
  border: 1px solid color-mix(in oklch, var(--facet-edge) 70%, transparent);
  border-radius: var(--r-s);
  background: color-mix(in oklch, var(--obsidian-2) 70%, transparent);
  color: var(--on-obsidian);
  transition: border-color var(--d-fast) var(--e-out),
              box-shadow var(--d-base) var(--e-out);
}

/* 0.78, not 0.6. At 0.6 the placeholder measured 4.11:1 against the
   ground and failed 1.4.3; at 0.78 it measures about 6.2:1 and is still
   clearly quieter than a real value, which sits at 17.3:1. */
.account__input::placeholder { color: var(--on-obsidian-70); opacity: 0.78; }

.account__input:focus {
  outline: none;
  border-color: var(--light);
  box-shadow: 0 0 28px -10px color-mix(in oklch, var(--light) 80%, transparent);
}

.account__error {
  font-family: var(--font-num);
  font-size: var(--t-meta);
  color: var(--light-tax);
  min-height: 1.2em;
}

.account__error:empty { visibility: hidden; }
.account .cta { margin-top: var(--s3); justify-self: start; }
.account[data-state="sending"] .cta { opacity: 0.6; pointer-events: none; }

/* ============================================================
   Footer
   ============================================================ */

.footer {
  padding-block: var(--s9);
  border-top: 1px solid color-mix(in oklch, var(--facet-edge) 30%, transparent);
}

.footer__fine { color: var(--on-obsidian-70); opacity: 0.8; max-width: 60ch; }

/* ============================================================
   Reveal
   ============================================================ */

.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--d-base) var(--e-out) var(--reveal-delay, 0ms),
              transform var(--d-base) var(--e-out) var(--reveal-delay, 0ms);
}

.reveal.is-in { opacity: 1; transform: none; }

/* ============================================================
   Mobile: every asymmetric structure collapses. No exceptions.
   ============================================================ */

@media (max-width: 767px) {
  .hero {
    grid-template-columns: 1fr;
    width: 100%;
    padding-inline: var(--gut);
    min-height: auto;
    padding-block: calc(var(--header-h) + var(--s8)) var(--s9);
  }

  .hero__copy, .hero__stage { grid-column: 1; max-width: none; }
  .hero__lede { margin-top: var(--s8); }

  /* The label is three words and must never break across lines. */
  .cta { padding: var(--s4) var(--s5); white-space: nowrap; }

  .hero__stage {
    width: 100%;
    margin-right: 0;
    justify-self: stretch;
    margin-top: var(--s8);
  }

  .grid-12 { grid-template-columns: 1fr; }
  .dest__label-wrap, .dest__note { grid-column: 1; }
  .dest__note { margin-top: var(--s7); }
  .shell { width: 100%; padding-inline: var(--gut); }
  .header__inner { width: 100%; padding-inline: var(--gut); }
  .ledger__row { grid-template-columns: 1fr auto; }
  .ledger__date { grid-column: 1; }

  .facet { border-radius: var(--r-l); padding-inline: var(--s6); }
  .facet--fractured { clip-path: none; }
}

/* ============================================================
   Reduced motion: the sequence never loads
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .reveal { opacity: 1; transform: none; }
  .facet::after { display: none; }
}

/* ============================================================
   Hero load choreography
   ============================================================ */

.hero__headline .word {
  display: inline-block;
  overflow: hidden;
  /* The clip box has to clear the descenders of a 176px face set at
     line-height .92, or "payment" and "already" lose their tails. */
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
  vertical-align: top;
}

.hero__headline .word > span {
  display: inline-block;
  transform: translateY(105%);
  transition: transform var(--d-enter) var(--e-out-q) var(--word-delay, 0ms);
}

.hero__headline.is-in .word > span { transform: none; }

/* The sphere arrives from off its resting point rather than being there
   when the gate lifts. Transform only: the layer is already promoted. */
.caustics__sphere { transition: none; }
.caustics.is-arriving .caustics__sphere { transition: transform var(--d-slow) var(--e-out); }

@media (prefers-reduced-motion: reduce) {
  .hero__headline .word > span { transform: none; transition: none; }
  .caustics.is-arriving .caustics__sphere { transition: none; }
}
