/* ═══════════════════════════════════════════════════════════════════════════
   Hidden Spend — marketing site
   ───────────────────────────────────────────────────────────────────────────
   Dark purple, one saturated accent, glossy 3D mascot as the only illustration
   style. Type is Sora for headings (the app uses Sora_700Bold / 600SemiBold)
   and the system stack for body, which mirrors the app exactly.

   The violet ramp is lifted from the app's own design tokens
   (src/constants/theme.ts) so the site and the App Store screenshots read as
   one product rather than two. The near-black canvas is the brief's, not the
   app's — the app is light-mode-first; this is its dark-mode dress.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1. Tokens ──────────────────────────────────────────────────────────── */

:root {
  /* Canvas. Deep, near-black, unmistakably purple. */
  --bg: #0d0b16;
  --bg-raised: #16121f;
  --bg-card: #191426;
  --bg-card-hover: #1f1930;

  /* Brand. 400/500/600 are the app's exact violet stops; `accent` is the
     brief's lighter purple, used for type and glows where the app's 500 is
     too dark to read on near-black. */
  --violet-400: #6f60ed;
  --violet-500: #6c5ce7;
  --violet-600: #5b4bd6;
  --accent: #8b78f5;
  --accent-bright: #a394ff;
  --magenta: #d01e71;

  /* Type. */
  --text: #f6f5fa;
  --text-secondary: #a9a4bd;
  --text-tertiary: #7c7791;

  /* Lines and washes. */
  --border: rgba(255, 255, 255, 0.08);
  --border-strong: rgba(255, 255, 255, 0.14);
  --accent-wash: rgba(139, 120, 245, 0.12);
  --accent-wash-strong: rgba(139, 120, 245, 0.22);

  /* Type scale — fluid, so nothing needs a breakpoint to stay proportionate. */
  --step-hero: clamp(2.75rem, 1.6rem + 5.4vw, 5.5rem);
  --step-display: clamp(2.1rem, 1.45rem + 2.9vw, 3.6rem);
  --step-title: clamp(1.5rem, 1.25rem + 1.1vw, 2.1rem);
  --step-lead: clamp(1.05rem, 0.98rem + 0.36vw, 1.3rem);
  --step-body: 1.0625rem;

  --radius: 20px;
  --radius-lg: 28px;
  --radius-sm: 12px;

  --shell: 1200px;
  --gutter: clamp(1.25rem, 5vw, 2.5rem);
  --section-y: clamp(5rem, 11vw, 9.5rem);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* Set to 0.01ms by the reduced-motion block so every transition can key off
     one variable instead of each rule repeating the media query. */
  --dur-fast: 180ms;
  --dur: 320ms;
  --dur-slow: 700ms;

  color-scheme: dark;
}

/* ── 2. Reset ───────────────────────────────────────────────────────────── */

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

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The header is fixed and 72px tall. Without this, following any in-page
     link parks the target's top edge underneath it — including the skip link,
     which is supposed to be the bypass. It also covers Shift+Tab: the browser
     scrolls a focused element into view using scroll-padding, so this is what
     keeps a backward-tabbed control from landing under the header
     (WCAG 2.4.11 Focus Not Obscured). scroll-margin on the targets alone
     cannot fix the focus case, because focus can land on any element. */
  scroll-padding-top: 6rem;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
    Arial, sans-serif;
  font-size: var(--step-body);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

/* A single fixed wash so the page never reads as flat black between sections —
   it should feel like the light in the room, not an object moving past.
   ─────────────────────────────────────────────────────────────────────────
   Drawn on a fixed pseudo-element rather than with `background-attachment:
   fixed` on the body. That property forces the browser to repaint the gradient
   across the whole viewport on every scroll frame, and it cannot be composited;
   on this page it was starving the marquee, which is a continuously animating
   layer immediately below the fold. A fixed element gets its own layer and is
   simply offset by the compositor instead. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(60rem 45rem at 78% -8%, rgba(108, 92, 231, 0.28), transparent 62%),
    radial-gradient(45rem 38rem at 8% 34%, rgba(139, 120, 245, 0.1), transparent 58%);
}

img,
svg,
video {
  display: block;
  max-width: 100%;
  /* Required, not cosmetic. Every image carries width/height attributes so the
     browser can reserve its box before the file arrives; without `height:auto`
     the height attribute wins over a CSS-set width and the image renders at its
     intrinsic height — the hero ghost came out 384×853 instead of 384×256.
     Rules that genuinely want a fixed height (.hero-video) override this from a
     class and are unaffected. */
  height: auto;
}

a {
  color: inherit;
  text-decoration: none;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

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

::selection {
  background: var(--accent);
  color: #0b0813;
}

/* ── 3. Typography ──────────────────────────────────────────────────────── */

h1,
h2,
h3,
h4,
.font-display {
  font-family: 'Sora', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.03em;
  text-wrap: balance;
}

h3,
h4 {
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.02em;
}

p {
  text-wrap: pretty;
}

.overline {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

.lead {
  font-size: var(--step-lead);
  line-height: 1.6;
  color: var(--text-secondary);
}

/* The one place colour is allowed to carry meaning rather than decorate. */
.accent-text {
  background: linear-gradient(
    100deg,
    var(--accent-bright) 0%,
    var(--accent) 42%,
    var(--violet-400) 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* Without this the descenders of a clipped gradient get shaved in WebKit. */
  padding-bottom: 0.08em;
}

/* ── 4. Layout ──────────────────────────────────────────────────────────── */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

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

/* Anchor targets must clear the fixed header, or following an in-page link
   parks the section heading underneath it — invisible, and for a keyboard user
   the focused element lands off-screen. */
section[id],
[id]:target {
  scroll-margin-top: 6rem;
}

.section-head {
  max-width: 46rem;
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
}

.section-head .overline {
  display: block;
  margin-bottom: 1rem;
}

.section-head h2 {
  font-size: var(--step-display);
  margin-bottom: 1.25rem;
}

.center {
  text-align: center;
  margin-inline: auto;
}

.skip-link {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translate(-50%, -120%);
  z-index: 200;
  background: var(--accent);
  color: #0b0813;
  font-weight: 600;
  padding: 0.7rem 1.4rem;
  border-radius: 0 0 12px 12px;
  transition: transform var(--dur-fast) var(--ease);
}

.skip-link:focus-visible {
  transform: translate(-50%, 0);
}

/* The skip link's target carries tabindex="-1" so activating it actually moves
   focus rather than only scrolling. A focus ring around the entire main
   landmark is noise, though — the scroll position is the feedback. */
main:focus {
  outline: none;
}

/* ── 5. Buttons ─────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 1rem 1.75rem;
  border-radius: 999px;
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: -0.01em;
  /* Apple's minimum tappable target, same as the app's Layout.touchTarget. */
  min-height: 44px;
  transition:
    transform var(--dur-fast) var(--ease),
    box-shadow var(--dur) var(--ease),
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease);
}

.btn-primary {
  background: linear-gradient(135deg, var(--violet-400), var(--violet-600));
  color: #fff;
  box-shadow:
    0 10px 30px -8px rgba(108, 92, 231, 0.6),
    inset 0 1px 0 rgba(255, 255, 255, 0.18);
}

.btn-primary:hover {
  transform: translateY(-2px);
  box-shadow:
    0 16px 40px -10px rgba(108, 92, 231, 0.75),
    inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

.btn-primary:active {
  transform: translateY(0) scale(0.985);
}

.btn-ghost {
  border: 1px solid var(--border-strong);
  color: var(--text);
  background: rgba(255, 255, 255, 0.03);
}

.btn-ghost:hover {
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.24);
  transform: translateY(-2px);
}

.btn-apple-glyph {
  width: 1.15em;
  height: 1.15em;
  fill: currentColor;
  flex: none;
  /* Apple's mark sits optically high against a cap-height label. */
  margin-top: -0.14em;
}

.cta-note {
  font-size: 0.875rem;
  color: var(--text-tertiary);
  margin-top: 1.1rem;
}

/* ── 6. Header ──────────────────────────────────────────────────────────── */

.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    backdrop-filter var(--dur) var(--ease);
  border-bottom: 1px solid transparent;
}

.site-header.is-stuck {
  background: rgba(13, 11, 22, 0.82);
  backdrop-filter: blur(18px) saturate(1.4);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  border-bottom-color: var(--border);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  height: 72px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  font-family: 'Sora', sans-serif;
  font-weight: 700;
  font-size: 1.0625rem;
  letter-spacing: -0.02em;
}

.brand img {
  width: 34px;
  height: 34px;
  border-radius: 9px;
}

.nav {
  display: flex;
  align-items: center;
  gap: 2rem;
}

.nav-links {
  display: flex;
  gap: 2rem;
  list-style: none;
  padding: 0;
}

.nav-links a {
  font-size: 0.9375rem;
  color: var(--text-secondary);
  transition: color var(--dur-fast) var(--ease);
  position: relative;
  /* Padding rather than a height: it grows the hit area past the WCAG 2.5.8
     24px floor without moving the text off the header's optical centre. */
  display: inline-block;
  padding-block: 0.55rem;
}

.nav-links a::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0.35rem;
  width: 100%;
  height: 1.5px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}

.nav-links a:hover {
  color: var(--text);
}

.nav-links a:hover::after {
  transform: scaleX(1);
}

.nav .btn {
  padding: 0.65rem 1.3rem;
  font-size: 0.9375rem;
}

@media (max-width: 760px) {
  .nav-links {
    display: none;
  }
}

/* ── 7. Hero ────────────────────────────────────────────────────────────── */

.hero {
  position: relative;
  padding-top: clamp(8rem, 16vw, 11rem);
  padding-bottom: var(--section-y);
  overflow: hidden;
  isolation: isolate;
}

/* The looping cinematic sits behind everything at low opacity — it reads as
   moving light rather than as a video someone has to watch. */
.hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.4;
  z-index: -2;
}

.hero-scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(180deg, rgba(13, 11, 22, 0.72) 0%, rgba(13, 11, 22, 0.55) 40%, var(--bg) 96%),
    linear-gradient(90deg, var(--bg) 4%, rgba(13, 11, 22, 0.45) 46%, transparent 78%);
}

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  align-items: center;
  gap: clamp(2rem, 5vw, 4.5rem);
}

.hero-copy {
  max-width: 34rem;
}

.hero-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.45rem 0.95rem 0.45rem 0.6rem;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.04);
  /* iOS Safari needed the prefix through 17, and iPhone users are the audience. */
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 1.6rem;
}

.hero-eyebrow img {
  width: 22px;
  height: 22px;
  border-radius: 6px;
}

.hero h1 {
  font-size: var(--step-hero);
  margin-bottom: 1.5rem;
}

.hero .lead {
  margin-bottom: 2.25rem;
  max-width: 30rem;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
}

/* The mascot's stage. The glow is a sibling rather than a box-shadow so it can
   sit under a transparent PNG without clipping to its bounding box. */
.hero-art {
  position: relative;
  display: grid;
  place-items: center;
}

.hero-art::before {
  content: '';
  position: absolute;
  width: 78%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(139, 120, 245, 0.4), transparent 66%);
  filter: blur(46px);
  z-index: 0;
}

/* ── Hero phones ──────────────────────────────────────────────────────────
   Device frames derived from devices.css (marvelapp), MIT licensed:
     Copyright (c) 2016 Marvel  —  https://github.com/marvelapp/devices.css
   Its `.device-iphone-14-pro` supplied the real geometry — Dynamic Island,
   side-button placement, antenna stripes, and the inset rail highlights that
   make a frame read as titanium rather than as a grey outline.

   Two deliberate departures from the library:

   1. Pro Max, not Pro. The library's screen is 390x830pt (0.470); these
      screenshots are 1320x2868 (0.4603), which is exactly Pro Max at
      440x956pt. Using the library's own aspect would letterbox or crop every
      screenshot, so the geometry is retuned rather than adopted wholesale.
      No maintained library ships an iPhone 17 Pro Max frame; this is the
      current Pro-line design, which has not changed shape since the 14 Pro.

   2. Sized in em, not fixed pixels. devices.css hard-codes px and expects a
      `transform: scale()` to resize, which leaves the layout box at full size
      and would wreck the hero grid's centring. Here `.device` sets
      `font-size: --phone-w / 48`, so 1em = 10 design px and every dimension
      below is fluid. The element's own box stays correct at any width.
   ────────────────────────────────────────────────────────────────────────── */

.hero-phones {
  --phone-w: clamp(10rem, 17vw, 15.5rem);
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  /* The grid centres the *front* phone, but the angled one juts out to the
     left, so the pair as a whole would sit left of centre. Padding the left
     edge shifts the front phone right by half its value, which re-centres the
     composition rather than just the element the grid can see. */
  padding: 1.5rem 0 1.5rem var(--phone-w);
}

/* 1em = 10 design px. Device is 480 x 996 design px => 48em x 99.6em. */
/* Height is deliberately left to the content. Pinning it to the design's
   99.6em fought the border-box frame — padding and border eat 4.2em, so a
   fixed screen height rendered the screenshot at 0.4550 instead of its native
   0.4603 and quietly stretched it. Letting the image set the height makes the
   aspect exact by construction. */
.device {
  position: relative;
  font-size: calc(var(--phone-w) / 48);
  width: 48em;
}

/* Bezel from the real hardware, not from the library. A Pro Max is 77.6mm wide
   around a 74.0mm screen — 1.8mm a side, or 2.3% of the width. devices.css uses
   19px on a 428px body (4.4%), which is nearly double and is the single thing
   that makes a CSS frame read as a drawing rather than a phone. At 48em wide,
   1.8mm works out at 1.11em.

   The radii follow from that: the screen corner is ~55pt on a 440pt-wide
   screen (12.5%), and the body corner is simply that plus the bezel. */
.device-frame {
  width: 100%;
  padding: 1.11em;
  border: 0.1em solid #7d7671;
  border-radius: 6.9em;
  background: #010101;
  /* The two inset rings are what sell the metal: a bright specular edge just
     inside the border, then a darker band behind it for depth. */
  box-shadow:
    inset 0 0 0.4em 0.16em #e6e1da,
    inset 0 0 0 0.4em #5a544f,
    0 4em 8em -2.8em rgba(0, 0, 0, 0.9);
}

/* The cutout is pinned to the Pro Max screen ratio, so the body keeps phone
   proportions no matter what is dropped in.

   `cover`, not `contain`. Assets from scripts/prepare-screenshots.py are cropped
   to this exact ratio so neither does anything — but if a raw screenshot is ever
   dropped in by hand, `contain` would letterbox it and put black gaps back
   inside the screen. `cover` fills edge to edge and loses a sliver of height
   instead, which is the right trade for a device mockup: a gap reads as a bug,
   a 2% crop reads as nothing at all. */
/* aspect-ratio must match what scripts/prepare-screenshots.py emits — it prints
   the value to use. `cover` + `top` fills the cutout edge to edge and anchors
   the image to the top edge, so the screenshot's own status bar always sits
   exactly where it belongs and any trimming can only ever come off the bottom.
   With prepared assets the ratios already agree and nothing is cropped at all. */
.device-screen {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 720 / 1564;
  object-fit: cover;
  object-position: top center;
  background: #000;
  border-radius: 5.75em;
}

/* Dynamic Island — a pill, not a notch.
   ─────────────────────────────────────────────────────────────────────────
   Position is measured against the screenshots rather than guessed, because
   these captures carry their own status bar and the two have to line up the
   way they do on real hardware: the island sits *between* the clock and the
   status icons, sharing their vertical centre.

   Re-derived for the 720x1564 batch. Every one of those captures puts its
   status bar ink at 2.56%–4.22% of screen height (centre 3.39%) and leaves a
   free gap from 26.2% to 71.0% of the width. A real island is 37pt tall on a
   956pt screen (3.87%) and 28% of the width. Centring 3.87% on 3.39% puts its
   top at 1.455% of screen height.

   The screen is 45.58em wide and 99.01em tall inside the bezel, so:
     top    = 1.21em (bezel + border) + 1.455% x 99.01em = 2.65em
     height = 3.87% x 99.01em                            = 3.83em
     width  = 28%   x 45.58em                            = 12.76em

   The pill therefore spans 36%–64% of the width, inside the 26.2%–71.0% gap,
   so it sits between the clock and the status icons and touches neither.

   These numbers move whenever the asset aspect does — they are percentages of
   a screen height that is derived from it. The earlier 102.81em / top 2.38em
   pair was correct for the old 720x1624 assets and wrong for these. */
.device-island {
  position: absolute;
  z-index: 2;
  top: 2.65em;
  left: 50%;
  margin-left: -6.38em;
  width: 12.76em;
  height: 3.83em;
  border-radius: 1.92em;
  background: #000;
}

/* The front camera, offset inside the island as it is on the device. */
.device-island::after {
  content: '';
  position: absolute;
  top: 1.29em;
  right: 1.1em;
  width: 1.4em;
  height: 1.4em;
  border-radius: 50%;
  background:
    radial-gradient(farthest-corner at 30% 30%, #5b6fb8 0, transparent 45%),
    radial-gradient(farthest-corner at 75% 75%, #4a3280 0, #22505a 30%, transparent 60%);
  box-shadow: 0 0 0 0.1em rgba(255, 255, 255, 0.06);
}

/* Antenna bands across the rail. */
.device-stripe::before,
.device-stripe::after {
  content: '';
  position: absolute;
  left: 0;
  z-index: 3;
  width: 100%;
  height: 0.78em;
  border: solid rgba(0, 0, 0, 0.22);
  border-width: 0 0.78em;
}

.device-stripe::after {
  top: 9.5em;
}

.device-stripe::before {
  bottom: 9.5em;
}

/* Left rail: action button, then the volume rocker. */
.device-btns {
  position: absolute;
  z-index: 3;
  top: 12.9em;
  left: -0.22em;
  width: 0.34em;
  height: 3.6em;
  border-radius: 0.2em;
  background: #6b645f;
}

.device-btns::before,
.device-btns::after {
  content: '';
  position: absolute;
  left: 0;
  width: 0.34em;
  height: 7em;
  border-radius: 0.2em;
  background: #6b645f;
}

.device-btns::after {
  top: 6.7em;
}

.device-btns::before {
  top: 15.7em;
}

/* Right rail: side (power) button. */
.device-power {
  position: absolute;
  z-index: 3;
  top: 22.4em;
  right: -0.22em;
  width: 0.34em;
  height: 11.2em;
  border-radius: 0.2em;
  background: #6b645f;
}

.hero-phone--front {
  z-index: 2;
}

.hero-phone--back {
  position: absolute;
  z-index: 1;
  top: 50%;
  left: 50%;
  /* Offset by roughly half a phone so the two overlap rather than merely sit
     side by side, then rotated anticlockwise so the fan opens to the left. */
  transform: translate(-86%, -56%) rotate(-11deg) scale(0.9);
  transform-origin: center;
}

/* The back phone is scenery — the front one carries the message, and two
   fully-lit screens compete. */
.hero-phone--back .device-screen {
  filter: brightness(0.82) saturate(0.92);
}

@media (max-width: 900px) {
  .hero-grid {
    grid-template-columns: 1fr;
    text-align: center;
  }

  .hero-copy {
    margin-inline: auto;
  }

  .hero .lead {
    margin-inline: auto;
  }

  .hero-actions {
    justify-content: center;
  }

  .hero-art {
    order: -1;
    margin-bottom: 1rem;
  }

  /* Stacked above the copy, so the pair has to be smaller or it pushes the
     headline and CTAs off the first screen entirely. */
  .hero-phones {
    --phone-w: clamp(8rem, 34vw, 11rem);
  }
}

/* ── 8. Ticker ──────────────────────────────────────────────────────────── */

.ticker {
  position: relative;
  padding-block: 1.6rem;
  border-block: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.015);
  overflow: hidden;
  /* Fades the ends so names dissolve instead of being chopped by the viewport. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}

.ticker-label {
  text-align: center;
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: 1.1rem;
}

.ticker-track {
  display: flex;
  width: max-content;
  /* How many copies of the list are in the markup. The keyframe travels
     -100% / this, which is exactly one copy whatever the number — so adding or
     removing copies needs no other change. main.js updates it if it has to
     clone more for an unusually wide display. */
  --ticker-groups: 4;
  animation: marquee 42s linear infinite;
  /* Promote to its own compositor layer. Without this the track animated on the
     main thread: `will-change` was `auto` and the transform stayed an identity
     matrix, so every frame re-rastered inside the masked, overflow-hidden
     parent instead of being a GPU offset. */
  will-change: transform;
  transform: translateZ(0);
  backface-visibility: hidden;
}

/* The gap lives on the group, not the track, and each group carries its own
   trailing gap. That makes the two groups exactly equal in width, so -50% is
   precisely one group and the wrap is seamless without any correction term. */
.ticker-group {
  display: flex;
  gap: 3.5rem;
  padding-right: 3.5rem;
}

.ticker:hover .ticker-track {
  animation-play-state: paused;
}

.ticker-item {
  font-family: 'Sora', sans-serif;
  font-weight: 600;
  font-size: clamp(1rem, 0.9rem + 0.5vw, 1.35rem);
  letter-spacing: -0.02em;
  color: var(--text-tertiary);
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease);
}

.ticker-item:hover {
  color: var(--accent);
}

/* Exactly one group of travel, so the next copy lands precisely where the
   previous one began. Derived from the copy count rather than hard-coded:
   -100% is the whole track, so -100%/N is one copy of N. `from` is stated
   rather than implied so the animation cannot inherit the promoted layer's
   translateZ(0) as its start value. */
@keyframes marquee {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    transform: translate3d(calc(-100% / var(--ticker-groups)), 0, 0);
  }
}

/* ── 10. Features ───────────────────────────────────────────────────────── */
/* Six cards, so the grid divides evenly at one, two and three columns and no
   breakpoint can orphan a card. (The previous set of five needed a 3 + 2 split
   at six tracks to avoid exactly that.) */
.feature-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1rem, 2vw, 1.5rem);
  margin-top: clamp(2rem, 4vw, 3.5rem);
}

@media (min-width: 40rem) {
  .feature-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 62rem) {
  .feature-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.feature-tile {
  padding: clamp(1.4rem, 2.2vw, 1.9rem);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-card);
  transition:
    transform var(--dur) var(--ease),
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.feature-tile:hover {
  transform: translateY(-4px);
  background: var(--bg-card-hover);
  border-color: var(--border-strong);
}

/* A fixed, identical box for every card.
   ─────────────────────────────────────────────────────────────────────────
   This alone was not enough, and is worth explaining. The source poses are not
   consistent with one another: six are 500x500 but `reminder` and `vip` are
   669x373 landscape, and the drawing inside occupies between 29.7% and 65.6%
   of its canvas depending on the pose. An identical box plus `object-fit:
   contain` therefore produced wildly different apparent sizes — and for the two
   landscape files, whose content sits in the left or right third, a ghost
   letterboxed into a corner that read as a crop.

   So the assets are normalised first, by `scripts/normalize-mascots.py`: each
   is cropped to its alpha bounding box and re-centred on a 256px square with
   its longest side at exactly 210px. Every file in `mascot/tile/` is now the
   same shape at the same scale, which is what makes this rule sufficient.
   `contain` is kept regardless, so nothing can ever be cropped. */
.feature-mascot {
  width: 72px;
  height: 72px;
  object-fit: contain;
  object-position: center;
  margin-bottom: 0.9rem;
  filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.45));
}
.feature-tile h3 {
  font-size: 1.0625rem;
  margin-bottom: 0.5rem;
}

.feature-tile p {
  color: var(--text-secondary);
  font-size: 0.9375rem;
  line-height: 1.6;
}

/* ── 12. (was Pro) ──────────────────────────────────────────────────────────
   Deleted. The pricing banner, the price card, the perk list and the purchase
   note all lived here. Nothing on this site states a price any more — the App
   Store listing is the single place that does, so there is nothing here to
   drift out of sync with it. */

/* ── 12b. Screens ───────────────────────────────────────────────────────── */

/* Three upright phones in a row. The frame itself is the hero's — .device sizes
   everything from --phone-w, so the only thing this section sets is that one
   variable and the card around it. */
.screen-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.5vw, 2rem);
}

.screen-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: clamp(1.75rem, 3vw, 2.75rem) clamp(1.25rem, 2vw, 1.75rem);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  /* A lifted surface rather than a flat panel: the same card ground the feature
     tiles use, with a violet bloom at the top so the phone reads as lit from
     behind rather than pasted on. */
  background:
    radial-gradient(18rem 12rem at 50% -6%, var(--accent-wash), transparent 70%),
    var(--bg-card);
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.screen-card:hover {
  border-color: var(--border-strong);
}

/* The phone. --phone-w is what .device scales from, so this single value drives
   the frame, the bezel, the island and the side buttons together. */
.screen-phone {
  --phone-w: clamp(9rem, 15vw, 12.5rem);
  margin-bottom: clamp(1.5rem, 2.5vw, 2rem);
}

.screen-card h3 {
  font-size: 1.1875rem;
  margin-bottom: 0.5rem;
}

.screen-card p {
  color: var(--text-secondary);
  font-size: 0.9375rem;
  line-height: 1.6;
  max-width: 22rem;
}

/* 2-across, then the third centred underneath — the row of three is too narrow
   to stay legible before it needs to stack. */
@media (max-width: 60rem) {
  .screen-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .screen-card:last-child {
    grid-column: 1 / -1;
    justify-self: center;
    max-width: 22rem;
  }
}

@media (max-width: 34rem) {
  .screen-grid {
    grid-template-columns: 1fr;
  }

  .screen-card:last-child {
    grid-column: auto;
    max-width: none;
  }

  .screen-phone {
    --phone-w: min(58vw, 13rem);
  }
}

/* ── 13. FAQ ────────────────────────────────────────────────────────────── */

.faq-list {
  max-width: 48rem;
  margin-inline: auto;
  display: grid;
  gap: 0.75rem;
}

.faq-item {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-card);
  overflow: hidden;
  transition: border-color var(--dur) var(--ease);
}

.faq-item[open] {
  border-color: var(--border-strong);
}

.faq-item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.15rem 1.4rem;
  font-family: 'Sora', sans-serif;
  font-weight: 600;
  font-size: 1.0313rem;
  letter-spacing: -0.015em;
  cursor: pointer;
  list-style: none;
  transition: color var(--dur-fast) var(--ease);
}

.faq-item summary::-webkit-details-marker {
  display: none;
}

/* The summary fills its item's padding box exactly, and the item is
   `overflow: hidden` to clip the corner radius — so the global focus ring, drawn
   at +3px, was painted outside the clip region and never appeared. Six
   consecutive keyboard stops with no visible focus. Drawing it inward puts it
   back inside the clip. */
.faq-item summary:focus-visible {
  outline-offset: -3px;
  border-radius: var(--radius-sm);
}

.faq-item summary:hover {
  color: var(--accent-bright);
}

.faq-chevron {
  width: 17px;
  height: 17px;
  stroke: var(--accent);
  stroke-width: 2.2;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
  transition: transform var(--dur) var(--ease);
}

.faq-item[open] .faq-chevron {
  transform: rotate(180deg);
}

.faq-answer {
  padding: 0 1.4rem 1.35rem;
  color: var(--text-secondary);
  font-size: 0.9688rem;
  line-height: 1.7;
  max-width: 42rem;
}

/* ── 14. Download ───────────────────────────────────────────────────────── */

/* A slightly lifted ground so the closing band separates from the section above
   it without a rule. Same value as `--bg-raised`, used directly rather than
   through the token because this is the only full-bleed band on the page and a
   token change elsewhere should not silently restripe it. */
.download {
  position: relative;
  isolation: isolate;
  background: #16121f;
  border-top: 1px solid var(--border);
  padding-block: clamp(4rem, 8vw, 6.5rem);
}

/* A wash across the band so it is not flat black behind the mascot. Wider and
   weaker than the hero's — that one is 0.4 alpha over a video and is meant to
   be seen; this is 0.12 and should only be noticed if you go looking. It sits
   at -1 inside the section's own stacking context, so it never rises over the
   button. */
.download::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(38rem 22rem at 50% 8%, var(--accent-wash), transparent 68%),
    linear-gradient(180deg, #171223 0%, #16121f 62%);
}

/* Same construction as .notfound-art: a blurred violet disc behind a mascot,
   with the image on top of it. Kept to that idiom so the two read as one
   family rather than two ideas about the same effect. */
.download-art {
  position: relative;
  display: grid;
  place-items: center;
  margin-bottom: clamp(1.25rem, 2.5vw, 1.75rem);
}

.download-art::before {
  content: '';
  position: absolute;
  width: 78%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent-wash-strong), transparent 68%);
  filter: blur(40px);
}

.download-art img {
  position: relative;
  width: clamp(6.5rem, 11vw, 8.5rem);
  filter: drop-shadow(0 20px 30px rgba(0, 0, 0, 0.55));
}

.download-title {
  font-size: var(--step-title);
  margin-bottom: 1.75rem;
}

.download-actions {
  display: grid;
  justify-items: center;
  gap: 0.9rem;
}

.download-actions .cta-note {
  /* The shared note colour is `--text-tertiary`, which clears 4.5:1 on the
     page canvas but only manages 4.30:1 on this section's lifted ground. One
     step up puts it at 7.66:1; it still reads as a footnote because it is
     14px and sits under a button. */
  color: var(--text-secondary);
}

/* ── 15. Footer ─────────────────────────────────────────────────────────── */

.site-footer {
  border-top: 1px solid var(--border);
  padding-block: clamp(3rem, 6vw, 4.5rem) 2.5rem;
  background: rgba(0, 0, 0, 0.25);
}

.footer-top {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 1fr));
  gap: clamp(2rem, 5vw, 4rem);
  padding-bottom: 2.5rem;
}

.footer-brand p {
  color: var(--text-tertiary);
  font-size: 0.9375rem;
  margin-top: 1rem;
  max-width: 22rem;
}

.footer-col h3 {
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: 1.1rem;
  font-weight: 600;
}

.footer-col ul {
  list-style: none;
  padding: 0;
  display: grid;
  /* Trimmed to offset the padding added to the links below, so the visual
     rhythm is unchanged while each target grows to ~30px. */
  gap: 0.3rem;
}

.footer-col a {
  color: var(--text-secondary);
  font-size: 0.9375rem;
  transition: color var(--dur-fast) var(--ease);
  display: inline-block;
  padding-block: 0.28rem;
}

.footer-col a:hover {
  color: var(--accent-bright);
}

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 2rem;
  border-top: 1px solid var(--border);
  font-size: 0.875rem;
  color: var(--text-tertiary);
}

@media (max-width: 720px) {
  .footer-top {
    grid-template-columns: 1fr 1fr;
  }

  .footer-brand {
    grid-column: 1 / -1;
  }
}

/* ── 16. Legal pages ────────────────────────────────────────────────────── */

.legal-hero {
  padding-top: clamp(7.5rem, 14vw, 10rem);
  padding-bottom: clamp(2rem, 4vw, 3rem);
  border-bottom: 1px solid var(--border);
}

.legal-hero h1 {
  font-size: var(--step-display);
  margin-block: 1rem 0.85rem;
}

.legal-meta {
  color: var(--text-tertiary);
  font-size: 0.9375rem;
}

.legal-body {
  max-width: 46rem;
  padding-block: clamp(2.5rem, 5vw, 4rem) var(--section-y);
  /* Legal prose is read, not scanned — a slightly larger measure and looser
     leading than the marketing copy above it. */
  font-size: 1.0313rem;
  line-height: 1.78;
  color: var(--text-secondary);
}

.legal-body h2 {
  font-size: clamp(1.3rem, 1.15rem + 0.6vw, 1.6rem);
  color: var(--text);
  margin-top: 3rem;
  margin-bottom: 1rem;
  scroll-margin-top: 6rem;
}

.legal-body h2:first-child {
  margin-top: 0;
}

.legal-body h3 {
  font-size: 1.0938rem;
  color: var(--text);
  margin-top: 2rem;
  margin-bottom: 0.75rem;
}

.legal-body p {
  margin-bottom: 1.15rem;
}

.legal-body ul {
  margin-bottom: 1.4rem;
  padding-left: 1.3rem;
  display: grid;
  gap: 0.6rem;
}

.legal-body li {
  padding-left: 0.3rem;
}

.legal-body li::marker {
  color: var(--accent);
}

.legal-body strong {
  color: var(--text);
  font-weight: 600;
}

.legal-body a {
  color: var(--accent-bright);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--accent-wash-strong);
  transition: text-decoration-color var(--dur-fast) var(--ease);
}

.legal-body a:hover {
  text-decoration-color: var(--accent-bright);
}

.legal-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.875em;
  padding: 0.15em 0.42em;
  border-radius: 5px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--border);
  color: var(--text);
}

/* The "short version" paragraph the documents open with. */
.legal-summary {
  padding: clamp(1.25rem, 2.5vw, 1.75rem);
  border: 1px solid var(--accent-wash-strong);
  border-radius: var(--radius);
  background: var(--accent-wash);
  margin-bottom: 2.5rem;
}

.legal-summary p {
  margin-bottom: 0;
  color: var(--text);
}

.legal-toc {
  padding: clamp(1.25rem, 2.5vw, 1.6rem) clamp(1.4rem, 2.5vw, 1.8rem);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-card);
  margin-bottom: 3rem;
}

.legal-toc h2 {
  font-size: 0.75rem !important;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-tertiary) !important;
  margin: 0 0 1rem !important;
  font-family: inherit;
  font-weight: 600;
}

.legal-toc ol {
  margin: 0;
  padding-left: 1.2rem;
  display: grid;
  gap: 0.5rem;
  font-size: 0.9375rem;
}

.legal-toc li::marker {
  color: var(--text-tertiary);
}

.legal-toc a {
  text-decoration: none;
}

.legal-toc a:hover {
  color: var(--accent-bright);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ── 17. Motion ─────────────────────────────────────────────────────────── */

/* Scroll reveal.
   ─────────────────────────────────────────────────────────────────────────
   Scoped to `.reveals-armed`, which an inline script in <head> puts on <html>.
   The hiding must not exist unless the script that un-hides it is running: the
   markup carries `class="reveal"` statically, so an unscoped `opacity: 0` would
   render the whole page blank for anyone with JS disabled, blocked, or simply
   failing to load.

   Deliberately NOT `.js`. That flag means "scripting is available" and other
   rules depend on it — notably the motion toggle. When the failsafe fired it
   dropped `.js`, which un-hid the reveals but also permanently hid the pause
   control while the marquee kept running. Two jobs, two flags. */
.reveals-armed .reveal {
  opacity: 0;
  transform: translateY(24px);
  transition:
    opacity var(--dur-slow) var(--ease),
    transform var(--dur-slow) var(--ease);
}

.reveals-armed .reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* Stagger for children of a revealed group. */
.reveals-armed .reveal-group > * {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity var(--dur-slow) var(--ease),
    transform var(--dur-slow) var(--ease);
}

.reveals-armed .reveal-group.is-visible > * {
  opacity: 1;
  transform: none;
}

.reveals-armed .reveal-group.is-visible > *:nth-child(1) { transition-delay: 0ms; }
.reveals-armed .reveal-group.is-visible > *:nth-child(2) { transition-delay: 80ms; }
.reveals-armed .reveal-group.is-visible > *:nth-child(3) { transition-delay: 160ms; }
.reveals-armed .reveal-group.is-visible > *:nth-child(4) { transition-delay: 240ms; }
.reveals-armed .reveal-group.is-visible > *:nth-child(5) { transition-delay: 320ms; }

/* ── 17b. 404 ───────────────────────────────────────────────────────────── */

.notfound {
  padding-top: clamp(9rem, 18vw, 13rem);
}

.notfound-art {
  position: relative;
  display: grid;
  place-items: center;
  margin-bottom: 2rem;
}

.notfound-art::before {
  content: '';
  position: absolute;
  width: 42%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent-wash-strong), transparent 68%);
  filter: blur(40px);
}

.notfound-art img {
  position: relative;
  width: min(45%, 11rem);
  filter: drop-shadow(0 24px 36px rgba(0, 0, 0, 0.5));
}

.notfound h1 {
  font-size: var(--step-display);
  margin-block: 1rem 1.25rem;
}

.notfound .lead {
  max-width: 30rem;
  margin: 0 auto 2.25rem;
}

/* The 404 has no columns above it, so the divider the footer normally needs
   would be a rule under nothing. */
.footer-bottom-bare {
  border-top: 0;
  padding-top: 0;
}

/* ── 18. Motion toggle ──────────────────────────────────────────────────── */

/* WCAG 2.2.2: content that moves automatically for more than five seconds needs
   a mechanism to pause it. `prefers-reduced-motion` covers people who have set
   that preference system-wide; this covers everyone else, and is the only way a
   keyboard or touch user can stop the marquee (hover-to-pause reaches neither). */
.motion-toggle {
  position: fixed;
  right: clamp(0.85rem, 2vw, 1.5rem);
  bottom: clamp(0.85rem, 2vw, 1.5rem);
  z-index: 90;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  padding: 0.55rem 1rem 0.55rem 0.8rem;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: rgba(13, 11, 22, 0.86);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--text-secondary);
  font-size: 0.8125rem;
  font-weight: 500;
  transition:
    color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    opacity var(--dur) var(--ease);
  opacity: 0.55;
}

.motion-toggle:hover,
.motion-toggle:focus-visible {
  opacity: 1;
  color: var(--text);
  border-color: var(--accent);
}

.motion-toggle svg {
  width: 15px;
  height: 15px;
  stroke: var(--accent);
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
}

/* Each icon shows only in its matching state, so the button always depicts the
   action it will perform. */
.motion-toggle .icon-play { display: none; }
[data-motion='paused'] .motion-toggle .icon-play { display: block; }
[data-motion='paused'] .motion-toggle .icon-pause { display: none; }

[data-motion='paused'] .ticker-track {
  animation-play-state: paused;
}

/* An author `display` value beats the `hidden` attribute's UA rule, so without
   this every `toggle.hidden = ...` write in main.js would be a silent no-op and
   the button would show before it was wired up. */
.motion-toggle[hidden] {
  display: none;
}

/* Without JS there is nothing to toggle, and a dead control is worse than none. */
html:not(.js) .motion-toggle {
  display: none;
}

/* ── 19. Reduced motion ─────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 0.01ms;
    --dur: 0.01ms;
    --dur-slow: 0.01ms;
  }

  html {
    scroll-behavior: auto;
  }

  /* Anything that loops or travels stops outright. Transitions above are
     effectively instant via the duration variables. */
  .ticker-track {
    animation: none;
  }

  .reveals-armed .reveal,
  .reveals-armed .reveal-group > * {
    opacity: 1;
    transform: none;
  }

  .btn:hover,
  .feature-tile:hover {
    transform: none;
  }

  .hero-video {
    display: none;
  }
}
