/* Asiko landing page.
 *
 * Colour, the type ramp and motion durations come from tokens.css, which is GENERATED from
 * src/theme/tokens.ts — see scripts/build-web-tokens.mjs. Nothing in this file may declare
 * a hex value; if a colour is needed that the app does not have, it gets added to the app's
 * tokens first. That rule is what keeps a screenshot of the site and a screenshot of the app
 * looking like the same product.
 *
 * Spacing and radii ARE declared here, deliberately: the app's are phone-scale and a 14px
 * card radius is a rounded tile at 390pt and a barely-bent corner at 1240px.
 *
 * The legal pages (/privacy, /terms) still use style.css and are untouched by this file.
 * They are the App Store's mandatory URLs and there was no reason to put them at risk.
 *
 * Decisions, measurements and open items for this page: docs/landing-page.md (kept out of
 * site/ because everything in site/ is publicly served).
 */

:root {
  --font: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  /* Web-scale geometry. */
  --wrap: 1180px;
  --r-panel: 32px; /* measured off the inspiration's hero panel */
  --r-card: 20px;
  --r-pill: 999px;

  --gap-section: clamp(72px, 9vw, 128px);

  /* Muted chrome — the nav links and the hero's "Asiko App" caption. A semi-transparent
   * accent rather than a grey: it tints whatever is behind it, so it stays related to the
   * gradient at the top of the page and to plain paper further down. */
  --chrome-muted: color-mix(in srgb, var(--accent) 62%, transparent);

  /* Elevation, mixed from the app's own ink rather than from black — a neutral-grey shadow
   * under an indigo-tinted page reads as dirt. */
  --shadow-card: 0 16px 44px color-mix(in srgb, var(--text-primary) 8%, transparent);
  --shadow-phone: 0 28px 64px color-mix(in srgb, var(--text-primary) 18%, transparent);
  --shadow-notif: 0 14px 34px color-mix(in srgb, var(--text-primary) 15%, transparent);
}

/* ─── base ────────────────────────────────────────────────────────────────────────── */

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

html {
  scroll-behavior: smooth;
  /* The sticky nav would otherwise cover the heading of whatever a jump link lands on. */
  scroll-padding-top: 96px;
}

/* THE HIDDEN ATTRIBUTE NEEDS DEFENDING. `hidden` only works through a UA rule of
 * `display: none`, so any class that sets its own display silently beats it — `.btn` is
 * inline-flex, `.wl-close` is grid, and both stayed on screen after being set hidden while
 * a plain input beside them vanished correctly. That is a confusing bug to chase, so the
 * attribute is made to mean what it says once, here. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  color: var(--text-secondary);

  /* THE HERO GRADIENT. Lives here, not on .hero, so it runs up behind the sticky nav —
   * the inspiration's nav floats on the tint rather than sitting in a white band above it.
   *
   * BUILT FROM --accent-soft, NOT --accent, and that is the whole reason the first version
   * was invisible. Asiko's accent is a dark navy (#28397E); mixed into white at any
   * percentage low enough to stay subtle it drops all three channels almost evenly and
   * comes out grey, not blue. --accent-soft (#DDE4F7) is the palette's own light indigo:
   * its blue channel sits 26 points above its red, so it reads as a colour rather than as
   * a dirty white at the same lightness. Measured against the inspiration's own top stop,
   * this is a slightly stronger tint with more chroma — deliberately, since the brief was
   * that it should be obvious at a glance.
   *
   * Two layers: a bloom centred above the headline that gives the top the lit quality the
   * inspiration gets, and the four-stop wash whose stop positions are theirs. The final
   * stop equals the background-color, so the band's end is invisible at any viewport
   * height — it can overshoot or undershoot the hero without ever showing a seam. */
  background-color: var(--bg-page);
  background-image:
    radial-gradient(
      130% 62% at 50% -8%,
      color-mix(in srgb, var(--accent) 13%, transparent) 0%,
      transparent 62%
    ),
    linear-gradient(
      180deg,
      var(--accent-soft) 0%,
      color-mix(in srgb, var(--accent-soft) 74%, var(--bg-page)) 20%,
      color-mix(in srgb, var(--accent-soft) 28%, var(--bg-page)) 58%,
      var(--bg-page) 100%
    );
  background-repeat: no-repeat;
  background-size:
    100% 1180px,
    100% 1420px;
  font: 400 17px/1.6 var(--font);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

.wrap {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 24px;
}

h1,
h2,
h3 {
  color: var(--text-primary);
  font-weight: 600;
  margin: 0;
  letter-spacing: -0.02em;
}

/* Display sizes are set here rather than taken from the token ramp: the ramp's top step is
 * 32px, which is a phone headline and a subhead on a 1180px page. The RATIOS are the app's;
 * the absolute values are the web's. See the note at the top of tokens.css. */
h1 {
  font-size: clamp(40px, 5.6vw, 68px);
  line-height: 1.08;
  letter-spacing: -0.03em;
}
h2 {
  font-size: clamp(30px, 3.6vw, 44px);
  line-height: 1.16;
}
h3 {
  font-size: clamp(26px, 3vw, 36px);
  line-height: 1.2;
}

/* Every figure on this page is a rate, an amount or a date. IBM Plex Sans ships tabular
 * digits by design (R-RAT-05); declared anyway so a font swap cannot break alignment. */
.num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum';
}

a {
  color: var(--accent);
  text-decoration: none;
}

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: 6px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--text-primary);
  color: var(--accent-on-fill);
  padding: 12px 20px;
  border-radius: 0 0 10px 0;
  font-weight: 500;
}
.skip:focus {
  left: 0;
  z-index: 100;
}

.eyebrow {
  font-weight: 500;
  font-size: var(--type-body-sm-size);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 16px;
}

.lead {
  font-size: clamp(16px, 1.5vw, 18.5px);
  line-height: 1.6;
  max-width: 480px;
}

/* ─── buttons ─────────────────────────────────────────────────────────────────────── */

/* Geometry measured off the inspiration's own buttons rather than guessed: 48px tall,
 * 0/24px padding, 17px at weight 500, a 99px pill, 8px between icon and label, 16px between
 * the two buttons. Mine were 56 tall at 30px padding and weight 600 — heavier and chunkier
 * in a way that read as a different design language next to everything else on the page.
 * The height comes from an explicit 48px, not from vertical padding, so a one-line label
 * and an icon-plus-label button are exactly the same height. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: inherit;
  font-weight: 500;
  font-size: 17px;
  line-height: 1;
  height: 48px;
  padding: 0 24px;
  border: 0;
  border-radius: var(--r-pill);
  cursor: pointer;
  white-space: nowrap;
  transition:
    background-color var(--motion-fast) ease,
    color var(--motion-fast) ease,
    transform var(--motion-fast) ease,
    box-shadow var(--motion-base) ease-out;
}
.btn:hover {
  transform: translateY(-1px);
}
.btn:active {
  transform: translateY(0) scale(0.985);
}

/* Icons inherit the label's colour, so each button's glyphs are automatically right —
 * white on the ochre fill, indigo on the tinted one. That is the whole trick; there is no
 * per-icon colour anywhere. */
.btn-ico {
  flex: 0 0 auto;
}
.btn-icos {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* OCHRE APPEARS ONCE ON THIS PAGE. The tokens doc reserves it for "your moment has come" —
 * a target rate hit, or the best provider today — and forbids it on buttons. The primary
 * call to action is the page's one moment, so it takes the colour and nothing else on the
 * page may. The closing CTA is deliberately indigo for that reason. */
.btn-moment {
  background: var(--moment-fill);
  color: var(--accent-on-fill);
  box-shadow: 0 8px 22px color-mix(in srgb, var(--moment-fill) 28%, transparent);
}
.btn-moment:hover {
  background: var(--moment-hover);
}

/* The secondary, built the way the inspiration builds its own: the SAME hue as its partner
 * at a tenth opacity, with the label in that hue at full strength. No border. It sits on
 * the hero gradient and tints whatever is behind it rather than punching a white hole in
 * it, which is what the previous bordered-white version did. Indigo, not ochre — two ochre
 * buttons would spend the reserved colour twice in one row. */
.btn-soft {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent);
}
.btn-soft:hover {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

.btn-brand {
  background: var(--accent);
  color: var(--accent-on-fill);
}
.btn-brand:hover {
  background: var(--text-primary);
}

.btn-quiet {
  background: var(--bg-card);
  color: var(--text-primary);
  box-shadow: inset 0 0 0 1px var(--border-hairline);
}
.btn-quiet:hover {
  box-shadow: inset 0 0 0 1px var(--border-strong);
}

/* ─── nav ─────────────────────────────────────────────────────────────────────────── */

/* THE FROST IS ON THE OUTER ELEMENT, which spans the viewport; the inner row is what gets
 * constrained to the wrap. Both were sticky before and the frosted background sat on the
 * INNER one, so the glass stopped at 1180px and left bare page either side of it on a wide
 * screen. Only one of these may be sticky, and it has to be the full-width one. */
.nav-outer {
  position: sticky;
  top: 0;
  z-index: 50;
  transition:
    background-color var(--motion-base) ease,
    box-shadow var(--motion-base) ease;
}
.nav-outer.stuck {
  background: color-mix(in srgb, var(--bg-page) 78%, transparent);
  backdrop-filter: saturate(1.7) blur(16px);
  -webkit-backdrop-filter: saturate(1.7) blur(16px);
  box-shadow: 0 1px 0 var(--border-hairline);
}

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 13px 24px;
  max-width: var(--wrap);
  margin: 0 auto;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  /* No gap: the mark IS the first letter of the word, so a space between them reads as an
   * icon sitting next to a name rather than as one lockup. The 2px optical nudge accounts
   * for the stem's own right-hand bearing inside its square viewBox. */
  gap: 2px;
  font-weight: 600;
  font-size: 23px;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}
/* Sized to the cap height of the word beside it rather than to its line box, so the mark
 * reads as a letter in the same word rather than as a badge stuck on the front. */
.wordmark-mark {
  flex: 0 0 auto;
  width: 21px;
  height: 21px;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 28px;
}
.nav-links a {
  /* The same value the hero's "Asiko App" caption uses, so every muted piece of chrome on
   * this page is one colour rather than three that nearly match. Declared once below and
   * referenced in both places. */
  color: var(--chrome-muted);
  font-weight: 500;
  font-size: 14.5px;
  transition: color var(--motion-fast) ease;
}
.nav-links a:hover {
  color: var(--text-primary);
}
.nav-cta {
  background: var(--text-primary);
  color: var(--accent-on-fill) !important;
  padding: 8px 15px;
  border-radius: var(--r-pill);
  font-size: 14px;
}
.nav-cta:hover {
  background: var(--accent);
}
@media (max-width: 760px) {
  .nav-links a:not(.nav-cta) {
    display: none;
  }
}

/* ─── hero ────────────────────────────────────────────────────────────────────────── */

/* THE GRADIENT. Four stops, brand tint at the top washing to the page colour at the bottom,
 * on the SECTION rather than on a rounded panel — that is what makes it read as light in
 * the room rather than as a coloured box. Stop positions are the inspiration's; the colour
 * is Asiko's accent mixed into the page, so it cannot drift from the app's indigo. */
.hero {
  position: relative;
  text-align: center;
  padding: 40px 24px 0;
}

.hero-inner {
  max-width: var(--wrap);
  margin: 0 auto;
}

/* Measured off the inspiration: a 28px icon, a 20px label, 12px between them. The first
 * version was exactly double that — a 56px icon and 27px text — which turned a quiet
 * "this is the app" credit into a second headline competing with the real one directly
 * beneath it. It is a caption, and it should read like one. */
.app-lockup {
  display: inline-flex;
  align-items: center;
  gap: 12px;
}
.app-lockup img {
  width: 28px;
  height: 28px;
  /* ~22.5% of the size, which is the iOS icon mask's own ratio. The source is a square
   * bitmap, so the rounding has to come from here. No shadow: under a 28px tile it reads
   * as smudge rather than lift. */
  border-radius: 7px;
}
/* A lighter blue than the headline, and semi-transparent so it sits INTO the gradient
 * rather than on top of it. The accent at full strength made it compete with the "you". */
.app-name {
  font-weight: 500;
  font-size: 20px;
  color: var(--chrome-muted);
  letter-spacing: -0.01em;
}

.hero h1 {
  margin: 20px auto 18px;
  max-width: 22ch;
}
/* The rate in the headline, in the app's own indigo — the one place a figure is allowed to
 * carry colour, because it is the thing the whole product is about. */
.hero h1 .rate {
  color: var(--accent);
}

.hero-sub {
  font-size: clamp(16.5px, 1.9vw, 20px);
  line-height: 1.55;
  max-width: 570px;
  margin: 0 auto;
}

.cta-row {
  margin-top: 34px;
  display: flex;
  gap: 16px;
  justify-content: center;
  flex-wrap: wrap;
}
.fine {
  font-size: 13px;
  color: var(--text-muted);
  margin-top: 20px;
}

/* ─── the phone tray ──────────────────────────────────────────────────────────────── */

/* Geometry measured off the inspiration and held as RATIOS so it survives a width change:
 *   phone width      32% of the tray
 *   centre spacing   96% of a phone width  -> the phones overlap by about a twentieth
 *
 * THE OVERLAP IS BOUNDED BY THE SCREENSHOTS' OWN MARGINS. The side phones are each covered
 * on one edge by the centre one, and the app's screen titles start about 5% in — so an
 * overlap above that eats them. At the previous 11% the Alerts screen read as "...lerts",
 * which looks like a rendering fault rather than depth. 4.6% clears every title while
 * still reading as layered, and narrowing the phones from 335 to 322 buys that without
 * squeezing the tray margins down to a sliver.
 *
 * The inspiration overlaps by a fifth, which works there because its side phones are photos
 * that read fine half-covered. Ours are live markup with left-aligned labels, and at that
 * spacing the centre phone ate the Ledger heading and the start of every figure on the
 * right — "Ledger" rendered as "er". Opened up until the side content survives; still
 * clearly layered, and the phones still sit inside the tray's 32px corner radius.
 *   centre phone     7.6% taller, and sits higher by the difference
 *
 * THE LAYERED LOOK COMES FROM THE SIZE DIFFERENCE, NOT THE OVERLAP. That is the part worth
 * remembering, because the obvious move — slide the phones together until the centre one
 * clearly sits on top — is the one that destroys content. Measured on the real page:
 *
 *   spacing 296px   12.1% phone overlap   8.3% of the side SCREEN covered   clips headings
 *   spacing 308px    8.4% phone overlap   4.2% of the side SCREEN covered   safe
 *
 * (The image is the whole device, so 4.61% a side is bezel before any screen is touched.
 * Our screen headings start about 5% in, which is the line those numbers are judged against.)
 *
 * Both read as layered, because what makes the centre phone look in front is that it is 8%
 * bigger and breaks the top line of the row — the eye reads a stack rather than three tiles.
 * So the wider spacing costs nothing visually and keeps every side label intact.
 * All three run past the bottom edge and are clipped by the tray, which is what stops the
 * hero ending in a row of floating rectangles. */
.tray {
  position: relative;
  margin: 74px auto 0;
  max-width: 1000px;
  /* 672px, and the odd number is the point — it is the ONLY band that cuts both phones
   * through empty space. Measured off the shipped images rather than judged by eye:
   *
   *   left (USD·Parallel)   the fair-price block ends 577px down the phone. The cut is
   *                         `height - 92`, so anything below 669px starts eating the band —
   *                         which is why this went from 600 to 690 earlier in the day.
   *   centre (Rates)        the tab-bar icons sit 644-670px down, with clear white from 601.
   *                         The cut is `height - 34`, so 675px or less lands in that gap and
   *                         hides the bar cleanly. At 690 the cut fell at 656 — straight
   *                         through the icons, which is what read as broken.
   *
   * 669-675 is the entire overlap. 672 sits in the middle, a few pixels either side.
   *
   * Anything that moves the content invalidates this: the two `top` offsets, the centre
   * phone's width, or re-shooting either screen at a different scroll position. Re-measure
   * rather than nudging — both cuts should land on a row with no content in it. */
  height: 672px;
  /* Rounded on all four corners, like the inspiration's: with square bottom corners the
   * tray's tint ended in a hard horizontal rule across the page and read as a band that
   * stopped, rather than as a tray the phones sit in. */
  border-radius: var(--r-panel);
  overflow: hidden;
  /* Reverse of the hero gradient: the tray gets lighter at the top so it lifts off the
   * wash instead of merging with it. */
  background: linear-gradient(
    180deg,
    var(--bg-card) 0%,
    color-mix(in srgb, var(--accent-soft) 40%, var(--bg-card)) 100%
  );
}

/* THE PHONE IS ONE IMAGE now — a captured iOS Simulator window, device bezel and side
 * buttons included, transparent outside the rounded body. See scripts/build-shots.mjs.
 *
 * This replaced a CSS-drawn frame. The CSS version was cheaper and vector-sharp, but a
 * gradient-edged rectangle with box-shadow buttons reads as a *stylised* phone; the founder
 * wanted a literal Apple device, and there is no arrangement of CSS that convincingly
 * renders machined titanium, a camera plateau and a Dynamic Island. Where the frame is the
 * subject rather than a container, a real render wins.
 *
 * So `.phone` is now only a position: everything visible comes from the image. */
.phone {
  position: absolute;
  left: 50%;
  width: 322px;
}
/* <picture> is an inline element, so without this the <img> inside it would size against a
 * shrink-to-fit box rather than .phone's 322px. */
.phone picture {
  display: block;
}
.phone img {
  display: block;
  width: 100%;
  height: auto;
  /* drop-shadow, not box-shadow: the image's corners are transparent, and box-shadow would
   * draw the shadow of its RECTANGULAR box — a hard-edged rectangle behind a rounded phone.
   * drop-shadow follows the alpha channel. */
  filter: drop-shadow(0 28px 44px color-mix(in srgb, var(--text-primary) 26%, transparent));
}
.phone-left {
  transform: translateX(calc(-50% - 308px));
  top: 92px;
  z-index: 1;
}
.phone-right {
  transform: translateX(calc(-50% + 308px));
  top: 92px;
  z-index: 1;
}
/* Bigger, not just higher. The inspiration's centre phone is 7.6% taller than its
 * neighbours, and that — more than the overlap — is what makes it read as being in front:
 * it breaks the top line of the row, so the eye sees a stack rather than three tiles. */
.phone-main {
  width: 348px;
  transform: translateX(-50%);
  top: 34px;
  z-index: 2;
}

@media (max-width: 1040px) {
  .tray {
    height: 520px;
  }
  /* .phone-main is named explicitly. It carries its own width for the desktop stack, and
   * with equal specificity it would only lose to a bare `.phone` rule because that rule
   * happens to come later in the file — which is true today and one reorder away from the
   * centre phone staying 348px on a 390px screen. */
  .phone,
  .phone-main {
    width: 290px;
  }
  .phone-left,
  .phone-right {
    display: none;
  }
}
@media (max-width: 560px) {
  .tray {
    height: 440px;
    border-radius: 24px;
  }
  .phone,
  .phone-main {
    width: 250px;
  }
}

/* Survives the screenshot swap: still used by the "your $500" line in the alerts-maths
 * card. The rest of the .p-* set drew the hand-built phone interiors and went with them. */
.p-mut {
  font-size: 11px;
  color: var(--text-muted);
}

/* the push notification, arriving over the tray's top-RIGHT corner
 *
 * Moved from the left 31 Aug 2026. It now sits above the third phone — the alert screen —
 * so the push and the screen it came from are the same thought, and the first phone is left
 * unobstructed. `right` rather than a computed `left` so it mirrors the old placement
 * exactly rather than approximating it. */
.notif {
  position: absolute;
  z-index: 4;
  width: 262px;
  /* Clear of the phone's top edge (phones start at 92px), not landing on it. It reads as a
   * push arriving OVER the tray; overlapping the phone chrome made it read as a mis-stacked
   * card instead. `max()` keeps it on screen when the tray is narrower than its own offset. */
  right: max(16px, calc(50% - 476px));
  top: 8px;
  background: var(--bg-card);
  border-radius: 16px;
  padding: 12px 15px;
  box-shadow: var(--shadow-notif);
  text-align: left;
}
@media (max-width: 1040px) {
  .notif {
    right: 16px;
    top: 16px;
    width: 232px;
  }
}
/* The push arrives a beat after the page settles — the product's whole promise in one
 * gesture. A CSS animation rather than a JS timer, deliberately: the first version hid it
 * with JS and revealed it from inside requestAnimationFrame, which never fires in a
 * backgrounded tab — so opening the page in a new tab and switching to it later left the
 * notification permanently invisible. `both` holds the start state through the delay, and
 * with no JS (or none that ran) the element is simply already there, which is a correct
 * picture of the app rather than an empty corner.
 *
 * TIMING — the two numbers do opposite jobs, so they were tuned separately.
 *
 * DELAY was 800ms and read as lag rather than a beat: long enough that the eye had finished
 * with the tray and moved on, so the arrival landed after the moment it was meant to
 * punctuate. 400ms still separates it from the page load, which is the only reason to have a
 * delay at all, without making anyone wait for it.
 *
 * DURATION is 620ms, written out rather than using --motion-slow (320ms). A push does not
 * snap into place; it slides and settles, and at 320ms the overshoot in the curve below was
 * finished before it registered as movement. It stays a literal because this is a one-off
 * gesture — raising --motion-slow to suit it would slow every transition on the page too.
 *
 * DELAY IS TIED TO THE HERO ENTRANCE and is no longer free to choose. This element lives
 * INSIDE `.tray`, which now fades in at 450ms over 620ms and therefore settles at 1070ms. At
 * the previous 400ms the push slid in through a parent that was still fading, so it arrived
 * dim and muddled into the tray's own motion instead of reading as a separate event.
 *
 * 1020ms starts it just as the tray lands, making it the last beat of the sequence rather
 * than a competing one. If `.hero-inner > .tray`'s delay or duration changes, this has to
 * move with it. */
.notif {
  animation: notif-in 620ms cubic-bezier(0.2, 0.9, 0.3, 1.25) 1020ms both;
}
@keyframes notif-in {
  from {
    opacity: 0;
    transform: translateY(-16px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.n-app {
  font-size: 9.5px;
  color: var(--text-muted);
  letter-spacing: 0.07em;
  margin-bottom: 3px;
}
.n-head {
  font-weight: 600;
  font-size: 13.5px;
  color: var(--text-primary);
}
.n-sub {
  font-size: 11px;
  color: var(--text-muted);
  margin-top: 1px;
}

/* ─── sections ────────────────────────────────────────────────────────────────────── */

.sec {
  padding-top: var(--gap-section);
}
.split {
  display: flex;
  gap: clamp(40px, 5vw, 72px);
  align-items: center;
  flex-wrap: wrap;
}
.split > * {
  flex: 1 1 420px;
  min-width: 300px;
}

/* Stacked layout: the words go first.
 *
 * The sections alternate on desktop — Compare and Ledger put text on the left, Alerts puts
 * the card there — which is the rhythm that stops three feature rows reading as a list. Once
 * the columns stack, that alternation turns into a defect: Alerts is the one section where a
 * visitor meets a card of numbers before being told what the numbers are for.
 *
 * `order` rather than reordering the markup, because the DOM sequence is the DESKTOP one and
 * also the reading order for a screen reader, where the heading should precede its card
 * either way.
 *
 * 940px, not a round 900 or a device width. `.split` children are `flex: 1 1 420px` inside a
 * 48px-padded wrapper with a 5vw gap, so they wrap when the viewport drops below ~935px —
 * measured, not assumed: 936px stacks, 1000px does not. The query has to fire only once they
 * are stacked; at any wider width it would swap the columns left-to-right on the desktop
 * layout, which is not the ask. If the gap, the flex-basis or `--wrap` changes, re-measure. */
@media (max-width: 940px) {
  /* Once stacked, the column gap becomes VERTICAL space between a heading and the card that
   * illustrates it — a different job from separating two columns, and 40px was doing the
   * wrong one.
   *
   * The giveaway was the ratio, not the number. Sections are separated by 72px of padding,
   * and the measured gap from the last line of text to its own card was 56px: 78% of it. At
   * that point the page has no spacing hierarchy, so a card reads as a new thing rather than
   * as belonging to the words above it. 28px against 72px is 2.6:1, which is legible as
   * "these two are one unit".
   *
   * Note the measured 56 rather than 40 — see the margin rule below for where the extra
   * 16px came from. */
  .split {
    gap: 28px;
  }
  /* `.lead` carries `margin-bottom: 16px`, which is right mid-column and pure surplus when it
   * is the LAST thing in a stacked block: it simply adds to the flex gap. Trailing margins on
   * a final child are invisible on desktop and always wrong once things stack. */
  .split > * > :last-child {
    margin-bottom: 0;
  }
  #alerts .split > :first-child {
    order: 2;
  }
  #alerts .split > :last-child {
    order: 1;
  }
}

.card {
  background: var(--bg-card);
  border-radius: var(--r-card);
  padding: 30px 32px;
  box-shadow: var(--shadow-card);
}

/* provider bars */
.bar-caption {
  display: flex;
  justify-content: space-between;
  font-size: 12.5px;
  color: var(--text-muted);
  margin-bottom: 26px;
}
.bar-row {
  margin-bottom: 22px;
}
.bar-row:last-of-type {
  margin-bottom: 6px;
}
.bar-top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 8px;
  gap: 12px;
}
.bar-name {
  font-weight: 500;
  font-size: 15px;
  color: var(--text-primary);
}
.bar-name.plain {
  font-weight: 400;
}
/* "best delivered" is the one place ochre is allowed besides the CTA — it is literally the
 * best provider today, which is the reserved meaning. */
.tag {
  display: inline-block;
  background: var(--moment-bg);
  color: var(--moment-text);
  font-weight: 600;
  font-size: 10.5px;
  border-radius: 8px;
  padding: 3px 8px;
  margin-left: 8px;
  vertical-align: 1px;
}
.bar-note {
  font-size: 11.5px;
  color: var(--text-muted);
  font-weight: 400;
  margin-left: 8px;
}
.bar-amt {
  font-weight: 600;
  font-size: 15.5px;
  color: var(--text-primary);
  white-space: nowrap;
}
.bar-track {
  background: var(--bg-sunken);
  border-radius: 9px;
  height: 16px;
  overflow: hidden;
}
.bar-fill {
  background: var(--accent);
  border-radius: 9px;
  height: 100%;
  width: 0;
  transition: width 1s cubic-bezier(0.33, 1, 0.68, 1);
}
.bar-fill.quiet {
  background: var(--border-strong);
}
.bar-delta {
  text-align: right;
  font-size: 11.5px;
  color: var(--text-muted);
  margin-top: 5px;
}
.bar-foot {
  font-size: 12.5px;
  color: var(--text-muted);
  margin: 26px 0 0;
}

/* waiting math */
.w-line {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border-hairline);
  font-size: 15px;
}
.w-line:last-of-type {
  border-bottom: none;
}
.w-amt {
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
}
.w-gain {
  background: var(--bg-sunken);
  border-radius: 16px;
  padding: 22px 24px;
  margin-top: 18px;
}
.w-big {
  font-weight: 600;
  font-size: clamp(34px, 4vw, 46px);
  color: var(--text-primary);
  line-height: 1.1;
  letter-spacing: -0.02em;
}
.w-note {
  font-size: 12.5px;
  color: var(--text-muted);
  margin-top: 6px;
}

/* waitlist
 *
 * Revealed by either hero CTA; hidden until then. Sits inline under the buttons rather than
 * in a modal — a page asking people to trust it with money decisions should not throw a
 * dialog at them, and every hop between "interested" and "submitted" costs signups. */
.wl {
  max-width: 460px;
  margin: 22px auto 0;
  text-align: left;
}
/* In the row beside Join, not floating in a corner. Sized to the input's height so the three
 * controls share one baseline. */
.wl-close {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 34px;
  align-self: stretch;
  padding: 0;
  border: 0;
  border-radius: 12px;
  cursor: pointer;
  color: var(--text-muted);
  background: transparent;
}
.wl-close:hover {
  color: var(--text-primary);
  background: var(--bg-sunken);
}
.wl-close:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.wl-says {
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-muted);
  margin: 0 0 12px;
}
/* Visible to screen readers, not on screen. A placeholder is not a label: it disappears the
 * moment someone types, which leaves the field unnamed for exactly the people who need it
 * named. */
.wl-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.wl-row {
  display: flex;
  gap: 10px;
}
.wl-input {
  flex: 1 1 auto;
  min-width: 0;
  font: inherit;
  font-size: 15px;
  padding: 12px 14px;
  color: var(--text-primary);
  background: var(--bg-card);
  border: 1px solid var(--border-hairline);
  border-radius: 12px;
}
.wl-input::placeholder {
  color: var(--text-muted);
}
.wl-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.wl-submit {
  flex: 0 0 auto;
}
.wl-note {
  font-size: 13px;
  color: var(--text-muted);
  margin: 10px 0 0;
  /* Reserve the line so the panel does not jump when a message lands under the field. */
  min-height: 1.2em;
}
.wl-note a {
  color: var(--accent);
}

/* personal ledger
 *
 * Same construction as the waiting-math card above: hairline-separated rows, tabular
 * figures, amount right-aligned. No new visual language for a third card — three cards that
 * each invent their own rhythm read as three designs, not one product. */
.led-head {
  padding-bottom: 20px;
  border-bottom: 1px solid var(--border-hairline);
}
.led-month {
  font-size: 12.5px;
  color: var(--text-muted);
  margin: 0 0 7px;
}
.led-total {
  font-weight: 600;
  font-size: clamp(21px, 2.2vw, 26px);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--text-primary);
}
.led-avg {
  font-size: 12.5px;
  color: var(--text-muted);
  margin: 7px 0 0;
}
.led-row {
  display: flex;
  justify-content: space-between;
  /* baseline, not centre: the left column is two stacked lines and the right is one, so
   * centring floats the amount between them instead of aligning it to the name it belongs
   * to. */
  align-items: baseline;
  gap: 16px;
  padding: 16px 0;
  border-bottom: 1px solid var(--border-hairline);
}
.led-row:last-of-type {
  border-bottom: none;
}
.led-what {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.led-name {
  font-size: 15px;
  color: var(--text-primary);
}
.led-meta {
  font-size: 12.5px;
  color: var(--text-muted);
}
.led-amt {
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
}
.led-more {
  font-size: 12.5px;
  color: var(--text-muted);
  margin: 2px 0 0;
}

/* feature grid */
.grid {
  display: grid;
  /* Two up, not three: there are four cards, and auto-fit at 290px gave three columns and
   * a lone orphan on the second row. min() keeps it single-column on narrow screens. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
  gap: 20px;
  margin-top: 38px;
}
.g-card {
  background: var(--bg-sunken);
  border-radius: var(--r-card);
  padding: 30px 32px;
  transition: transform var(--motion-base) cubic-bezier(0.33, 1, 0.68, 1);
}
.g-card:hover {
  transform: translateY(-3px);
}
/* The four marks are inline SVG, stroked in currentColor, so they inherit the accent here
 * and re-theme with the palette rather than needing a second set of files for dark mode. */
.g-key {
  color: var(--accent);
  margin-bottom: 14px;
  line-height: 0;
}
.g-key svg {
  display: block;
}
.g-title {
  font-weight: 600;
  font-size: 20px;
  color: var(--text-primary);
  margin-bottom: 8px;
  letter-spacing: -0.01em;
}
.g-body {
  font-size: 15px;
  line-height: 1.6;
  margin: 0;
}

/* stats */
.stats {
  display: flex;
  justify-content: center;
  gap: clamp(32px, 6vw, 90px);
  flex-wrap: wrap;
  text-align: center;
  padding-top: var(--gap-section);
}
.stat-v {
  font-weight: 600;
  font-size: clamp(30px, 3.4vw, 40px);
  color: var(--text-primary);
  letter-spacing: -0.02em;
}
.stat-l {
  font-size: 14.5px;
  color: var(--text-muted);
  margin-top: 6px;
  max-width: 260px;
}

/* steps */
.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 28px;
  margin-top: 38px;
}
.step-num {
  width: 34px;
  height: 34px;
  background: var(--accent);
  color: var(--accent-on-fill);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 15px;
  margin-bottom: 16px;
}
.step-t {
  font-weight: 600;
  font-size: 19px;
  color: var(--text-primary);
  margin-bottom: 8px;
}
.step-b {
  font-size: 14.5px;
  line-height: 1.6;
  margin: 0;
}

/* faq */
.faq details {
  border-bottom: 1px solid var(--border-hairline);
}
.faq summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
  padding: 24px 4px 24px 0;
  font-weight: 500;
  font-size: 18px;
  color: var(--text-primary);
}
.faq summary::-webkit-details-marker {
  display: none;
}
.faq summary::after {
  content: '+';
  font-size: 24px;
  font-weight: 400;
  color: var(--text-muted);
  transition: transform var(--motion-base) cubic-bezier(0.33, 1, 0.68, 1);
  flex: 0 0 auto;
}
.faq details[open] summary::after {
  transform: rotate(45deg);
}
.faq .answer {
  font-size: 15.5px;
  line-height: 1.65;
  max-width: 800px;
  padding: 0 0 24px;
  margin: 0;
}

/* closing */
.close {
  text-align: center;
  padding: clamp(56px, 7vw, 84px) 24px;
  border-radius: var(--r-panel);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--accent-soft) 78%, var(--bg-page)) 0%,
    color-mix(in srgb, var(--accent-soft) 34%, var(--bg-page)) 100%
  );
}
.close-sub {
  font-size: 17px;
  margin-top: 14px;
}

/* footer */
.footer {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  padding: 44px 24px 52px;
  max-width: var(--wrap);
  margin: 0 auto;
  font-size: 13.5px;
  color: var(--text-muted);
}
/* Spacing lives on the row, not on each link.
 *
 * `margin-left: 18px` on every `a` also indents the FIRST one, so the links row started 18px
 * further in than the line above it — invisible while the row is pushed to the right edge on
 * desktop, obvious the moment the footer stacks and the two rows should share an edge. */
.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
}
.footer a {
  color: var(--text-muted);
  /* Underlined, unlike every other link on the page — the base `a` rule strips decoration
   * because most links here are buttons or sit inside prose that already marks them. These
   * three are bare words in a row of muted grey with nothing else to say they are clickable,
   * and two of them are the store-required Privacy and Terms routes, so they should look like
   * links without needing a hover to prove it.
   *
   * The line is drawn at 45% of the text colour rather than full strength: at 13.5px a solid
   * underline crowds the descenders in "Privacy" and reads heavier than the text above it. */
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  transition:
    color var(--motion-fast) ease,
    text-decoration-color var(--motion-fast) ease;
}
.footer a:hover {
  color: var(--text-primary);
  /* currentColor, so the underline firms up to match the darkening text. */
  text-decoration-color: currentColor;
}
.footer .brand {
  font-weight: 500;
  color: var(--text-secondary);
}
/* Second line of the legal block, under the disclaimer rather than beside it, so the widest
 * line — and so the 600px wrap point measured below — is unchanged. */
.footer-maker {
  margin-top: 6px;
}

/* Stacked footer: centre it.
 *
 * `justify-content: space-between` does nothing useful once the rows wrap — each ends up
 * alone on its line at flex-start, so the legal line ran the full width while the links
 * stopped 120px short of it, leaving a ragged edge that reads as broken rather than aligned.
 *
 * 600px, measured rather than guessed: the footer sits on one line at 590 and wraps by 575.
 * Erring slightly HIGH is deliberate — between the true wrap point and 600 the only effect is
 * two rows centred as a group instead of pushed to the edges, which is mild. Erring low would
 * leave a band of widths stacked AND left-aligned, which is the fault being fixed. Re-measure
 * if the legal line's wording changes length. */
@media (max-width: 600px) {
  .footer {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 16px;
  }
  .footer-links {
    justify-content: center;
  }
}


/* ─── the bloom canvas ────────────────────────────────────────────────────────────── */

/* Geometry only; bloom.js draws and sizes it. Empty on touch, on reduced motion, and
   without WebGL2 — the page is simply flat there, by decision. */
.bloom {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 0;
  display: block;
  pointer-events: none;
  z-index: 0;
}
/* The bloom canvas is positioned with z-index 0, and a positioned element paints above
 * NON-positioned block content — so the page's own sections have to be positioned too, or
 * the canvas covers them.
 *
 * `.nav-outer` is deliberately NOT in this list. It already carries `position: sticky` from
 * the nav section above, and including it here re-declared `position: relative` further
 * down the file, which won on cascade order and silently killed the sticky header: the nav
 * scrolled away with the page and the frosted state never appeared. Its z-index is set
 * where it is defined. */
main,
footer {
  position: relative;
  z-index: 1;
}

/* ─── motion ──────────────────────────────────────────────────────────────────────── */

/* Sections settle in as they enter. Kept to one property pair and one easing so it reads as
 * the page settling rather than as an effect.
 *
 * An earlier note here claimed the inspiration "does NOT fade its sections in on scroll — it
 * has no such transition anywhere on the page." That was measured badly and is wrong. Their
 * section HEADINGS reveal word by word, and the reveal is scroll-LINKED rather than triggered:
 * one heading sampled at four scroll depths read 0 / 0 / 0.80-0.69-0.52 / 0.86-0.78-0.66, the
 * per-word values climbing continuously with scroll depth. Their body copy and illustration
 * cards genuinely never animate — that half of the old claim held up.
 *
 * Ours stays a whole-block transition rather than per-word, which is a deliberate difference:
 * word-splitting needs the headings broken into spans and is a change to markup and JS, not
 * to this file.
 *
 * THIS RULE WAS DEAD FOR ITS ENTIRE LIFE. The comment above used to terminate early — a
 * stray close-comment marker mid-paragraph — so the remaining prose parsed as a selector, and
 * CSS error recovery swallowed everything up to the next open brace, which was this rule's.
 * `.js .reveal.in` survived and this one did not, so sections were pinned visible and nothing
 * ever faded. Nothing errored and the stylesheet still "worked", which is why it survived
 * being committed.
 *
 * Do not write a literal close-comment marker in this file's prose. Doing so while writing
 * the note above broke the stylesheet a second time, in the same way, within a minute.
 *
 * GATED ON `.js`, which an inline script in the head sets before first paint. Without it
 * the rule never applies and every section is simply visible — which is what must happen if
 * landing.js is blocked, errors, or never arrives. The ungated version hid 25 elements at
 * opacity 0 and depended on JavaScript to give them back: one script failure and everything
 * below the hero was a blank page. A decorative effect must never be able to do that. */
.js .reveal {
  opacity: 0;
  transform: translateY(16px);
  transition:
    opacity var(--motion-slow) ease,
    transform var(--motion-slow) cubic-bezier(0.33, 1, 0.68, 1);
}
.js .reveal.in {
  opacity: 1;
  transform: none;
}

/* ── hero entrance ────────────────────────────────────────────────────────────────────
 *
 * A staggered settle on first paint: badge, headline, subhead, buttons, then the phone tray.
 *
 * A CSS ANIMATION, not a transition waiting on a class. The `.js` flag is set by an inline
 * script in <head> before first paint, so the start state is in effect from the very first
 * frame and there is no unstyled flash. If that script never runs, none of this applies and
 * the hero is simply there — the same guarantee the scroll reveals have.
 *
 * `both` holds the from-state through the delay. Without it every element would paint at full
 * opacity, wait its turn, then blink out and fade back in.
 *
 * Unlike the CARD staggers, this one starts from opacity 0 rather than 0.8. That is safe here
 * for a reason that does not hold there: the hero is at the top of the document, so it is
 * always fully in view when this runs. There is no scroll position at which a visitor can
 * catch it half-finished and read it as a broken render.
 *
 * Timing is ours, not measured from the inspiration. Their hero does animate on load — JS
 * writing inline opacity and transform on the eyebrow, each headline word and the buttons —
 * but the tooling here could not sample the first two seconds after navigation, so the exact
 * durations were never captured. These numbers are chosen to match the rest of this page:
 * 620ms and the same easing as the notification. */
@keyframes hero-in {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.js .hero-inner > * {
  animation: hero-in 620ms cubic-bezier(0.33, 1, 0.68, 1) both;
}
.js .hero-inner > .app-lockup { animation-delay: 80ms; }
.js .hero-inner > h1 { animation-delay: 170ms; }
.js .hero-inner > .hero-sub { animation-delay: 260ms; }
.js .hero-inner > .cta-row { animation-delay: 350ms; }
/* The tray is the payoff, so it gets the longest wait and lands last. */
.js .hero-inner > .tray { animation-delay: 450ms; }

/* ── card interiors ───────────────────────────────────────────────────────────────────
 *
 * A staggered settle on the card's children — but it starts from VISIBLE, and that is the
 * whole design.
 *
 * The first version faded from opacity 0, which looked good once a card was fully on screen
 * and broken before that: the reveal wrapper is opaque, so the white card and its shadow were
 * drawn while every child inside was transparent. Measured at the moment it was flagged — 260
 * of the alerts card's 343px on screen, all four children at opacity 0 — an empty rounded
 * rectangle that reads as a failed render.
 *
 * The fix was NOT to delay the trigger. That was tried, and it only moved the empty state
 * further down the page. Measuring aave.com settled it: their headings animate word by word,
 * and a heading caught with 80px on screen read 0.95 / 0.92 / 0.88 / 0.82 — staggered, but
 * never near zero. Their animation starts from nearly-visible and travels a short distance.
 *
 * That is why theirs can fire whenever it likes and never look broken, and it is copied here:
 * 0.8 -> 1 with 8px of travel. Every word is legible at every point in the animation, so the
 * trigger can stay early with the ordinary reveal and no scroll position produces a blank
 * card. 8px is deliberately small — enough to read as motion, too little to look like a
 * layout glitch if someone stops mid-transition.
 *
 * The COUNTING is different and stays gated on full visibility in landing.js: a number
 * counting up is the payoff and has to be watched from the start, whereas a settle is
 * pleasant to catch halfway through. */
.js .reveal .stagger > * {
  opacity: 0.8;
  transform: translateY(8px);
  transition:
    opacity 520ms ease,
    transform 520ms cubic-bezier(0.33, 1, 0.68, 1);
}
.js .reveal.in .stagger > * {
  opacity: 1;
  transform: none;
}
.js .reveal.in .stagger > *:nth-child(1) { transition-delay: 60ms; }
.js .reveal.in .stagger > *:nth-child(2) { transition-delay: 130ms; }
.js .reveal.in .stagger > *:nth-child(3) { transition-delay: 200ms; }
.js .reveal.in .stagger > *:nth-child(4) { transition-delay: 270ms; }
.js .reveal.in .stagger > *:nth-child(5) { transition-delay: 340ms; }

/* Nothing here may hide card content.
 *
 * The first version staggered the children in from opacity 0, which looked good once a card
 * was fully on screen and looked BROKEN before that: the reveal wrapper is opaque, so the
 * white card and its shadow were drawn while every child inside was transparent. Measured at
 * the moment the founder flagged it — 260 of the alerts card's 343px on screen, all four
 * children at opacity 0 — an empty rounded rectangle that reads as a failed render, and the
 * only way out was to keep scrolling.
 *
 * The provider bars never had this problem, and they are the model: every label and figure is
 * present from the moment the card is, and the only thing that animates is a property with a
 * meaningful zero — a bar's width. So the other two cards animate their NUMBERS from zero
 * instead. Same energy, three moving figures against three moving bars, and no state in which
 * the card can look empty. See fillBars/countUp in landing.js. */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  .js .reveal .stagger > * {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .js .hero-inner > * {
    animation: none;
  }
  .js .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .bar-fill,
  .btn,
  .g-card,
  .faq summary::after,
  .nav {
    transition: none;
  }
  .btn:hover,
  .g-card:hover {
    transform: none;
  }
  .notif {
    animation: none;
  }
}

/* Dark mode is NOT wired up yet. tokens.css already carries the dark palette, so opting in
 * is a matter of dropping the `:root[data-theme='dark']` guard — but the hero gradient,
 * the tray and the phone mocks all need looking at on a dark ground before that ships, and
 * shipping a half-checked dark mode is worse than shipping none. */
