/*
  Wing Display — site tokens + base layer.

  Built on the LIGHT column of docs/design/tokens.md, alongside the SwiftUI theme
  (macos/WingDisplay/DesignSystem/DesignTokens.swift) and the Compose MaterialTheme
  (android/.../ui/theme/Theme.kt). Values come from that doc; don't invent new ones.

  WHY LIGHT, when the Electric Signal rule says always-dark: that rule governs app-owned
  CLIENT SCREENS, because the product is a display device and dark keeps the streamed
  picture uncluttered. A marketing site is not a client screen, and tokens.md already
  carries a light column the apps use under a light system appearance. Owner decision,
  2026-07-31.

  A handful of tokens below exist only here — background, hero-white, hero-bg, hero-accent, ink,
  accent-strong, warning-strong, surface-raised, the hero type step, space-4xl, radius-xl,
  glow-cyan, and two shadows — because a landing page needs a tinted ground, a large headline, a
  raised card, depth that the apps have no use for, and text colours dark enough to pass
  WCAG 1.4.3 on that ground. The colours among them are tabled in tokens.md, "Website-only values".

  No @font-face, no @import, no url() to any external host: the site promises zero
  third-party requests, and the privacy page states that as fact.
*/

:root {
  /* Color — light column */
  --color-accent: #0E8E92;
  --color-accent-strong: #0A6F72;
  --color-on-accent: #FFFFFF;
  --color-background: #F7FAFA;
  --color-surface: #F4F7F8;
  --color-surface-raised: #FFFFFF;
  --color-ink: #0B1416;
  --color-on-background: #0E1416;
  --color-text-secondary: #566569;
  --color-outline: #D3DBDD;
  --color-success: #0E9F6E;
  --color-warning: #B26A00;
  /* warning at text size: #B26A00 is under 4.5:1 on white, this is 6.4:1 */
  --color-warning-strong: #8A5200;
  --color-error: #C73B3B;
  /* Electric Signal brand layer — used here as light accents, not surfaces */
  --color-aurora-indigo: #4C5EFF;
  --color-feather-tip: #B8FFF9;
  --color-glow-cyan: #2BD4DB;

  /* Hero stage. White ground (owner decision 2026-08-02, superseding the dark stage), with
     the dark column's background kept for the DEVICE SCREENS — a lit display is dark-cased
     whatever the page behind it does. Both come from docs/design/tokens.md (FR-014). */
  --color-hero-white: #FBFDFD;
  --color-hero-bg: #0B1113;
  --color-hero-accent: #2BD4DB;

  /* Typography — system stack only; the apps use SF and Roboto for the same reason */
  --font-family: -apple-system, BlinkMacSystemFont, system-ui, "Segoe UI", Roboto,
    Helvetica, Arial, sans-serif;
  /* One characterful face, self-hosted so the zero-third-party promise holds. Display only —
     body stays native, matching the apps' "native fonts" rule in tokens.md. */
  --font-display: "Archivo", var(--font-family);

  --type-hero-size: 60px;
  --type-hero-line: 64px;
  --type-hero-weight: 700;

  --type-display-size: 34px;
  --type-display-line: 40px;
  --type-display-weight: 700;

  --type-title-size: 22px;
  --type-title-line: 28px;
  --type-title-weight: 600;

  --type-headline-size: 18px;
  --type-headline-line: 24px;
  --type-headline-weight: 600;

  --type-body-size: 16px;
  --type-body-line: 24px;
  --type-body-weight: 400;

  --type-label-size: 13px;
  --type-label-line: 16px;
  --type-label-weight: 500;

  --type-caption-size: 12px;
  --type-caption-line: 16px;
  --type-caption-weight: 400;

  /* Spacing — 4-pt base */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 12px;
  --space-lg: 16px;
  --space-xl: 24px;
  --space-2xl: 32px;
  --space-3xl: 48px;
  --space-4xl: 80px;

  /* Radius */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 28px;
  --radius-pill: 999px;

  /* Motion — calm, precise, quick; never flashy */
  --motion-quick: 120ms;
  --motion-standard: 220ms;
  --motion-standard-easing: cubic-bezier(0.2, 0, 0, 1);
  --motion-emphasized: 380ms;
  --motion-emphasized-easing: cubic-bezier(0.3, 0, 0, 1);

  /* Depth — website only; the apps use native elevation */
  --shadow-soft: 0 1px 2px rgba(14, 20, 22, 0.04), 0 4px 12px rgba(14, 20, 22, 0.06),
    0 16px 32px rgba(14, 20, 22, 0.05);
  --shadow-lifted: 0 2px 4px rgba(14, 20, 22, 0.08), 0 12px 24px rgba(14, 20, 22, 0.10),
    0 32px 64px rgba(14, 20, 22, 0.12);
}

/* Border-box everywhere so layout maths stays predictable once borders appear. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

body,
h1,
h2,
h3,
p,
figure,
ul {
  margin: 0;
}

body {
  /* Decorative glows and rotated device frames must never be able to widen the page.
     clip (not hidden) so it can't accidentally create a scroll container. */
  overflow-x: clip;
  background-color: var(--color-background);
  color: var(--color-on-background);
  font-family: var(--font-family);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
  font-weight: var(--type-body-weight);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

h1 {
  font-size: var(--type-display-size);
  line-height: var(--type-display-line);
  font-weight: var(--type-display-weight);
}

h2 {
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
  font-weight: var(--type-title-weight);
}

h3 {
  font-size: var(--type-headline-size);
  line-height: var(--type-headline-line);
  font-weight: var(--type-headline-weight);
}

/* Every accent-coloured TEXT uses accent-strong: plain accent is 3.8:1 on the ground, under
   WCAG 1.4.3's 4.5:1. Plain accent stays for marks that aren't text — rings, borders, ticks. */
a {
  color: var(--color-accent-strong);
  text-underline-offset: 0.1em;
}

/* Keyboard focus only — a mouse click shouldn't ring. 2px is a literal because the token
   doc defines no border-width scale; a spacing token here would be an 8px ring. */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: var(--space-xs);
  border-radius: var(--radius-sm);
}

/* Off-screen but reachable by keyboard and assistive tech. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ONLY the skip link reveals itself on focus. Scoped to `a` deliberately: .visually-hidden is
   also worn by focusable radio inputs (the billing switch, the plan tickets), and an unscoped
   rule threw those into the top-left corner of the viewport as stray visible controls the moment
   they were selected. */
a.visually-hidden:focus {
  position: fixed;
  top: var(--space-md);
  left: var(--space-md);
  width: auto;
  height: auto;
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  overflow: visible;
  clip-path: none;
  white-space: normal;
  background-color: var(--color-surface-raised);
  color: var(--color-on-background);
  box-shadow: var(--shadow-lifted);
  border-radius: var(--radius-md);
  z-index: 100;
}



/* Self-hosted (assets/fonts/), SIL OFL — see Archivo-OFL.txt. swap so text is readable
   immediately and the face upgrades in place; never a blocking request. */
@font-face {
  font-family: "Archivo";
  src: url("/assets/fonts/archivo-latin.woff2") format("woff2");
  font-weight: 600 800;
  font-style: normal;
  font-display: swap;
}

/* ============================================================
   Shared chrome — header, nav, buttons, footer. Used by every
   page, so changes here are not homepage-local: the hero's dark
   variants live under .hero further down, scoped so the three
   document pages stay untouched.
   ============================================================ */

.site-header {
  position: relative;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  padding: var(--space-lg) var(--space-2xl);
  border-bottom: 1px solid var(--color-outline);
  background: var(--color-background);
}

.site-mark {
  display: flex;
  align-items: center;
  /* 44px is the comfortable-tap floor; the mark is only 24px of artwork, so the target has
     to be made rather than inherited. */
  min-height: 44px;
  gap: var(--space-sm);
  font-family: var(--font-display);
  font-size: var(--type-headline-size);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--color-ink);
  text-decoration: none;
}

.nav { display: flex; align-items: center; gap: var(--space-xl); }

/* Padded to the same 44px tap target as the footer links. */
.nav a:not(.btn) {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--color-text-secondary);
  text-decoration: none;
  font-size: var(--type-label-size);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  transition: color var(--motion-standard) var(--motion-standard-easing);
}

.nav a:not(.btn):hover { color: var(--color-ink); }

/* Buttons: flat and quiet. The boldness is spent on the seam, not here. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-xl);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: var(--type-body-size);
  font-weight: var(--type-label-weight);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--motion-standard) var(--motion-standard-easing),
    border-color var(--motion-standard) var(--motion-standard-easing),
    color var(--motion-standard) var(--motion-standard-easing);
}

.btn-primary { background: var(--color-ink); color: var(--color-background); }
.btn-primary:hover { background: var(--color-accent-strong); }

.btn-secondary { background: transparent; color: var(--color-ink); border-color: var(--color-outline); }
.btn-secondary:hover { border-color: var(--color-ink); }

/* ---- Shared type roles. Used by the hero AND the three document pages, so these live
   with the chrome rather than inside the hero block. (Deleting them with the old seam
   layout silently shrank every document page's heading — hence T005's regression gate.) */
.eyebrow {
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-strong);
}

.headline {
  font-family: var(--font-display);
  font-size: clamp(44px, 6.4vw, 104px);
  line-height: 0.92;
  letter-spacing: -0.045em;
  font-weight: 800;
  color: var(--color-ink);
  text-wrap: balance;
  margin: 0;
}

.headline em { font-style: normal; color: var(--color-text-secondary); }
/* The headline's full stop is the one place the accent appears in the type. */
.headline b { color: var(--color-accent-strong); font-weight: inherit; }

.lead {
  max-width: 40ch;
  font-size: clamp(16px, 1.2vw, 19px);
  line-height: 1.6;
  color: var(--color-text-secondary);
}

.actions { display: flex; gap: var(--space-md); flex-wrap: wrap; align-items: center; }

.meta {
  font-size: var(--type-caption-size);
  letter-spacing: 0.04em;
  color: var(--color-text-secondary);
}
/* A short link at the end of a wrapping caption ("how to join") should move as one piece,
   not break across two lines. */
.meta a { white-space: nowrap; }

/* ============================================================
   The hero — "The Escape" (spec 032)

   A full-bleed dark stage. Two screens sit inside it with real
   air between them, and the copy is overlaid on the stage rather
   than placed beside it (FR-027, FR-030).

   THE SCREENS ARE CSS; ONLY THE PARTICLES ARE CANVAS. That split
   is load-bearing, not stylistic: with scripting dead the hero
   must still show two displays with content crossing between
   them (contract C1, SC-010). Painting the screens onto the
   canvas would leave a blocked script showing an empty box —
   the exact failure spec 032 exists to fix.
   ============================================================ */

/* Grid, not absolute positioning: the copy and the scene each get their own row, so
   they cannot overlap however the type wraps or the viewport resizes. Positioning the
   scene absolutely meant guessing the copy's height, and the screens landed on top of
   the download button. */
.hero {
  position: relative;
  /* NO isolation. The particle canvas is a fixed, root-level element at z-index 2, so isolating
     the hero trapped .hero-copy's z-index 3 inside a context that itself sits at z-index auto —
     and particles painted straight over the headline. Without it the three layers compete in the
     root context in the order they need to: scene 1 < canvas 2 < copy 3. */
  overflow: hidden;
  /* Shorter than the old 88vh/850px: with the devices beside the copy instead of under it, the
     extra height only opened a dead band between the headline and the lead. */
  min-height: min(78vh, 720px);
  display: grid;
  grid-template-rows: auto 1fr;
  /* The browser keeps vertical scrolling on the hero, so a finger dragging to scroll past
     it always can. motion.js never calls preventDefault on move — between scrolling and
     pushing particles, scrolling wins (FR-019a). */
  touch-action: pan-y;
  /* White stage. Owner decision 2026-08-02, superseding the dark ground: the whole site is
     bright now, and the particles carry the colour instead of the background. */
  background:
    radial-gradient(120% 92% at 16% 6%, rgba(43, 212, 219, 0.10), transparent 56%),
    radial-gradient(105% 85% at 84% 96%, rgba(76, 94, 255, 0.08), transparent 60%),
    var(--color-hero-white);
  color: var(--color-on-background);
}

/* The particle field sits ABOVE the screens (z-index 2 > scene's 1) and below the copy
   (3). The particles are pixels ON the displays, so painting them under an opaque screen
   hides them entirely — which is exactly what z-index 0 did.

   Deaf to the pointer so it can never swallow a click meant for the download (FR-012,
   FR-024); motion.js listens on the hero itself, not on this canvas. */
.hero-field {
  position: fixed;
  inset: 0;
  z-index: 2;
  /* Explicit 100% — NOT auto, and NOT 100vw/100vh.
     `auto` is a trap: <canvas> is a replaced element, so its box would derive from its backing
     store, which this code sets from its box — a feedback loop that doubles the canvas every
     ResizeObserver tick until it hits the 33.5M-pixel cap.
     100vw/100vh are wrong too: 100vw includes the scrollbar, and 100vh is the LARGE viewport (the
     height with a mobile URL bar hidden), so on a phone the canvas was taller than the area it was
     painted into and the particles sat off-register from the screens they belong inside.
     100% of a fixed element's containing block is exactly the layout viewport, which is what
     `inset: 0` targets and what documentElement.clientWidth/Height report. */
  width: 100%;
  height: 100%;
  pointer-events: none;
}



/* ---- The scene: two screens and the air between them ---- */
/* The scene spans the stage, but its left padding is the copy column, so the devices start
   where the copy ends. They used to start under it, and at 900–1080px the lead ran over the
   lit laptop screen. align-items:center puts the tablet at the laptop's level rather than sunk
   below it. */
.hero-scene {
  grid-area: 1 / 1;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 4%;
  padding: 0 3vw 0 var(--hero-col);
  pointer-events: none;
}

/* ---- The laptop: lid, base and all. Shown whole rather than as a floating panel,
   because a screen with no body reads as an illustration of a screen. ---- */
/* 55 + 37 + the 4% gap leaves room for the base, which is 112% of the lid and so hangs 6% of
   the laptop's width out on each side. */
.laptop {
  flex: 0 0 auto;
  width: 55%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.laptop-lid {
  width: 100%;
  padding: 10px 10px 13px;
  border-radius: var(--radius-md) var(--radius-md) 3px 3px;
  background: linear-gradient(158deg, #2E3A3D, #141D20 46%, #0B1215);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.14),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    0 26px 60px rgba(0, 0, 0, 0.5);
}

/* Wider than the lid, so the base reads as tilting toward the viewer. */
/* Real height and a lit top edge. At 13px of near-black the base was technically present
   and visually absent, which left the lid looking like a floating panel. */
.laptop-base {
  position: relative;
  width: 112%;
  height: 20px;
  border-radius: 2px 2px 11px 11px;
  background: linear-gradient(180deg, #55666B 0 2px, #334246 2px, #1B2427 55%, #0D1417);
  box-shadow: 0 20px 34px rgba(0, 0, 0, 0.6);
}

/* The thumb notch on the front lip — the one detail that says "laptop" rather than "slab". */
.laptop-base span {
  position: absolute;
  left: 50%;
  top: 2px;
  width: 12%;
  height: 6px;
  border-radius: 0 0 var(--radius-pill) var(--radius-pill);
  background: rgba(0, 0, 0, 0.55);
  transform: translateX(-50%);
}

/* Bigger, and lifted by the height of the laptop's base so the two SCREENS line up rather
   than the two outlines. */
.tablet {
  flex: 0 0 auto;
  width: 37%;
  padding: 8px;
  margin-bottom: 20px;
  border-radius: var(--radius-lg);
  background: linear-gradient(158deg, #2B3639, #131C1E 46%, #0A1012);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.13),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    0 26px 60px rgba(0, 0, 0, 0.5);
}

.screen-glass {
  position: relative;
  overflow: hidden;
  /* A size container, so the screen art can respond to the screen it is on rather than to the
     viewport — the laptop is narrowest just above the 900px breakpoint, not below it. */
  container-type: inline-size;
  border-radius: 4px;
  aspect-ratio: 16 / 10;
  background:
    radial-gradient(120% 92% at 8% 0%,
      color-mix(in srgb, var(--color-glow-cyan) 46%, transparent), transparent 62%),
    linear-gradient(135deg,
      color-mix(in srgb, var(--color-accent) 62%, var(--color-hero-bg)),
      color-mix(in srgb, var(--color-aurora-indigo) 56%, var(--color-hero-bg)));
}

.tablet .screen-glass { border-radius: var(--radius-sm); aspect-ratio: 16 / 10.4; }

/* Glass, not paint — one raking highlight. */
.screen-glass::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(116deg,
    rgba(255, 255, 255, 0.12) 0%, transparent 34%,
    transparent 74%, rgba(255, 255, 255, 0.05) 100%);
}

/* The desktop inside each screen. A single contained region so a real build
   screenshot can replace it later without touching anything around it (FR-005). */
.desktop { position: absolute; inset: 0; }

/* ---- What the screens show: the brand, composed rather than a window mockup ---- */
.art {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.6%;
  text-align: center;
  padding: 6%;
}

.art-mark {
  width: clamp(38px, 5.6vw, 104px);
  height: auto;
  filter: drop-shadow(0 0 20px color-mix(in srgb, var(--color-feather-tip) 65%, transparent));
}

.art-word {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1;
  font-size: clamp(15px, 2.3vw, 42px);
  color: #FFFFFF;
}

/* Two weights in one wordmark: the lighter half keeps it from reading as a shout. */
.art-word span {
  font-weight: 500;
  color: color-mix(in srgb, var(--color-feather-tip) 82%, transparent);
}

.art-sub {
  font-size: clamp(9px, 0.86vw, 15px);
  letter-spacing: 0.06em;
  color: rgba(255, 255, 255, 0.62);
}

/* Under ~240px of glass the subline wraps and spills a lone word below the wordmark. */
@container (max-width: 240px) {
  .art-sub { display: none; }
}

.art-sm { gap: 3%; }
.art-sm .art-mark { width: clamp(26px, 3.2vw, 60px); }
.art-sm .art-word { font-size: clamp(11px, 1.4vw, 25px); }

/* ---- The tablet waking ----
   The CSS default is AWAKE. Only a live script puts the screen to sleep, so a hero with no
   scripting shows two working displays and SC-010 still holds. Gated on .js, which is set
   before first paint, so there is no flash of a lit tablet before it goes dark. */
.js .tablet .desktop::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: #05080A;
  opacity: 1;
}

.js .tablet.is-waking .desktop::before {
  animation: wake 1.05s steps(1, end) forwards;
}

.js .tablet.is-awake .desktop::before {
  opacity: 0;
  transition: opacity 520ms var(--motion-standard-easing);
}

/* The stutter of a panel finding its signal — irregular on purpose; evenly spaced flashes
   read as a blinking cursor rather than as a screen coming up. */
@keyframes wake {
  0%   { opacity: 1; }
  9%   { opacity: 0.18; }
  15%  { opacity: 1; }
  29%  { opacity: 0.08; }
  36%  { opacity: 1; }
  51%  { opacity: 0.04; }
  57%  { opacity: 0.66; }
  71%  { opacity: 0; }
  77%  { opacity: 0.42; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  /* No flicker for anyone who asked for less motion — the tablet is simply on. */
  .js .tablet .desktop::before { opacity: 0; animation: none; }
}

/* Content mid-flight between the screens — the static statement of FR-002, so the
   crossing is visible in a still frame with no script running (SC-010).

   A flex item in the gap rather than an absolutely-positioned overlay: the screens'
   widths are percentages, so any hardcoded offset would drift the moment they change. */
.crossing {
  flex: 0 0 auto;
  align-self: center;
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-bottom: 12%;
  pointer-events: none;
}

.crossing i {
  display: block;
  border-radius: var(--radius-pill);
  background: var(--color-hero-accent);
  box-shadow: 0 0 18px color-mix(in srgb, var(--color-feather-tip) 65%, transparent);
}
.crossing i:nth-child(1) { width: 7px; height: 7px; opacity: 0.9; }
.crossing i:nth-child(2) { width: 5px; height: 5px; opacity: 0.65; }
.crossing i:nth-child(3) { width: 9px; height: 9px; opacity: 1; }
.crossing i:nth-child(4) { width: 4px; height: 4px; opacity: 0.5; }
.crossing i:nth-child(5) { width: 6px; height: 6px; opacity: 0.75; }

/* ---- The copy, overlaid on the stage ---- */
/* Flex column with a gap, because the base layer resets every margin to 0 — without
   this the eyebrow, headline, lead, actions and meta sit flush against each other. */
/* The headline band spans the full width, so the line can run horizontally. */
.hero-copy {
  position: relative;
  /* 4, not 3: .hero-say also sits at 3 and comes LATER in the DOM, so its scrim won a same-z
     tie-break and painted over the headline — "Your" was visibly washed out. */
  z-index: 4;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-md);
  padding: var(--space-2xl) 4vw var(--space-sm) 6vw;
}

/* The lead's type size is set here so that `ch` means the lead's character width in both the
   copy column and the scene's matching padding — a custom property's `ch` resolves wherever it
   is used, so the two would disagree if either had its own font size.
   --hero-measure is the lead's line length: ~36ch on a small laptop, ~44ch on a large screen.
   --hero-col is the whole copy column: page gutter + measure + the air before the devices. */
.hero-lower {
  --hero-measure: clamp(36ch, 32vw, 44ch);
  --hero-col: calc(6vw + var(--hero-measure) + var(--space-3xl));
  position: relative;
  display: grid;
  align-items: center;
  /* Below 1100px the CTAs stack and the copy outgrows the devices, filling the row; without
     this the lead butted against the headline and the last meta line sat ~20px above the band. */
  padding: var(--space-xl) 0 var(--space-3xl);
  font-size: clamp(16px, 1.2vw, 19px);
}

/* Lead and buttons stay in front of the devices. */
.hero-say {
  position: relative;
  z-index: 3;
  grid-area: 1 / 1;
  align-self: center;
  justify-self: start;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-lg);
  /* Exactly the column the scene leaves free (border-box: the gutter and the air are inside). */
  width: var(--hero-col);
  padding: 0 var(--space-3xl) 0 6vw;
}

/* A soft pool of the ground colour under the copy: the particle field is fixed to the viewport
   and passes behind the text, and this damps it there (FR-028). Feathered on all four sides by
   two intersected masks — a radial gradient left hard edges wherever the box cut it off, and the
   top and right edges drew seams across the stage. It stops at the column's edge, so it never
   reaches the devices. */
.hero-say::before {
  --scrim-feather: var(--space-3xl);
  content: "";
  position: absolute;
  /* Bounded in px: a percentage inset is a share of this block's own height, and grew into the
     headline above. The left edge sits off-screen, so only three edges are ever seen. */
  inset: calc(var(--scrim-feather) * -1) 0 calc(var(--scrim-feather) * -1) calc(var(--scrim-feather) * -1);
  z-index: -1;
  pointer-events: none;
  background: color-mix(in srgb, var(--color-hero-white) 90%, transparent);
  -webkit-mask-image:
    linear-gradient(to right, transparent, #000 var(--scrim-feather), #000 calc(100% - var(--scrim-feather)), transparent),
    linear-gradient(to bottom, transparent, #000 var(--scrim-feather), #000 calc(100% - var(--scrim-feather)), transparent);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to right, transparent, #000 var(--scrim-feather), #000 calc(100% - var(--scrim-feather)), transparent),
    linear-gradient(to bottom, transparent, #000 var(--scrim-feather), #000 calc(100% - var(--scrim-feather)), transparent);
  mask-composite: intersect;
}

/* A soft pool of the ground colour behind the headline. The stacking fix above is what keeps
   particles off the text; this is the second line of defence FR-028 asks for, and it was lost
   when the headline moved to its own full-width band. */
.hero-copy::before {
  content: "";
  position: absolute;
  /* Bounded in px, not %. A percentage inset resolves against THIS block's own height, so as the
     headline grows the scrim grows with it and spills into whatever sits below — which is exactly
     how it ended up painting over the first lines of the lead paragraph. Both vertical edges are
     now fixed distances that cannot reach a neighbour. */
  inset: -24px -10% 4px -8vw;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(60% 100% at 26% 50%,
    var(--color-hero-white) 0%,
    color-mix(in srgb, var(--color-hero-white) 78%, transparent) 52%,
    transparent 82%);
  /* The ellipse is still most of the way opaque where the box ends below the headline, which
     drew a hard line across the stage; fade the bottom out instead. */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - var(--space-3xl)), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - var(--space-3xl)), transparent);
}

.hero-copy .headline { margin-top: var(--space-xs); }
.hero-copy .meta { margin-top: calc(var(--space-xs) * -1); }

/* Horizontal, so the size is bounded by the LINE LENGTH rather than the column: at 5vw the
   sentence overflows a 1500px viewport. */
.hero .headline {
  font-size: clamp(30px, 4.2vw, 68px);
  line-height: 1.02;
  text-wrap: nowrap;
}

/* The one artistic flourish, spent on the word the whole page turns on. A clipped gradient
   plus a soft bloom behind it — restrained everywhere else so this reads. The gradient stops at
   indigo: it used to run on to glow-cyan, which is 1.8:1 on the white stage, so the word's tail
   failed contrast. The cyan lives in the bloom instead, where it carries no text. */
.hero .headline em {
  position: relative;
  font-style: normal;
  background: linear-gradient(96deg, var(--color-accent-strong), var(--color-aurora-indigo));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.hero .headline em::after {
  content: "";
  position: absolute;
  inset: 12% -14% 4% -6%;
  z-index: -1;
  pointer-events: none;
  border-radius: var(--radius-pill);
  background: radial-gradient(60% 100% at 50% 50%,
    color-mix(in srgb, var(--color-glow-cyan) 34%, transparent), transparent 72%);
  filter: blur(14px);
}

/* The column sets the measure (--hero-measure), so the lead just fills it. `pretty` keeps a
   lone "on Pro." off the last line. */
.hero .lead { max-width: none; text-wrap: pretty; }

/* The two CTAs share the row equally and, once the measure is too narrow for both, each takes a
   full row — straight edges either way, never a 184px button over a 193px one. Desktop only:
   stacked, the column is the full screen width and the shared narrow rules below apply. */
@media (min-width: 901px) {
  .hero-say .actions .btn { flex: 1 1 0; min-width: max-content; }
}

/* The copy arrives in two beats around the gather beginning: "Your desk" just before, the
   rest just after. Staged ONLY under .js, so with scripting dead it is simply present —
   FR-010 forbids hiding content behind an animation, and this is the one place on the page
   where that rule could be broken by accident.

   Desktop only. Below 901px the hero stacks, so the copy is not over the scene and the staging
   buys nothing — while holding the lead and the download at opacity 0 delayed the Largest
   Contentful Paint by about 3 s (spec 032 FR-012: motion must not delay reading or the
   download). motion.js still adds the beat classes there; with no rules they do nothing. */
@media (min-width: 901px) {
  .js .hero-copy > .eyebrow,
  .js .hero-say > *,
  .js .headline-a,
  .js .headline-b {
    opacity: 0;
    transform: translateY(16px);
    transition:
      opacity var(--motion-emphasized) var(--motion-emphasized-easing),
      transform var(--motion-emphasized) var(--motion-emphasized-easing);
  }

  /* Beat one: the eyebrow and the first half of the line. */
  .js .hero-copy.beat-1 > .eyebrow,
  .js .hero-copy.beat-1 .headline-a { opacity: 1; transform: none; }

  /* Beat two: the rest of the line, then the lead, buttons and meta in reading order. */
  .js .hero-copy.beat-2 .headline-a,
  .js .hero-copy.beat-2 .headline-b,
  .js .hero-say.beat-2 > * { opacity: 1; transform: none; }

  .js .hero-say.beat-2 > .actions { transition-delay: 130ms; }
  .js .hero-say.beat-2 > .meta { transition-delay: 240ms; }
}

@media (prefers-reduced-motion: reduce) {
  /* No staging at all: the reduced-motion visitor gets the copy immediately. */
  .js .hero-copy > .eyebrow,
  .js .hero-say > *,
  .js .headline-a,
  .js .headline-b { opacity: 1; transform: none; transition: none; }
}

/* On a light stage the shared light-column styles already apply; the hero adds nothing. */

/* The keyboard route into the particle field (FR-016, FR-019). A real control:
   focusable, visibly focused, and it does the same thing the pointer does. */
.hero-nudge {
  margin-top: var(--space-xl);
  min-height: 44px;
  padding: var(--space-sm) var(--space-md);
  border: 1px dashed var(--color-outline);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text-secondary);
  font-family: inherit;
  font-size: var(--type-caption-size);
  cursor: pointer;
  transition: color var(--motion-standard) var(--motion-standard-easing),
    border-color var(--motion-standard) var(--motion-standard-easing);
}
.hero-nudge:hover { color: var(--color-ink); border-color: var(--color-ink); }

/* motion.js un-hides the nudge after first paint, which pushed the scene down ~80px — a layout
   shift. Holding its box from the start removes the shift; visibility keeps it out of the
   accessibility tree and the tab order until then. Only where motion.js will reveal it: it never
   does for reduced-motion visitors, who would otherwise keep a permanent blank gap. */
@media (prefers-reduced-motion: no-preference) {
  .js .hero-nudge[hidden] { display: block; visibility: hidden; }
}

/* The header sits over the dark hero on this page only. Scoped with a modifier so
   the three sibling pages keep the light header untouched (FR-031). */

/* The edge where the dark stage meets the light page — deliberate, not accidental
   (FR-031). */
.hero + .facts { border-top: 0; }

/* ---- The first scroll: a statement band ----
   Pure CSS transform on a doubled track, so it costs one composited layer and no script. The
   claim is the wedge: competitors sell trust because you are subscribing or buying hardware;
   this says there is nothing to trust us with. */
.band {
  overflow: hidden;
  padding: var(--space-2xl) 0;
  border-top: 1px solid var(--color-outline);
  background: var(--color-ink);
}

.band-track {
  display: flex;
  width: max-content;
  animation: band-scroll 38s linear infinite;
}

.band-run {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  padding-right: var(--space-lg);
  font-family: var(--font-display);
  font-size: clamp(28px, 4.6vw, 66px);
  font-weight: 800;
  letter-spacing: -0.035em;
  white-space: nowrap;
  color: var(--color-background);
}

/* The separators carry the accent so the line reads as rhythm rather than a wall of words. */
.band-run i { font-style: normal; color: var(--color-glow-cyan); }

/* -50% because the track holds exactly two identical runs: at half its width the second run
   sits precisely where the first began, so the loop has no seam. */
@keyframes band-scroll {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-50%, 0, 0); }
}

/* Motion that runs past five seconds must be stoppable by anyone, not only by readers who set
   reduced motion (WCAG 2.2.2): pointing at the band holds it still so it can be read. */
.band:hover .band-track { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  /* Static, and only the first run is needed — the second exists to hide the seam. */
  .band-track { animation: none; }
  .band-track .band-run:nth-child(2) { display: none; }
}

/* ---- The mark beats once when you touch it ----
   It is a wing. The app's own logo carries a `flap` parameter (Brand.kt) and this SVG is that
   geometry frozen at flap=0.5, so a wingbeat on hover is the mark doing what it already knows
   how to do. Transform only: no repaint, no layout. */
.wing {
  transform-origin: 60% 80%;
  transition: transform var(--motion-standard) var(--motion-standard-easing);
}

.site-mark:hover .wing,
.site-mark:focus-visible .wing { animation: wingbeat 620ms var(--motion-emphasized-easing); }

@keyframes wingbeat {
  0%   { transform: rotate(0deg) scale(1); }
  22%  { transform: rotate(-13deg) scale(1.1); }
  46%  { transform: rotate(7deg) scale(0.96); }
  72%  { transform: rotate(-4deg) scale(1.03); }
  100% { transform: rotate(0deg) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .site-mark:hover .wing,
  .site-mark:focus-visible .wing { animation: none; }
}

/* ============================================================
   Below the hero. The page used to run hero -> three text cards
   -> footer, which left anyone convinced by scrolling with
   nothing to act on and no idea what actually happens.
   ============================================================ */

.steps {
  padding: var(--space-4xl) 6vw var(--space-3xl);
  border-top: 1px solid var(--color-outline);
}

.steps-head { max-width: 46ch; margin-bottom: var(--space-3xl); }

.steps-head h2 {
  margin-top: var(--space-sm);
  font-family: var(--font-display);
  font-size: clamp(28px, 3.4vw, 46px);
  line-height: 1.04;
  letter-spacing: -0.035em;
  font-weight: 800;
  color: var(--color-ink);
}

.steps-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3xl);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}

/* The rule above each step, with the number sitting on it — the three read as points along
   one line rather than as three unrelated cards. */
.step {
  counter-increment: step;
  padding-top: var(--space-xl);
  border-top: 2px solid var(--color-outline);
}

.step::before {
  content: counter(step, decimal-leading-zero);
  display: block;
  margin-bottom: var(--space-md);
  font-family: var(--font-display);
  font-size: var(--type-label-size);
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--color-accent-strong);
  font-variant-numeric: tabular-nums;
}

.step h3 {
  margin-bottom: var(--space-sm);
  font-family: var(--font-display);
  font-size: var(--type-title-size);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--color-ink);
}

.step p { color: var(--color-text-secondary); }

.steps-more {
  margin-top: var(--space-3xl);
  font-size: var(--type-label-size);
  color: var(--color-text-secondary);
}

/* ---- Pricing, as tickets ----
   The notch is a real stub edge cut with radial-gradients rather than an image, so it costs
   nothing and scales. The FAQ shares the section frame and head, so the two read as one
   rhythm down the page. */
.pricing,
.faq {
  padding: var(--space-4xl) 6vw var(--space-3xl);
  border-top: 1px solid var(--color-outline);
}

.pricing-head,
.faq-head { max-width: 52ch; margin-bottom: var(--space-3xl); }

.pricing-head h2,
.faq-head h2 {
  margin-top: var(--space-sm);
  font-family: var(--font-display);
  font-size: clamp(28px, 3.4vw, 46px);
  line-height: 1.04;
  letter-spacing: -0.035em;
  font-weight: 800;
  color: var(--color-ink);
}

.pricing-sub { margin-top: var(--space-lg); color: var(--color-text-secondary); }

/* ---- Billing switch ----
   Pure CSS on native radio inputs: no script, arrow-key navigation for free, and it still
   works if motion.js never loads. `:has()` reads the checked input from the section above. */
.billing {
  border: 1px solid var(--color-outline);
  min-inline-size: 0;
  display: inline-flex;
  margin: var(--space-xl) 0 0;
  padding: var(--space-xs);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
}

/* Inline-flex to a 44px tap target, like the header and footer links. */
.billing label {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--space-sm) var(--space-xl);
  border-radius: var(--radius-pill);
  font-size: var(--type-label-size);
  font-weight: 500;
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: background var(--motion-standard) var(--motion-standard-easing),
    color var(--motion-standard) var(--motion-standard-easing);
}

.billing label:hover { color: var(--color-ink); }

.billing input:checked + label {
  background: var(--color-ink);
  color: var(--color-background);
}

/* The input is visually hidden, so the ring has to be drawn on its label instead — otherwise
   a keyboard user cannot see where they are. */
.billing input:focus-visible + label {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Yearly is the default, so the monthly figures start hidden. The "Best value" flag carries
   .per-year too, so it marks the yearly plan and vanishes on monthly. */
.per-month { display: none; }

.pricing:has(#bill-month:checked) .per-year { display: none; }
.pricing:has(#bill-month:checked) .per-month { display: inline; }

.tickets {
  /* A fieldset, so the group has an accessible name. Its UA border/padding/margin must go or the
     grid inherits a legend box and inset padding. */
  border: 0;
  padding: 0;
  margin: 0;
  min-inline-size: 0;   /* fieldsets default to min-content, which stops grid children shrinking */
  display: grid;
  /* Two plans, Free and Pro. Capped so a wide window doesn't stretch each card to half the page;
     left-aligned under the left-aligned heading. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  max-width: 58rem;
  gap: var(--space-xl);
  /* stretch, not start: Pro carries five list items and Free three, so left alone the cards are
     different heights before anything is selected. */
  align-items: stretch;
  margin-top: var(--space-xl);
}

/* Selectable. The plan name is the radio's label, stretched over the whole card (see
   .ticket-name::after), so a click anywhere on the card picks that plan, arrow keys move
   between them, and none of it needs a script. */
.ticket {
  position: relative;
  display: block;
  cursor: pointer;
  transition:
    transform var(--motion-standard) var(--motion-standard-easing),
    box-shadow var(--motion-standard) var(--motion-standard-easing),
    border-color var(--motion-standard) var(--motion-standard-easing);
  padding: var(--space-2xl) var(--space-xl);
  border: 1px solid var(--color-outline);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
  /* The stub notch. Measured from the TOP, not as a percentage: the cards are different
     heights, so a percentage put each notch somewhere else and the row looked accidental. */
  --notch: 13px;
  --notch-y: 168px;
  -webkit-mask:
    radial-gradient(var(--notch) at 0 var(--notch-y), transparent 98%, #000) left / 51% 100% no-repeat,
    radial-gradient(var(--notch) at 100% var(--notch-y), transparent 98%, #000) right / 51% 100% no-repeat;
  mask:
    radial-gradient(var(--notch) at 0 var(--notch-y), transparent 98%, #000) left / 51% 100% no-repeat,
    radial-gradient(var(--notch) at 100% var(--notch-y), transparent 98%, #000) right / 51% 100% no-repeat;
}

/* The tier most people should pick, marked once and quietly. This is the RECOMMENDATION and
   is independent of which card the visitor has selected. */
.ticket-pick {
  border-color: var(--color-accent);
  box-shadow: var(--shadow-soft);
}

/* A hint that the card responds, before anything is chosen. */
.ticket:hover { border-color: var(--color-accent); }

/* The chosen plan lifts above the others. `transform` rather than width or padding, so the
   grid never reflows and the two unchosen cards do not shift on the page.

   Scoped through .tickets deliberately: the scroll-reveal rule `.js [data-reveal].is-in` sets
   `transform: none` at specificity (0,3,0), which beat a bare `.ticket:has(...)` at (0,2,1) and
   silently cancelled the scale. Both rules animate `transform` on the same element, so one had
   to win — this makes it the selection. */
.tickets .ticket:has(input:checked) {
  z-index: 2;
  transform: scale(1.045);
  border-color: var(--color-accent);
  box-shadow: var(--shadow-lifted);
}

/* The radio is visually hidden, so the focus ring has to be drawn on the card — and drawn
   INSIDE it, because the stub mask clips anything outside the box, an outward ring included.
   Inset by 8px so it reads as its own line, clear of .ticket-pick's 1px accent border. */
.ticket:has(input:focus-visible) {
  outline: 2px solid var(--color-accent-strong);
  outline-offset: calc(var(--space-xs) * -2);
}

@media (prefers-reduced-motion: reduce) {
  /* The lift still happens — it is state, not decoration — but it does not animate. */
  .ticket { transition: none; }
}

/* Inside the card, not straddling its edge: the stub mask clips anything outside the box, so
   an overhanging flag gets sliced in half. */
.ticket-flag {
  position: absolute;
  top: var(--space-lg);
  right: var(--space-xl);
  padding: var(--space-xs) var(--space-md);
  border-radius: var(--radius-pill);
  background: var(--color-accent-strong);
  color: var(--color-on-accent);
  font-size: var(--type-caption-size);
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.ticket-name {
  display: block;
  font-size: var(--type-label-size);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-strong);
  cursor: pointer;
}

/* Stretches the label over the whole card. The card's mask makes it a stacking context, so
   z-index 1 stays inside the card while still sitting above the positioned list items. */
.ticket-name::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

.ticket-price {
  margin-top: var(--space-sm);
  font-family: var(--font-display);
  letter-spacing: -0.04em;
  color: var(--color-ink);
}

.ticket-price b { font-size: clamp(38px, 3.6vw, 52px); font-weight: 800; }
/* The unit only (`b + span`): the .per-year/.per-month wrappers are spans too, and a bare
   `span` greyed the whole Pro price while Free's $0 stayed in ink. */
.ticket-price b + span { font-size: var(--type-body-size); font-weight: 500; color: var(--color-text-secondary); }

.ticket-period {
  margin-top: var(--space-xs);
  font-size: var(--type-label-size);
  color: var(--color-text-secondary);
}

/* Below the notch: the stub, where what you get is listed. */
.ticket-list {
  margin-top: var(--space-3xl);
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--space-sm);
  font-size: var(--type-label-size);
  line-height: 1.5;
  color: var(--color-text-secondary);
}

.ticket-list li { padding-left: var(--space-xl); position: relative; }

.ticket-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.42em;
  width: 9px;
  height: 5px;
  border-left: 1.5px solid var(--color-accent);
  border-bottom: 1.5px solid var(--color-accent);
  transform: rotate(-45deg);
}

.pricing-note {
  max-width: 62ch;
  margin-top: var(--space-2xl);
  font-size: var(--type-label-size);
  color: var(--color-text-secondary);
}

/* ---- FAQ: open on the page, not hidden in accordions — the answers are the point, and
   collapsed text is text a reader has to go looking for. Ruled like the troubleshooting
   entries, because it is the same kind of lookup. ---- */
.faq-list { max-width: 72ch; }

.faq-item {
  padding: var(--space-xl) 0;
  border-top: 1px solid var(--color-outline);
}
.faq-item:last-child { border-bottom: 1px solid var(--color-outline); }

.faq-item h3 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--color-ink);
}

.faq-item p {
  margin-top: var(--space-sm);
  color: var(--color-text-secondary);
}

/* The Pro marker. Used beside a fact key on the homepage and beside headings on the setup and
   troubleshooting pages, so it is deliberately not scoped to either. */
.tag {
  display: inline-block;
  margin-left: var(--space-sm);
  padding: 2px var(--space-sm);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--color-accent) 14%, transparent);
  color: var(--color-accent-strong);
  font-family: var(--font-family);
  font-size: var(--type-caption-size);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: none;
  vertical-align: middle;
}

/* ---- The closing action ---- */
.closer {
  padding: var(--space-4xl) 6vw;
  border-top: 1px solid var(--color-outline);
  background:
    radial-gradient(70% 120% at 50% 0%,
      color-mix(in srgb, var(--color-glow-cyan) 12%, transparent), transparent 70%),
    var(--color-surface);
  text-align: center;
}

.closer-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-lg);
  max-width: 60ch;
  margin: 0 auto;
}

.closer h2 {
  font-family: var(--font-display);
  font-size: clamp(28px, 3.6vw, 50px);
  line-height: 1.04;
  letter-spacing: -0.035em;
  font-weight: 800;
  color: var(--color-ink);
  text-wrap: balance;
}

.closer p { color: var(--color-text-secondary); }
.closer .actions { justify-content: center; }

/* ---- The small diagram above each fact ---- */
.fact-art {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  height: 48px;
  margin-bottom: var(--space-xl);
  color: var(--color-accent);
}

.fact-art svg { width: 120px; height: 48px; overflow: visible; }

/* A price with a line through it: the claim IS that there isn't one. */
.price-tag {
  position: relative;
  font-family: var(--font-display);
  font-size: 38px;
  /* Without this it inherits the 24px body line box and the 52–92px lead "$0" spills out of it,
     sitting closer to the rule above than the other facts' art does. */
  line-height: 1;
  font-weight: 800;
  letter-spacing: -0.04em;
  color: var(--color-ink);
}

.price-note {
  margin-left: calc(var(--space-xs) * -1);
  font-size: var(--type-label-size);
  letter-spacing: 0.04em;
  color: var(--color-text-secondary);
}

@media (max-width: 900px) {
  /* The bento collapses to one column: two unequal columns at phone width is just two narrow
     columns. */
  .bento { grid-template-columns: 1fr; grid-template-rows: none; }
  .bento .fact-lead { grid-row: auto; }
  .band { padding: var(--space-xl) 0; }
  .steps { padding: var(--space-3xl) var(--space-lg) var(--space-2xl); }
  .steps-list { grid-template-columns: 1fr; gap: var(--space-2xl); }
  .steps-head { margin-bottom: var(--space-2xl); }
  .closer { padding: var(--space-3xl) var(--space-lg); }
  .pricing,
  .faq { padding: var(--space-3xl) var(--space-lg) var(--space-2xl); }
  .tickets { grid-template-columns: 1fr; gap: var(--space-lg); }
  .pricing-head,
  .faq-head { margin-bottom: var(--space-2xl); }
}

/* The pressure curve swells as if a pen were pressing harder. A CSS animation, not SMIL: an
   <animate> child ignores prefers-reduced-motion and no stylesheet rule can stop it, whereas
   this one is halted by the global reduced-motion reset further down. */
.fact-art .pressure {
  stroke-width: 1.5;
  animation: pressure 4s var(--motion-standard-easing) infinite;
}

@keyframes pressure {
  50% { stroke-width: 7; }
}

/* ---- Facts, set like a spec sheet rather than marketing cards ---- */
.facts {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--color-outline);
}

/* Not an equal grid: the price is the claim the competition cannot answer, so it gets the
   space. Two rows, with the lead card holding the left column full height. */
.bento {
  grid-template-columns: 1.05fr 1fr;
  grid-template-rows: auto auto;
}

.bento .fact-lead {
  grid-row: 1 / 3;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background:
    radial-gradient(80% 90% at 20% 15%,
      color-mix(in srgb, var(--color-glow-cyan) 10%, transparent), transparent 68%);
}

/* The lead card carries the argument, so its type steps up with it. */
.bento .fact-lead .price-tag { font-size: clamp(52px, 6vw, 92px); }
.bento .fact-lead .fact-art { height: auto; margin-bottom: var(--space-2xl); }
.bento .fact-lead h2 { font-size: var(--type-display-size); line-height: var(--type-display-line); }
.bento .fact-lead p { max-width: 34ch; }

.fact {
  padding: var(--space-3xl) var(--space-2xl);
  border-right: 1px solid var(--color-outline);
}
.fact:last-child { border-right: 0; }

/* The bento's right column is two stacked cells, so a column rule there would only run down the
   window edge. The outer cells take the page's 6vw gutter, so "$0" lines up with every other
   section heading instead of jumping out to 32px. Desktop only: stacked, .fact's own narrow
   padding already matches the phone gutter. */
@media (min-width: 901px) {
  .bento .fact:not(.fact-lead) { border-right: 0; padding-right: 6vw; }
  .bento .fact-lead { padding-left: 6vw; }
}

.fact h2 {
  font-family: var(--font-display);
  font-size: var(--type-title-size);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin-bottom: var(--space-sm);
  color: var(--color-ink);
}

.fact p { color: var(--color-text-secondary); max-width: 34ch; }

.fact-key {
  display: block;
  margin-bottom: var(--space-lg);
  font-size: var(--type-caption-size);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-strong);
  font-variant-numeric: tabular-nums;
}

.site-footer {
  border-top: 1px solid var(--color-outline);
  padding: var(--space-2xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-md);
  color: var(--color-text-secondary);
  font-size: var(--type-caption-size);
}
.site-footer nav { display: flex; flex-wrap: wrap; gap: var(--space-sm); }

/* Padded to a 44px tap target. These are standalone navigation links, not links inside a
   sentence — prose links are exempt by convention and by WCAG 2.5.5. */
.site-footer a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-sm);
  color: var(--color-text-secondary);
  text-decoration: none;
}
.site-footer a:hover { color: var(--color-ink); }

/* ---- Narrow: the seam turns horizontal rather than disappearing ---- */
@media (max-width: 900px) {
  .facts { grid-template-columns: 1fr; }
  .fact { border-right: 0; border-bottom: 1px solid var(--color-outline); padding: var(--space-2xl) var(--space-lg); }
  .fact:last-child { border-bottom: 0; }
}

/* ---- The hero on narrow screens ----
   Everything above assumes room for a headline beside two devices. Below this width there
   isn't any, so the hero stops overlaying and simply stacks: line, words, devices. */
@media (max-width: 900px) {
  .hero {
    min-height: 0;
    grid-template-rows: auto auto;
  }

  /* Wrapping is mandatory here — nowrap plus body's overflow-x:clip would silently CUT the
     end of the sentence off rather than scroll, which is worse than either. */
  .hero .headline {
    font-size: clamp(30px, 8.4vw, 54px);
    text-wrap: balance;
    white-space: normal;
  }

  .headline-a,
  .headline-b { display: inline; }

  .hero-copy { padding: var(--space-2xl) var(--space-lg) var(--space-lg); }

  .hero-lower {
    display: flex;
    flex-direction: column;
    /* MUST be reset: the base rule centres on the cross axis, which in a column makes every
       child shrink to its content width — the scene collapsed to 50px and the laptop to 11px. */
    align-items: stretch;
    gap: var(--space-2xl);
    padding: 0;   /* .hero-copy's padding and the scene's own bottom padding frame the stack */
  }

  /* Stacked, the copy no longer sits over a lit screen, so the scrim has nothing to do. */
  /* stretch: the desktop rule's align-self:center, inside this flex column, shrank the copy to its
     content and centred it, so between 761 and 900px it sat ~40px right of the headline. */
  .hero-say {
    align-self: stretch;
    width: auto;
    padding: 0 var(--space-lg);
  }
  .hero-say::before { content: none; }
  .hero .lead { max-width: 42ch; }

  .hero-scene {
    justify-content: center;
    gap: 3%;
    padding: 0 var(--space-lg) var(--space-2xl);
  }

  /* 60 + 33 + 3, not 62 + 34 + 3: at 99% the base's 6% overhang ran off the left edge and was
     clipped at 768–900px. */
  .laptop { width: 60%; }
  .tablet { width: 33%; margin-bottom: 14px; }
  .laptop-lid { padding: 5px 5px 7px; }
  .tablet { padding: 4px; }
  .laptop-base { height: 11px; }

  .art { padding: 4%; gap: 3%; }
  .art-sub { display: none; }   /* unreadable at this size; the lead says it anyway */
}

@media (max-width: 760px) {
  .site-header { padding: var(--space-md) var(--space-lg); }
  .nav a:not(.btn) { display: none; }
  .site-footer { padding: var(--space-xl) var(--space-lg); }
  /* The links keep their padded 44px tap targets; pulling the row back by that padding lines
     the first link's text up with the copyright above it once the footer stacks. */
  .site-footer nav { margin-left: calc(var(--space-sm) * -1); }

  /* Phone width stacks the two buttons. As a column the widest one sets the width of both, so
     the stack has straight edges instead of two ragged ones. The nudge is not a CTA: it keeps
     its own size. */
  .actions { flex-direction: column; align-items: stretch; }
  .actions > .hero-nudge { align-self: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
}

/* ============================================================
   Ambient motion. All decorative — see motion.js. The default
   state of everything here is the VISIBLE state, so a blocked or
   failed script can never hide content.
   ============================================================ */

/* The light field sits behind the dark half and bleeds across the seam. */
/* Reveal: visible by default, offset only once JS has opted in. */
[data-reveal] { opacity: 1; }

.js [data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition:
    opacity var(--motion-emphasized) var(--motion-emphasized-easing),
    transform var(--motion-emphasized) var(--motion-emphasized-easing);
}

.js [data-reveal].is-in { opacity: 1; transform: none; }

.js [data-reveal]:nth-child(2) { transition-delay: 90ms; }
.js [data-reveal]:nth-child(3) { transition-delay: 180ms; }

/* Cursor sheen — the highlight follows the pointer via custom properties set in JS. */
[data-sheen] { position: relative; overflow: hidden; }

[data-sheen]::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(340px circle at var(--mx, 50%) var(--my, 50%),
    color-mix(in srgb, var(--color-accent) 12%, transparent), transparent 60%);
  transition: opacity var(--motion-standard) var(--motion-standard-easing);
}

[data-sheen]:hover::before { opacity: 1; }

.fact { transition: background var(--motion-standard) var(--motion-standard-easing); }
.fact:hover { background: color-mix(in srgb, var(--color-accent) 4%, transparent); }

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] { opacity: 1; transform: none; }
}

/* ============================================================
   Document pages — setup, troubleshooting, privacy.

   The homepage's seam is a vertical hairline where the Mac meets
   the tablet. Here the same line turns and runs DOWN the page as
   a rail the content hangs from, so these read as one continuous
   surface rather than a different site.

   Setup steps sit on the rail as numbered nodes because setup
   genuinely IS a sequence — you cannot pair before you install.
   Troubleshooting entries deliberately carry NO numbers: it is a
   lookup table, and numbering it would imply an order that isn't
   there and that nobody should follow top-to-bottom.
   ============================================================ */

.doc {
  max-width: 76ch;
  margin: 0 auto;
  padding: var(--space-4xl) var(--space-2xl);
}

.doc-head h1 {
  margin-top: var(--space-md);
  font-family: var(--font-display);
  font-size: clamp(34px, 4.4vw, 58px);
  line-height: 1.02;
  letter-spacing: -0.04em;
  font-weight: 800;
  color: var(--color-ink);
  text-wrap: balance;
}

.doc-lead {
  margin-top: var(--space-lg);
  max-width: 58ch;
  font-size: clamp(16px, 1.1vw, 18px);
  line-height: 1.65;
  color: var(--color-text-secondary);
}

/* Scoped to .doc-section so it can't fight the homepage's .facts spacing. */
.doc-section { margin-top: var(--space-4xl); }

.doc-section > h2 {
  font-family: var(--font-display);
  font-size: var(--type-display-size);
  line-height: var(--type-display-line);
  font-weight: 700;
  letter-spacing: -0.025em;
  color: var(--color-ink);
}

.doc-section > h2 + p,
.doc-section > h2 + .qa { margin-top: var(--space-lg); }
.doc-section p { color: var(--color-text-secondary); }
.doc-section p + p { margin-top: var(--space-lg); }

.doc-section a { text-decoration: underline; }

.doc-section ul {
  margin-top: var(--space-lg);
  padding-left: var(--space-xl);
  color: var(--color-text-secondary);
}
.doc-section li + li { margin-top: var(--space-sm); }

/* Inline literals — menu paths, filenames, toggles the user must find by name. */
.path {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
  padding: 0.12em 0.42em;
  border: 1px solid var(--color-outline);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-ink);
  /* Settings paths are long and will wrap. Without this the border is drawn once around the
     whole run, leaving each wrapped fragment with a torn open edge. */
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* ---- The rail ---- */
.doc-steps {
  position: relative;
  margin: var(--space-2xl) 0 0;
  padding: 0 0 0 52px;
  list-style: none;
  counter-reset: step;
}

/* The seam, turned vertical. It ignites at the top and fades to a plain
   hairline — the signal enters here and runs the length of the page. */
.doc-steps::before {
  content: "";
  position: absolute;
  left: 15px;
  top: var(--space-md);
  bottom: var(--space-md);
  width: 1px;
  background: linear-gradient(180deg, var(--color-accent), var(--color-outline) 24%);
}

.doc-step {
  position: relative;
  counter-increment: step;
  padding-bottom: var(--space-3xl);
}
.doc-step:last-child { padding-bottom: 0; }

.doc-step::before {
  content: counter(step);
  position: absolute;
  left: -52px;
  top: -3px;
  display: grid;
  place-items: center;
  width: 31px;
  height: 31px;
  border: 1px solid var(--color-outline);
  border-radius: var(--radius-pill);
  background: var(--color-surface-raised);
  color: var(--color-accent-strong);
  font-size: var(--type-caption-size);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.doc-step h3 {
  margin-bottom: var(--space-sm);
  font-family: var(--font-display);
  font-size: var(--type-title-size);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--color-ink);
}

/* ---- Peer cards: equal columns, not a ranked list ---- */
/* Equal peers get equal columns — auto-fit left a third card stranded on a row of its own,
   which reads as a ranking the content doesn't have. Three by default (the 404 page's three
   ways onward); .modes-2 for setup's two connection modes. */
.modes {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-lg);
  margin-top: var(--space-2xl);
}

.modes-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* Positioned so a heading link's ::after (below) can cover the whole card. */
.mode {
  position: relative;
  padding: var(--space-xl);
  border: 1px solid var(--color-outline);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
}

.mode h3 {
  margin-bottom: var(--space-sm);
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--color-ink);
}

.mode-when {
  display: block;
  margin-bottom: var(--space-lg);
  font-size: var(--type-caption-size);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-strong);
}

.mode p { font-size: var(--type-label-size); line-height: 1.55; }
.mode h3 a { color: inherit; }

/* Where a card's heading is a link (the 404 page), the whole card is the tap target, not just
   the words. Setup's cards have no heading links, so they stay plain. */
.mode h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* Prose following a card grid needs the grid's own rhythm, not a paragraph gap. */
.modes + p { margin-top: var(--space-2xl); }

/* ---- Troubleshooting: symptom, cause, fix. No numbers, by design. ---- */
.qa {
  padding: var(--space-2xl) 0;
  border-top: 1px solid var(--color-outline);
}
.qa:last-of-type { border-bottom: 1px solid var(--color-outline); }

.qa > h3 {
  font-family: var(--font-display);
  font-size: var(--type-title-size);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--color-ink);
}

.qa-row {
  display: grid;
  grid-template-columns: 76px minmax(0, 1fr);
  gap: var(--space-lg);
  margin-top: var(--space-lg);
}

.qa-label {
  font-size: var(--type-caption-size);
  line-height: var(--type-body-line);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-strong);
}

.qa-row p { margin: 0; }

/* ---- Privacy inventory: the claim is the table, so it gets to be the table ---- */
.collect {
  margin-top: var(--space-xl);
  border: 1px solid var(--color-outline);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-surface-raised);
}

.collect-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-lg) var(--space-xl);
  border-bottom: 1px solid var(--color-outline);
}
.collect-row:last-child { border-bottom: 0; }

/* Beats `.doc-section p + p`, which otherwise pushes every answer down by a line and
   breaks the row's alignment. Equal specificity, resolved by coming later in the file. */
.collect-row p,
.collect-row p + p { margin-top: 0; }

/* Through .collect-row for the same reason: a bare .collect-answer loses to `.doc-section p`,
   which silently greyed every answer. accent-strong and warning-strong, not success and
   warning: those two fail 4.5:1 at this 13px text size on the white table. */
.collect-row .collect-answer {
  font-weight: 600;
  font-size: var(--type-label-size);
  letter-spacing: 0.02em;
  color: var(--color-accent-strong);
  white-space: nowrap;
}
.collect-row .collect-answer[data-answer="some"] { color: var(--color-warning-strong); }

.note {
  margin-top: var(--space-xl);
  padding: var(--space-lg) var(--space-xl);
  border-left: 2px solid var(--color-accent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: color-mix(in srgb, var(--color-accent) 5%, transparent);
}
.note p { color: var(--color-text-secondary); }

/* Current page in the header nav — a state, not decoration. */
.nav a[aria-current="page"] { color: var(--color-ink); }

@media (max-width: 760px) {
  .doc { padding: var(--space-3xl) var(--space-lg); }
  .doc-steps { padding-left: 42px; }
  .doc-step::before { left: -42px; }
  .doc-steps::before { left: 15px; }
  .qa-row { grid-template-columns: 1fr; gap: var(--space-xs); }
  .modes,
  .modes-2 { grid-template-columns: 1fr; }
  /* Stacked, the answer belongs under its own label rather than floating right. */
  .collect-row { grid-template-columns: 1fr; gap: var(--space-xs); }
}
