/* Pulled — pulled.cards
   One stylesheet, no build step. The tokens are the app's (Sources/DesignSystem/DesignTokens.swift):
   the page radial, the four-rung elevation ramp, white text with dimmed steps, and a top-lit rim
   instead of outlines. The legal pages and the app door follow the system scheme (light is dark's
   negative); the home page is the 3D stage and is dark only (docs/design/DESIGN-SYSTEM.md §15).
   Geist carries the wordmark and the page. */

@font-face { font-family: "Geist"; src: url("/fonts/Geist-Regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }
@font-face { font-family: "Geist"; src: url("/fonts/Geist-Medium.woff2") format("woff2"); font-weight: 500; font-display: swap; }
@font-face { font-family: "Geist"; src: url("/fonts/Geist-SemiBold.woff2") format("woff2"); font-weight: 600; font-display: swap; }

:root {
  color-scheme: dark light;
  --page-top: #0E0F13; --page-mid: #0A0B0E; --page-foot: #06070A;
  --sheet: #111216; --stage: #131418; --raised: #1F1F23;
  --text: #FFFFFF; --text-2: rgba(255,255,255,.62); --text-3: rgba(255,255,255,.40);
  --rim-top: rgba(255,255,255,.14); --rim-bottom: rgba(255,255,255,.04);
  --hair: rgba(255,255,255,.08);
  --surface: #FFFFFF; --ink: #0A0A0A;
  --mark-front: #FFFFFF; --mark-rear: #2E3137;
  --cast: rgba(0,0,0,.55);
  --ease: cubic-bezier(.16,1,.3,1);
}
@media (prefers-color-scheme: light) {
  :root {
    --page-top: #ECEDF1; --page-mid: #E7E9ED; --page-foot: #E2E4E9;
    --sheet: #F4F5F8; --stage: #FAFAFC; --raised: #FFFFFF;
    --text: #17181C; --text-2: rgba(23,24,28,.66); --text-3: rgba(23,24,28,.46);
    --rim-top: rgba(20,22,28,.14); --rim-bottom: rgba(20,22,28,.04);
    --hair: rgba(20,22,28,.09);
    --surface: #17181C; --ink: #FFFFFF;
    --mark-front: #17181C; --mark-rear: #C9CBD3;
    --cast: rgba(20,22,28,.22);
  }
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
/* The over-scroll bounce paints the document's ground, not the fixed canvas, so a flick past either
   end shows a band with no field in it. */
html:has(body.home), body.home { overscroll-behavior: none; }
body {
  margin: 0; min-height: 100vh;
  font-family: "Geist", -apple-system, "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
  font-size: 17px; line-height: 1.55; letter-spacing: -0.005em;
  color: var(--text);
  background: radial-gradient(120% 90% at 50% -10%, var(--page-top) 0%, var(--page-mid) 55%, var(--page-foot) 100%) fixed;
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; text-decoration: none; }
a:focus-visible { outline: 2px solid var(--text); outline-offset: 5px; border-radius: 4px; }
p { margin: 0; }
h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.025em; line-height: 1.08; }

.wrap { width: min(1080px, calc(100% - 48px)); margin: 0 auto; }

/* Header: the app icon as it sits on the Home Screen, the wordmark, three links */
.top { min-height: 64px; display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
.brand { display: inline-flex; align-items: center; gap: 11px; font-weight: 600; font-size: 19px; letter-spacing: -0.025em; }
.brand picture { display: block; }
.brand img { width: 32px; height: 32px; border-radius: 22.4%; display: block;
  box-shadow: 0 1px 0 rgba(255,255,255,.12) inset, 0 6px 16px -6px var(--cast); }
.top nav { display: flex; align-items: center; gap: 26px; font-size: 14.5px; font-weight: 500; color: var(--text-2); }
.top nav a { transition: color .2s var(--ease); }
.top nav a:hover, .top nav a[aria-current="page"] { color: var(--text); }

/* The one filled control */
.btn { display: inline-flex; align-items: center; height: 48px; padding: 0 22px; border-radius: 999px;
  background: var(--surface); color: var(--ink); font-weight: 600; font-size: 15.5px; letter-spacing: -0.01em;
  transition: transform .18s var(--ease), opacity .18s var(--ease); }
.btn:hover { opacity: .92; }
.btn:active { transform: scale(.97); }
.quiet { font-size: 14.5px; color: var(--text-3); }

/* Legal pages and the app door: the stage's clothes without the stage. Dark only, the same
   ground, the same header (scrolling with the page here: a fixed header over a
   document needs a fade beneath it, and on iOS a fixed fade stops at the layout viewport while
   the text runs on under the toolbar, which read as a seam), and the rating plate for a footer. */
.page { color-scheme: dark; background: var(--ground);
  --page-top: #0E0F13; --page-mid: #0A0B0E; --page-foot: #06070A;
  --sheet: #111216; --stage: #131418; --raised: #1F1F23;
  --text: #FFFFFF; --text-2: rgba(255,255,255,.62); --text-3: rgba(255,255,255,.40);
  --rim-top: rgba(255,255,255,.14); --rim-bottom: rgba(255,255,255,.04); --hair: rgba(255,255,255,.08);
  --surface: #FFFFFF; --ink: #0A0A0A; --cast: rgba(0,0,0,.55);
  --gutter: max(28px, calc((100% - 1160px) / 2)); --ground: #0A0B0E; }
.page .doc { padding-top: 40px; }
.doc { padding: 40px 0 80px; max-width: 66ch; }
.doc .date { font-size: 14px; color: var(--text-3); }
.doc h1 { font-size: clamp(34px, 4.6vw, 48px); margin-top: 10px; }
.doc > p { margin-top: 18px; color: var(--text-2); }
.doc section { margin-top: 36px; }
.doc h2 { font-size: 21px; margin-bottom: 10px; }
.doc section p, .doc li { color: var(--text-2); }
.doc section p + p { margin-top: 12px; }
.doc ul { margin: 10px 0 0; padding-left: 20px; }
.doc a { color: var(--text); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--text-3); }
.tile { display: block; width: 96px; aspect-ratio: 1; filter: drop-shadow(0 18px 28px var(--cast)); }
.tile img { width: 100%; height: 100%; display: block; border-radius: 22.4%; }
.app-door { padding-top: 56px; }
.app-door .tile { margin-bottom: 30px; }
.app-door h1 { max-width: 16ch; }
.app-door .cta { margin-top: 30px; display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.app-door .cta a { text-decoration: none; color: var(--ink); }

.vh { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
[hidden] { display: none !important; }

/* Footer: every page ends in the rating plate (rules under the home block, shared with .page) */

@media (max-width: 820px) {
  .top nav { gap: 18px; font-size: 14px; }
}

/* ── Home: the 3D stage ─────────────────────────────────────────────────────────────────────────
   Dark only. The canvas (#gl, js/hero.js) is fixed behind the page; every section is one viewport
   and a snap stop, and a swipe moves exactly one stop. The hero paints a flat phone (.dphone) that
   the 3D phone takes over in place; the tour's phone is pinned, and its captions sit in the left
   column on a desktop and in a dock under the phone on a phone. The phone leaves the stage before
   the closing section: there, the badge and the QR are the page. */
html:has(body.home) { scroll-snap-type: y mandatory; }
.home {
  color-scheme: dark;
  --page-top: #0E0F13; --page-mid: #0A0B0E; --page-foot: #06070A;
  --sheet: #111216; --stage: #131418; --raised: #1F1F23;
  --text: #FFFFFF; --text-2: rgba(255,255,255,.72); --text-3: rgba(255,255,255,.52);
  --rim-top: rgba(255,255,255,.14); --rim-bottom: rgba(255,255,255,.04); --hair: rgba(255,255,255,.08);
  --surface: #FFFFFF; --ink: #0A0A0A; --cast: rgba(0,0,0,.55);
  --gutter: max(28px, calc((100% - 1200px) / 2));
  /* One ground for the stage: the body, the canvas's clear colour and the theme-color are this hex.
     The page radial is for the document pages; under the stage it only ever showed at the edges the
     canvas could not reach, as a seam. */
  --ground: #0A0B0E;
  background: var(--ground);
}
/* The canvas is as tall as the LARGE viewport and never changes size while the browser's chrome
   comes and goes, so its 3x drawing buffer is never reallocated mid-scroll (hero.js maps the
   composition to the part the browser shows). On iOS every fixed layer is clipped to the layout
   viewport anyway; the status bar and the toolbar zone are the browser's, painted from theme-color,
   which is why the page has one flat ground and the field fades out before either visible edge.
   It stays transparent until its first frame is complete, then fades in over the flat phone. */
.home :is(#first, #gl) { position: fixed; top: 0; left: 0; width: 100%; height: 100vh; height: 100lvh; z-index: 0; display: block; transform: translateZ(0); }
/* #first (js/first.js) draws the stage's field and hand before three.js arrives, from the same
   numbers and clock; #gl fades in over it on the same pixels and it stops. */
.home #gl { will-change: transform; opacity: 0; transition: opacity .5s var(--ease); }
html.gl-ready .home #gl { opacity: 1; }
:is(.home, .page) .top { position: fixed; top: 0; left: 0; right: 0; z-index: 3; height: 72px; min-height: 72px; flex-wrap: nowrap;
  padding: 0 max(var(--gutter), env(safe-area-inset-left)) 0 max(var(--gutter), env(safe-area-inset-right)); }
/* On the stage the header steps out of the way while you read down and returns as you scroll up
   (js/chrome.js), so copy never runs under it. Document pages keep a static header. */
.home .top { transition: transform .38s var(--ease), opacity .3s var(--ease); }
.home .top.away { transform: translateY(-100%); opacity: 0; }
.page .top { position: static; }
:is(.home, .page) .brand { font-size: 22px; gap: 12px; }
:is(.home, .page) .brand img { width: 36px; height: 36px; }
:is(.home, .page) .top nav { gap: 28px; font-size: 15px; }

/* The copy arrives with the stage: the entrance lives on the children, because the parent's opacity
   is driven per frame from the scroll position. */
@keyframes riseIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
.home .hero h1, .home .hero .lede, .home .hero .cta, .home .hero .slot,
.home .top .brand, .home .top nav { animation: riseIn .7s var(--ease) both; }
.home .top .brand { animation-delay: .02s; }
.home .top nav { animation-delay: .06s; }
.home .hero h1 { animation-delay: .04s; }
.home .hero .slot { animation-delay: .1s; }
/* With script, the phone rises in only once both its images are decoded (js/boot.js), whole. */
html.js:not(.phone-in) .home .hero .slot { animation: none; opacity: 0; }
html.js.phone-in .home .hero .slot { animation-delay: 0s; }
.home .hero .lede { animation-delay: .12s; }
.home .hero .cta { animation-delay: .2s; }

.home main { position: relative; z-index: 2; }
.home section { height: 100vh; height: 100svh; min-height: 540px; position: relative; scroll-snap-align: start; scroll-snap-stop: always;
  display: flex; align-items: center; padding: 72px var(--gutter) 28px; }
.home .copy { position: relative; z-index: 2; will-change: opacity, transform; }
/* No text-wrap: balance here: iOS 27 Safari crashed laying out the fixed caption dock with it (CoreText, 2026-09-23). */

.home .hero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; gap: 24px; }
.home .hero h1 { font-size: clamp(46px, 5.2vw, 76px); line-height: 1.0; letter-spacing: -0.04em; }
.home .hero .lede { margin-top: 24px; font-size: clamp(18px, 1.5vw, 21px); line-height: 1.45; color: var(--text-2); max-width: 30ch; }
.home .cta { margin-top: 36px; display: flex; align-items: center; gap: 18px; }

/* The first paint: the phone as two stacked images, sized by CSS alone, in the rectangle the 3D
   phone will occupy. */
.home .slot { position: relative; height: min(80svh, 800px); display: flex; align-items: center; justify-content: flex-end; padding-right: 4%; min-height: 0; }
.home .dphone { position: relative; height: 100%; aspect-ratio: 1350 / 2760; max-width: 100%; transition: opacity .45s var(--ease); }
.home .dphone img { position: absolute; display: block; }
/* the phone's face; js/first.js turns it to the stage's rest pose (the .dphone box stays the layout) */
.home .dface { position: absolute; inset: 0; transform-origin: 0 0; }
.home .dbezel { inset: 0; width: 100%; height: 100%; }
/* the display cut-out: x 72, y 70, 1206 x 2620 of the 1350 x 2760 bezel */
.home .dscreen { left: 5.333%; top: 2.536%; width: 89.333%; height: 94.928%; border-radius: 7% / 3.3%; object-fit: cover; background: #0B0C0F; }
/* the scan loop, started by js/first.js over the still (its first frame) and adopted by the stage */
.home .dloop { position: absolute; left: 5.333%; top: 2.536%; width: 89.333%; height: 94.928%; border-radius: 7% / 3.3%; object-fit: cover; opacity: 0; }
.home .dloop.on { opacity: 1; }
html.gl-ready .home .dphone { opacity: 0; }

/* The official badge is the entire download control: no extra surface or border around it. */
.home .badge { display: inline-flex; flex: none; margin: 0; padding: 0; text-decoration: none;
  border-radius: 7px; cursor: pointer; }
.home .badge .store-art { display: block; width: auto; height: 76px; }
.home .badge:focus-visible { outline: 2px solid var(--text); outline-offset: 5px; }
.home .qr { display: block; width: 76px; height: 76px; border-radius: 14px; background: #fff; padding: 7px; }

/* The tour: a kicker, a headline, one explaining line per stop. */
.home .step .copy { width: min(40%, 520px); }
.home .kicker { font-size: 12px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); margin-bottom: 18px; }
.home .step h2 { font-size: clamp(36px, 3.9vw, 58px); line-height: 1.02; letter-spacing: -0.035em; max-width: 13ch; }
.home .step .copy > p:last-child { margin-top: 20px; font-size: 18.5px; line-height: 1.5; color: var(--text-2); max-width: 38ch; }

/* The closing section: no phone; the hand deals back in above the headline, and the badge and a
   large QR are the page. It shares the last screen with the footer. */
.home .download { height: auto; min-height: 96vh; min-height: 96svh; padding-top: 34vh; padding-top: 34svh; padding-bottom: 10vh; justify-content: center; text-align: center; }
.home .download .copy { width: min(760px, 100%); margin: 0 auto; }
.home .download h2 { font-size: clamp(40px, 4.6vw, 66px); line-height: 1.02; letter-spacing: -0.04em; }
.home .download .copy > p:not(.fine):not(.badge) { margin-top: 18px; color: var(--text-2); font-size: 20px; }
.home .download .cta { margin-top: 44px; justify-content: center; gap: 24px; }
.home .download .qr { width: 200px; height: 200px; padding: 16px; border-radius: 26px; }
.home .download .fine { margin-top: 36px; font-size: 13.5px; color: var(--text-3); }

/* The footer is the instrument's rating plate: one rung up the elevation ramp, drawn by the light
   along its top edge (the app's rim law, §5.1), with tracked micro labels over their values. */
:is(.home, .page) footer { position: relative; z-index: 2; scroll-snap-align: end; display: block; padding: 52px var(--gutter) 40px;
  pointer-events: auto; }
:is(.home, .page) footer::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.16) 18%, rgba(255,255,255,.16) 82%, transparent); }
:is(.home, .page) .plate { display: flex; align-items: flex-start; justify-content: space-between; gap: 48px; flex-wrap: wrap; }
:is(.home, .page) footer .brand { font-size: 19px; gap: 11px; }
:is(.home, .page) footer .brand img { width: 30px; height: 30px; }
:is(.home, .page) footer dl { margin: 0; display: flex; gap: 64px; flex-wrap: wrap; }
:is(.home, .page) footer dl div { display: flex; flex-direction: column; gap: 7px; }
:is(.home, .page) footer dt { font-size: 10px; font-weight: 700; letter-spacing: .10em; text-transform: uppercase; color: var(--text-3); margin-bottom: 3px; }
:is(.home, .page) footer dd { margin: 0; font-size: 14.5px; font-weight: 500; color: var(--text-2); }
:is(.home, .page) footer dd a { transition: color .2s var(--ease); }
:is(.home, .page) footer dd a:hover { color: var(--text); }
:is(.home, .page) footer .legal { margin-top: 44px; padding-top: 20px; border-top: 1px solid var(--hair);
  display: flex; justify-content: space-between; gap: 24px; flex-wrap: wrap; font-size: 12.5px; color: var(--text-3); line-height: 1.5; }
:is(.home, .page) footer .legal p { max-width: 82ch; }

/* No WebGL: the canvas hides and the flat phone stays. */
html.no-gl .home #gl { display: none; }

@media (max-aspect-ratio: 9/10) {
  /* Support stays in the phone header; Privacy and Terms stay in the plate. */
  :is(.home, .page) .top { padding: 0 20px; height: 64px; min-height: 64px; gap: 12px; }
  :is(.home, .page) .top nav { gap: 16px; font-size: 14px; }
  :is(.home, .page) .top nav a:not([href="/support/"]) { display: none; }
  :is(.home, .page) .brand { font-size: 20px; }
  :is(.home, .page) .brand img { width: 32px; height: 32px; }
  .home { --gutter: 20px; }
  /* svh, not dvh: a section that changes height while the browser chrome retracts moves the snap
     points under the finger, which is what read as glitchy scrolling. */
  .home section { height: 100svh; min-height: 568px; }
  /* The hero centres on a phone: the copy above, the flat phone (then the 3D one) below it. */
  .home .hero { display: flex; flex-direction: column; align-items: stretch; text-align: center; gap: 0; padding-top: calc(64px + 2svh); padding-bottom: 18px; }
  .home .hero h1 { font-size: clamp(34px, 9.6vw, 46px); }
  .home .hero .lede { font-size: 16.5px; margin: 14px auto 0; max-width: 30ch; }
  .home .hero .cta { justify-content: center; margin-top: 20px; }
  .home .qr { display: none; }                              /* on a phone the badge is the door */
  .home .hero .badge .store-art { height: 56px; }
  .home .slot { flex: 1; height: auto; margin-top: 18px; justify-content: center; padding-right: 0; }
  .home .dphone { height: 100%; max-width: 62vw; }
  /* Tour captions sit in a dock under the phone; the scroll slides them through it (hero.js).
     Without script they stay in the page's flow at the bottom of their stop. */
  .home .step { align-items: flex-end; }
  .home .step .copy { width: 100%; }
  .home .step .copy.docked { position: fixed; left: 20px; right: 20px; bottom: max(22px, env(safe-area-inset-bottom)); width: auto; }
  .home .kicker { margin-bottom: 10px; font-size: 11px; }
  .home .step h2 { font-size: clamp(27px, 7.6vw, 36px); max-width: none; }
  .home .step .copy > p:last-child { margin-top: 10px; font-size: 15.5px; max-width: none; }
  .home .download { min-height: 0; padding-top: calc(64px + 30svh); padding-bottom: 8svh; }
  .home .download h2 { font-size: clamp(32px, 9vw, 42px); }
  .home .download .copy > p:not(.fine):not(.badge) { font-size: 16.5px; }
  .home .download .cta { margin-top: 30px; }
  .home .download .badge .store-art { height: 72px; }
  .home .download .fine { margin-top: 20px; font-size: 12.5px; }
  :is(.home, .page) footer { padding: 28px max(20px, env(safe-area-inset-right)) calc(24px + env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); }
  :is(.home, .page) .plate { flex-direction: column; gap: 22px; }
  :is(.home, .page) footer dl { gap: 22px 40px; }
  :is(.home, .page) footer .legal { margin-top: 22px; padding-top: 16px; flex-direction: column; gap: 8px; }
}
/* Portrait tablets (and tall narrow windows 600 px and up) keep the stacked layout, but at a size
   for the screen: desktop type and store door, the QR beside the badge (someone on an iPad scans it
   with their phone), a phone that leaves the copy room, and captions centred under it. */
@media (max-aspect-ratio: 9/10) and (min-width: 600px) {
  :is(.home, .page) .top { padding: 0 32px; height: 72px; min-height: 72px; }
  :is(.home, .page) .top nav { gap: 24px; font-size: 15px; }
  :is(.home, .page) .top nav a:not([href="/support/"]) { display: inline; }
  .home { --gutter: 32px; }
  .home .hero { padding-top: calc(72px + 5svh); padding-bottom: 36px; }
  .home .hero h1 { font-size: clamp(52px, 7.6vw, 84px); }
  .home .hero .lede { font-size: 20px; margin-top: 20px; max-width: 36ch; }
  .home .hero .cta { margin-top: 30px; gap: 16px; }
  .home .qr { display: block; }
  .home .hero .badge .store-art { height: 76px; }
  .home .slot { margin-top: 40px; }
  .home .dphone { max-height: 60svh; }
  .home .step .copy.docked { left: max(32px, calc(50% - 330px)); right: max(32px, calc(50% - 330px)); bottom: max(44px, env(safe-area-inset-bottom)); text-align: center; }
  .home .kicker { margin-bottom: 14px; font-size: 12px; }
  .home .step h2 { font-size: clamp(40px, 6vw, 58px); }
  .home .step .copy > p:last-child { margin-top: 14px; font-size: 19px; }
  .home .download { padding-top: calc(72px + 28svh); }
  .home .download h2 { font-size: clamp(46px, 6.6vw, 66px); }
  .home .download .copy > p:not(.fine):not(.badge) { font-size: 20px; }
  .home .download .cta { margin-top: 40px; gap: 24px; }
  .home .download .badge .store-art { height: 76px; }
  .home .download .fine { margin-top: 32px; font-size: 13.5px; }
}
/* Below 360 px the wordmark does not fit beside Support: the app icon alone is the
   home link, still named "Pulled home" for assistive technology. */
@media (max-aspect-ratio: 9/10) and (max-width: 359px) {
  :is(.home, .page) .top .brand { font-size: 0; gap: 0; }
}

/* Landscape phones keep the side-by-side layout with readable, reachable copy. */
@media (min-aspect-ratio: 9/10) and (max-height: 600px) {
  .home { --gutter: 24px; }
  .home section { min-height: 0; }
  .home .hero h1 { font-size: clamp(30px, 4.4vw, 44px); }
  .home .hero .lede, .home .step .copy > p:last-child { font-size: 15px; margin-top: 12px; }
  .home .hero .cta { margin-top: 18px; gap: 14px; }
  .home .hero .badge .store-art { height: 56px; }
  .home .hero .qr { display: none; }
  .home .step h2 { font-size: 32px; }
  :is(.home, .page) .top nav a:not([href="/support/"]) { display: none; }
  /* the close fits one short screen: the hand above, then the headline and a row of smaller tiles */
  .home .download { padding-top: calc(56px + 14svh); padding-bottom: 6svh; }
  .home .download h2 { font-size: clamp(28px, 4.2vw, 40px); }
  .home .download .copy > p:not(.fine):not(.badge) { font-size: 15px; margin-top: 10px; }
  .home .download .cta { margin-top: 20px; gap: 16px; }
  .home .download .badge .store-art { height: 56px; }
  .home .download .qr { width: 132px; height: 132px; padding: 10px; border-radius: 20px; }
  .home .download .fine { margin-top: 18px; font-size: 12px; }
}
@media (prefers-reduced-motion: reduce) {
  html:has(body.home) { scroll-snap-type: y proximity; }
  .home .hero h1, .home .hero .lede, .home .hero .cta, .home .hero .slot,
  .home .top .brand, .home .top nav { animation: none; }
  .home .copy { will-change: auto; }
  .home .top, .home a, .page a { transition: none; }
}
