/*
  The 75 Hours website — one stylesheet for every page.

  This used to be a 1,600-line <style> block inside index.html, with a cut-down copy of the
  tokens in legal.css for the privacy and terms pages. Two pages could live with that. Six
  cannot: a colour changed in one place and not the other is a drift nobody notices until a
  screenshot goes out. So there is one file now, and every page links it.

  It is served with `max-age=0, must-revalidate` like everything else here, so it is fetched
  once and revalidated with a 304 after that — one round trip, shared across every page.

  Order: tokens, base, chrome (nav, footer, buttons), the landing page's own sections, then
  the document pages at the end. Everything from `--- document pages ---` down is scoped to
  `body.doc`, which is what the prose pages set, so the two layouts cannot reach each other.
*/

/* Cross-document fade, so moving between pages is a transition rather than a white
   flash. Ignored where unsupported. */
@view-transition { navigation: auto; }

:root {
  color-scheme: light dark;

  --bg:        #f6f6f8;
  --surface:   #ffffff;
  --sunken:    #eff0f2;
  --line:      #e2e2e7;
  --ink:       #14161a;
  --muted:     #5c6270;
  --faint:     #8b909c;
  --accent:    #1b5aa5;
  --accent-2:  #2f8ff0;
  --day:       #c97a16;
  --night:     #4b4bc4;
  --success:   #1f7a54;
  --warn:      #b4690e;

  --frame:     #d5d7dc;
  --shadow:    0 2px 4px rgba(20, 22, 26, .04), 0 12px 32px rgba(20, 22, 26, .07);
  --shadow-lg: 0 4px 10px rgba(20, 22, 26, .06), 0 30px 70px rgba(20, 22, 26, .14);

  --ease: cubic-bezier(.16, 1, .3, 1);
  /* A little overshoot at the end — used only on entrances, never on anything the reader
     is waiting for. */
  --spring: cubic-bezier(.22, 1.2, .36, 1);
  --page: 1080px;

  --spot: color-mix(in srgb, var(--accent) 26%, transparent);
  --grain: .030;

  --nav-shadow:       0 1px 2px rgba(20, 22, 26, .05), 0 6px 20px rgba(20, 22, 26, .07);
  --nav-shadow-stuck: 0 1px 2px rgba(20, 22, 26, .07), 0 10px 30px rgba(20, 22, 26, .12);

  /* One ladder for everything that stacks, so a new layer is chosen from a list rather
     than by guessing a number bigger than the last one. The grain is deliberately the
     ceiling: it is the only thing that sits over the entire page. */
  --z-under:   -1;   /* card glows, section backdrops, anything behind its own parent */
  --z-base:     1;   /* content lifted above a decorative sibling */
  --z-sticky:   2;   /* the pinned showcase stage */
  --z-nav:     50;   /* the floating nav island */
  --z-skip:    60;   /* the skip link, which must clear the nav when focused */
  --z-grain:   90;   /* the page-wide noise sheet */

  /* ---------- liquid glass ----------

     Glass here is four things stacked, not one blur. The pane itself is a translucent
     tint over a saturated blur of whatever is behind it. A bright hairline runs along the
     top edge where light would catch the lip, and a dark one along the bottom where the
     pane's own thickness shades the surface underneath. Inside the rim there is a soft
     bloom, which is what stops a blurred panel reading as flat frosted plastic. The
     fourth is the specular — a highlight that tracks the pointer across the pane — and
     that one lives on `.glass::before` rather than in a token.

     `--glass-tint` is the only value a section overrides to colour its own glass. */
  --glass:        rgba(255, 255, 255, .55);
  --glass-strong: rgba(255, 255, 255, .74);
  --glass-tint:   transparent;
  --glass-edge:   rgba(255, 255, 255, .9);
  --glass-under:  rgba(20, 22, 26, .07);
  --glass-bloom:  rgba(255, 255, 255, .5);
  --glass-spec:   rgba(255, 255, 255, .75);
  --glass-blur:   22px;
  --glass-sat:    180%;

  /* Phosphor `check`, carried as a mask so it can be recoloured from CSS. */
  --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath d='M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z'/%3E%3C/svg%3E");
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg:        #0e0e11;
    --surface:   #1a1a1f;
    --sunken:    #141418;
    --line:      #2c2c33;
    --ink:       #f2f3f5;
    --muted:     #9ba1ad;
    --faint:     #6c727e;
    --accent:    #4a96ef;
    --accent-2:  #7ab6ff;
    --day:       #f0a83c;
    --night:     #8b8bef;
    --success:   #3fbf8a;
    --warn:      #e9a63f;

    --frame:     #34343c;
    --shadow:    0 2px 4px rgba(0, 0, 0, .3), 0 12px 32px rgba(0, 0, 0, .4);
    --shadow-lg: 0 4px 10px rgba(0, 0, 0, .35), 0 30px 70px rgba(0, 0, 0, .55);

    --spot: color-mix(in srgb, var(--accent) 40%, transparent);
    --grain: .042;

    --nav-shadow:       0 1px 2px rgba(0, 0, 0, .3), 0 6px 20px rgba(0, 0, 0, .35);
    --nav-shadow-stuck: 0 1px 2px rgba(0, 0, 0, .4), 0 10px 30px rgba(0, 0, 0, .5);

    /* In the dark the pane is a dark tint, but the rim keeps working the same way: light
       catches the top lip, the bottom drops into shadow. The specular has to come down a
       long way or every panel looks wet. */
    --glass:        rgba(32, 33, 41, .58);
    --glass-strong: rgba(32, 33, 41, .8);
    --glass-edge:   rgba(255, 255, 255, .2);
    --glass-under:  rgba(0, 0, 0, .55);
    --glass-bloom:  rgba(255, 255, 255, .07);
    --glass-spec:   rgba(255, 255, 255, .16);
    --glass-sat:    170%;
  }
}

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* A single sheet of fine grain over the whole page. It is here for the large soft
   gradients — the hero glow and the night sky band both show stepping on an 8-bit
   display, and a few percent of noise breaks the bands up completely. That it also
   reads as print stock is the reason it is worth the one paint. */
body::after {
  content: "";
  position: fixed; inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: var(--grain);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

img { max-width: 100%; display: block; }
/* Every <picture> here wraps a block-level image and inherits its box, so the AVIF/PNG
   swap changes nothing about layout, centring or aspect ratio. */
picture { display: block; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---------- focus ---------- */

/* The form fields already draw their own ring. Everything else was falling back to the
   browser's default outline, which is legible but sits tight against a 980px-radius pill
   and ignores the page's colour entirely. One offset ring, in the accent, on every
   focusable thing that is not a field — and only for keyboard focus, so a pointer press
   never lights it up. */
:where(a, button, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 6px;
}
/* Pills and cards carry their own curve; the ring has to follow it rather than cutting
   the corners off. */
.btn:focus-visible,
.nav-links a:focus-visible,
.nav-brand:focus-visible { border-radius: 980px; outline-offset: 4px; }

/* The first thing a keyboard reaches, and invisible until it is reached. Parked off the
   top of the page rather than in a 1px box, so it animates down into view. */
.skip {
  position: fixed; top: 0; left: 50%;
  z-index: var(--z-skip);
  transform: translate(-50%, -140%);
  padding: 11px 22px;
  border-radius: 0 0 14px 14px;
  background: var(--accent); color: #fff;
  font-size: .92rem; font-weight: 600;
  box-shadow: 0 8px 24px color-mix(in srgb, var(--accent) 40%, transparent);
  transition: transform .3s var(--spring);
}
.skip:focus-visible {
  transform: translate(-50%, 0);
  text-decoration: none;
  outline-offset: -4px;
}

/* ---------- copy, drag and save protection ----------

   Asked for explicitly, and worth being plain in the source about what it is and is not.
   The screenshots are still fetched over ordinary HTTP and still sit in the browser cache,
   so DevTools, View Source and the network panel all retrieve them untouched; the same
   images are public on the App Store listing besides. What this stops is the casual
   right-click-and-save and the accidental text drag, and nothing more determined.

   The form is exempt on purpose. People paste their email address into it, and a contact
   form you cannot paste into is a contact form that does not get used. */
body {
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
  /* Kills the iOS long-press "Save Image" / "Copy" sheet. */
  -webkit-touch-callout: none;
}
/* Fields stay fully selectable, copyable and pasteable. */
input, textarea, select {
  -webkit-user-select: text;
     -moz-user-select: text;
          user-select: text;
  -webkit-touch-callout: default;
}
img, picture, svg, .phone, .watch {
  -webkit-user-drag: none;
     -khtml-user-drag: none;
      -moz-user-drag: none;
           user-drag: none;
}

/* ---------- liquid glass ---------- */

/*
  One primitive, used by every pane on the page. Three layers make it read as glass rather
  than as a blurred rectangle:

    the pane    — a translucent tint over a saturated blur of whatever is behind it
    ::before    — the specular, a broad highlight that follows the pointer across the face
    ::after     — the rim, a one-pixel ring that is bright along the top lip, shaded along
                  the bottom, and picks up the accent where the pointer is nearest

  `--px`/`--py` are already written by the card script as the pointer moves, and sit at a
  neutral default, so a pane that is never pointed at — and every pane on a touch screen —
  is a still, evenly lit piece of glass rather than a dead one.
*/
.glass {
  position: relative;
  isolation: isolate;
  background:
    linear-gradient(var(--glass-tint), var(--glass-tint)),
    var(--glass);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
          backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border: 0;
  box-shadow: inset 0 0 34px var(--glass-bloom), var(--shadow);
}

.glass::before {
  content: "";
  position: absolute; inset: 0;
  z-index: var(--z-under);
  border-radius: inherit;
  background:
    radial-gradient(300px circle at var(--px, 50%) var(--py, 0%), var(--spot), transparent 70%),
    radial-gradient(560px circle at var(--px, 50%) var(--py, -10%), var(--glass-spec), transparent 58%);
  opacity: .5;
  transition: opacity .5s ease;
  pointer-events: none;
}
.glass:hover::before { opacity: 1; }

/* The rim, cut out of a filled box by two masks that cancel everywhere except the border.
   The linear layer is the pane's own thickness catching light at the top and dropping into
   shadow at the bottom; the radial on top of it is the accent gathering where the pointer
   is, which is what makes the edge feel wet rather than printed. */
.glass::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: inherit;
  padding: 1px;
  background:
    radial-gradient(240px circle at var(--px, 50%) var(--py, 0%),
                    color-mix(in srgb, var(--accent) 60%, transparent), transparent 66%),
    linear-gradient(180deg, var(--glass-edge), transparent 44%, var(--glass-under));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}

/* Where the glass sits directly on the page rather than over artwork, the blur has almost
   nothing to work with, so the pane leans on its tint instead. */
.glass-solid { background: linear-gradient(var(--glass-tint), var(--glass-tint)), var(--glass-strong); }

/* No backdrop-filter — mostly older Firefox. The panes become ordinary opaque surfaces
   rather than transparent holes with unreadable text over them. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass { background: var(--surface); box-shadow: var(--shadow); }
}

/* Images fade up as they decode rather than snapping in half-drawn. `.is-loaded` is set
   by the script; with no script the rule below never applies and everything is visible. */
html.js img[data-fade] { opacity: 0; transform: scale(.995); }
html.js img[data-fade].is-loaded {
  opacity: 1; transform: none;
  transition: opacity .7s var(--ease), transform .9s var(--ease);
}

/* ---------- layout ---------- */

.wrap { width: 100%; max-width: var(--page); margin: 0 auto; padding: 0 24px; }
.narrow { max-width: 720px; margin-left: auto; margin-right: auto; }
section { padding: 96px 0; }
@media (max-width: 700px) { section { padding: 64px 0; } }

.sunken { background: var(--sunken); }

/* Glass only reads as glass when there is something behind it to refract. This section is
   otherwise a flat panel, so it gets a slow field of the page's own three hues — accent,
   day amber, night indigo — well under the panels and heavily blurred. It is the same
   material as the hero's aurora, used here as a ground rather than as a headline. */
.field-band { position: relative; overflow: hidden; }
.field-band > .wrap { position: relative; z-index: var(--z-base); }
.field-band::before {
  content: "";
  position: absolute; inset: -12% -6%;
  pointer-events: none;
  background:
    radial-gradient(46% 38% at 18% 26%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
    radial-gradient(38% 34% at 82% 18%, color-mix(in srgb, var(--day) 15%, transparent), transparent 72%),
    radial-gradient(44% 40% at 66% 86%, color-mix(in srgb, var(--night) 17%, transparent), transparent 70%);
  filter: blur(38px);
  mask-image: radial-gradient(ellipse 84% 76% at 50% 50%, #000 30%, transparent 84%);
  -webkit-mask-image: radial-gradient(ellipse 84% 76% at 50% 50%, #000 30%, transparent 84%);
}

/* Every in-page link lands under a 52px sticky nav otherwise. */
section[id], main[id] { scroll-margin-top: 78px; }

/* ---------- type ---------- */

h1, h2, h3 {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
  font-weight: 700;
  letter-spacing: -0.022em;
  line-height: 1.08;
  margin: 0;
  text-wrap: balance;
}

h1 { font-size: clamp(2.25rem, 6.6vw, 4.4rem); letter-spacing: -0.035em; }
h2 { font-size: clamp(1.9rem, 4.2vw, 2.9rem); letter-spacing: -0.028em; }
h3 { font-size: 1.16rem; letter-spacing: -0.014em; line-height: 1.3; }

.eyebrow {
  font-size: .82rem;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 14px;
}

.lede {
  font-size: clamp(1.08rem, 2.1vw, 1.3rem);
  line-height: 1.5;
  color: var(--muted);
  margin: 20px 0 0;
  text-wrap: pretty;
}

p { margin: 16px 0 0; text-wrap: pretty; }
.center { text-align: center; }

/* ---------- nav ---------- */

/*
  The bar used to run the full width of the window, frosted, with a hard bottom edge — and
  because the hero's glow fades out behind it, that edge read as the gradient being sliced
  off rather than as a piece of chrome. So there is no bar any more: the brand and the links
  sit together on one floating island, sized to its own contents, with the page passing
  cleanly around it. Nothing has an edge to cut anything off with.

  The header itself is only a positioning frame, so it lets clicks through to the page on
  either side of the island.
*/
header.nav {
  position: sticky; top: 0; z-index: var(--z-nav);
  padding: 12px 16px 0;
  pointer-events: none;
}
.nav-inner {
  pointer-events: auto;
  width: fit-content;
  max-width: calc(100% - 8px);
  margin: 0 auto;
  display: flex; align-items: center;
  gap: clamp(14px, 3vw, 30px);
  height: 46px;
  padding: 0 8px 0 18px;
  border-radius: 980px;
  font-size: .92rem;
  /* Surface, rim and specular all come from `.glass`; the island keeps only its own
     shadow, which is tighter than the one a full-size pane carries. */
  box-shadow: inset 0 0 34px var(--glass-bloom), var(--nav-shadow);
  transition: background-color .4s ease, box-shadow .4s ease;
}
/* Once the page has moved, the island firms up so it stays legible over whatever is
   sliding underneath it — the same glass, thickened. */
header.nav.stuck .nav-inner {
  background: linear-gradient(var(--glass-tint), var(--glass-tint)), var(--glass-strong);
  box-shadow: inset 0 0 34px var(--glass-bloom), var(--nav-shadow-stuck);
}
.nav-brand { display: flex; align-items: center; gap: 9px; font-weight: 600; color: var(--ink); }
.nav-brand:hover { text-decoration: none; }
.nav-brand img { width: 22px; height: 22px; border-radius: 5px; transition: transform .5s var(--spring); }
.nav-brand:hover img { transform: rotate(-6deg) scale(1.08); }

/* The links sit above the sliding pill, which is the only thing that moves. */
.nav-links { position: relative; display: flex; gap: 6px; }
.nav-links a {
  position: relative; z-index: 1;
  padding: 6px 12px; border-radius: 980px;
  color: var(--muted);
  transition: color .3s ease;
}
.nav-links a:hover { color: var(--ink); text-decoration: none; }
.nav-links a.current { color: var(--accent); }
@media (max-width: 620px) { .nav-links a.hide-sm { display: none; } }

/* Parked at zero width until the script has something to point at, so it never flashes
   in the wrong place on load. */
.nav-pill {
  position: absolute; top: 50%; left: 0;
  height: 30px; width: 0;
  margin-top: -15px;
  border-radius: 980px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  opacity: 0;
  transform: translateX(var(--pill-x, 0));
  transition: transform .5s var(--ease), width .5s var(--ease), opacity .3s ease;
  pointer-events: none;
}
.nav-pill.on { opacity: 1; }

/* ---------- hero ---------- */

/*
  The hero has to clip, or the aurora's box spills out of it. But the phone sits flush
  against the bottom edge with a 44px drop-shadow under it, and plain `hidden` cut that
  shadow off along a hard horizontal line. `overflow: clip` takes a margin, so the clip
  box can be pushed out far enough to let the shadow finish while still catching the
  aurora. Browsers without it keep the old behaviour.
*/
.hero { position: relative; padding: 88px 0 0; overflow: hidden; }
@supports (overflow: clip) {
  .hero { overflow: clip; overflow-clip-margin: 120px; }
}
.hero > * { position: relative; }

/* Three fields of colour on long, deliberately mismatched loops, so the pattern never
   visibly repeats. Heavily blurred and low contrast: the reader should feel the hero is
   alive without ever being able to point at what moved. */
.aurora {
  position: absolute; inset: -280px 0 auto;
  height: 900px;
  pointer-events: none;
  overflow: hidden;
  mask-image: linear-gradient(180deg, #000 62%, transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, #000 62%, transparent 100%);
}
/* `will-change` is scoped to the moment the drift is actually running. Left on
   permanently it keeps three blurred layers promoted for the whole visit. */
.aurora i {
  position: absolute;
  display: block;
  border-radius: 50%;
  filter: blur(60px);
}
.aurora.in-view i { will-change: transform; }
.aurora i:nth-child(1) {
  left: 50%; top: 90px; width: 940px; height: 620px; margin-left: -470px;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 30%, transparent), transparent 72%);
}
.aurora i:nth-child(2) {
  left: 50%; top: 200px; width: 620px; height: 520px; margin-left: -520px;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--accent-2) 26%, transparent), transparent 70%);
}
.aurora i:nth-child(3) {
  left: 50%; top: 250px; width: 560px; height: 460px; margin-left: -40px;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--day) 18%, transparent), transparent 70%);
}
/* Gated: a hero the reader scrolled past two screens ago should not still be animating. */
html.js .aurora.in-view i:nth-child(1) { animation: drift-a 46s var(--ease) infinite alternate; }
html.js .aurora.in-view i:nth-child(2) { animation: drift-b 61s var(--ease) infinite alternate; }
html.js .aurora.in-view i:nth-child(3) { animation: drift-c 53s var(--ease) infinite alternate; }

@keyframes drift-a { to { transform: translate3d(-70px, 44px, 0) scale(1.12); } }
@keyframes drift-b { to { transform: translate3d(120px, -36px, 0) scale(1.18); } }
@keyframes drift-c { to { transform: translate3d(-96px, 52px, 0) scale(.9); } }

.hero-icon {
  width: 108px; height: 108px; margin: 0 auto 30px;
  border-radius: 24px;
  filter: drop-shadow(0 12px 28px color-mix(in srgb, var(--accent) 40%, transparent));
}

.hero p.lede { margin-left: auto; margin-right: auto; max-width: 34ch; }

/* ---------- hero entrance ---------- */

/* One choreographed opening, played once on load. The finished state is the CSS default
   as everywhere else on this page: `html.js` is set by the script in the head, so with no
   script — or under reduced motion, which removes the class again — the hero is simply
   already open. */
html.js .hero [data-enter],
html.js .hero .hero-icon,
html.js .hero .hero-shot { opacity: 0; }
html.js .hero.open [data-enter] {
  animation: enter-rise .9s var(--ease) both;
  animation-delay: calc(var(--enter, 0) * 90ms);
}
html.js .hero.open .hero-icon {
  animation: enter-pop 1s var(--spring) both;
}
html.js .hero.open .hero-shot {
  animation: enter-lift 1.35s var(--ease) both;
  animation-delay: calc(var(--enter, 0) * 90ms);
}

@keyframes enter-rise {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes enter-pop {
  from { opacity: 0; transform: scale(.86) translate3d(0, 12px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes enter-lift {
  from { opacity: 0; transform: translate3d(0, 46px, 0) scale(.965); }
  to   { opacity: 1; transform: none; }
}

/* The headline arrives a line at a time from behind its own edge. The clip box has to
   carry a little slack top and bottom or it shaves the descender off "logged" and the
   comma after "hours" — the negative margin takes the slack back out of the layout. */
h1 .line {
  display: block;
  overflow: hidden;
  padding: .06em 0 .12em;
  margin: -.06em 0 -.12em;
}
html.js .hero h1 .line > span { display: block; }
html.js .hero.open h1 .line > span {
  animation: enter-line 1.05s var(--ease) both;
  animation-delay: calc(var(--enter, 0) * 90ms);
}
@keyframes enter-line {
  from { transform: translate3d(0, 105%, 0); }
  to   { transform: none; }
}

.cta-row {
  display: flex; flex-wrap: wrap; gap: 14px;
  justify-content: center; align-items: center;
  margin: 34px 0 0;
}
/* `--bx`/`--by` are the magnetic offset the script writes while the pointer is near the
   button; they are zero everywhere else, so the rule is inert without it. */
.btn {
  position: relative;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 12px 24px;
  border-radius: 980px;
  font-size: .98rem; font-weight: 600;
  letter-spacing: -0.01em;
  overflow: hidden;
  isolation: isolate;
  transform: translate3d(var(--bx, 0px), var(--by, 0px), 0);
  transition: transform .35s var(--ease), background-color .25s ease, box-shadow .35s ease;
}
.btn:hover { text-decoration: none; }
.btn:active { transform: translate3d(var(--bx, 0px), var(--by, 0px), 0) scale(.975); }

/* A specular pass across the face on hover. One sweep per hover, never a loop. */
.btn::after {
  content: "";
  position: absolute; inset: 0;
  z-index: -1;
  background: linear-gradient(105deg, transparent 34%, rgba(255, 255, 255, .38) 50%, transparent 66%);
  transform: translateX(-120%);
  pointer-events: none;
}
.btn:hover::after { animation: sheen .85s var(--ease) 1; }
@keyframes sheen { to { transform: translateX(120%); } }

.btn-primary {
  background: linear-gradient(180deg,
               color-mix(in srgb, var(--accent) 88%, #fff),
               var(--accent) 58%,
               color-mix(in srgb, var(--accent) 92%, #000));
  color: #fff;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .45),
    inset 0 -1px 0 rgba(0, 0, 0, .18),
    0 6px 20px color-mix(in srgb, var(--accent) 34%, transparent);
}
.btn-primary:hover {
  background: color-mix(in srgb, var(--accent) 88%, var(--ink));
  box-shadow: 0 10px 30px color-mix(in srgb, var(--accent) 46%, transparent);
}
/* The quiet button is a small piece of the same glass, tinted with the accent so it still
   reads as an action rather than as a chip. */
.btn-quiet {
  color: var(--accent);
  background: var(--glass);
  -webkit-backdrop-filter: blur(16px) saturate(var(--glass-sat));
          backdrop-filter: blur(16px) saturate(var(--glass-sat));
  box-shadow:
    inset 0 1px 0 var(--glass-edge),
    inset 0 -1px 0 var(--glass-under),
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
}
.btn-quiet:hover {
  background: color-mix(in srgb, var(--accent) 10%, var(--glass));
  box-shadow:
    inset 0 1px 0 var(--glass-edge),
    inset 0 -1px 0 var(--glass-under),
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 46%, transparent);
}
.btn-quiet::after { background: linear-gradient(105deg, transparent 34%, color-mix(in srgb, var(--accent) 16%, transparent) 50%, transparent 66%); }
.cta-note { width: 100%; text-align: center; font-size: .86rem; color: var(--faint); margin: 4px 0 0; }

/* Deliberately the site's own pill rather than an imitation of Apple's "Download on the
   App Store" badge — one line of type next to the mark, not the badge's stacked lockup.
   If the official badge artwork is wanted instead it drops straight in here. */
.btn-store { padding-left: 20px; }
.btn-store svg {
  width: 17px; height: 17px;
  flex: none;
  /* The mark reads low against a cap-height word without this. */
  margin-top: -2px;
}
.footer-cta { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; margin-bottom: 28px; }
.footer-cta .btn { font-size: .92rem; padding: 10px 20px; }
.footer-cta p { margin: 0; font-size: .86rem; color: var(--faint); }

.hero-shot { margin: 56px auto 0; max-width: 358px; }

/* ---------- device frames ---------- */

/* The bezel is part of the PNG — Apple's own device image, with the capture pasted into
   the display cutout at native resolution. So there is no CSS chrome here: just size the
   image and let it keep its own aspect ratio. `height: auto` is the whole fix for the
   stretching, and the shadow has to be a drop-shadow rather than a box-shadow because the
   art is transparent outside the rounded body of the phone. */
.phone img,
.watch img {
  display: block;
  width: 100%;
  height: auto;
}
.phone img { filter: drop-shadow(0 22px 44px rgba(20, 22, 26, .22)); }
.watch img { filter: drop-shadow(0 16px 32px rgba(20, 22, 26, .2)); }

/* In the dark the shadow does nothing — a dark shadow under a dark titanium frame on a
   near-black page leaves the Lock Screen capture in particular with no edge at all. The
   second drop-shadow is a hairline of light tracing the outline of the art, which is what
   actually separates the device from the page. */
@media (prefers-color-scheme: dark) {
  .phone img {
    filter: drop-shadow(0 22px 44px rgba(0, 0, 0, .6))
            drop-shadow(0 0 1px rgba(255, 255, 255, .55));
  }
  .watch img {
    filter: drop-shadow(0 16px 32px rgba(0, 0, 0, .55))
            drop-shadow(0 0 1px rgba(255, 255, 255, .5));
  }
}

@media (prefers-reduced-motion: reduce) {
  .phone img, .watch img { filter: none; }
}

/* ---------- stat band ---------- */

.stats {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  padding: 44px 0;
}
.stat { text-align: center; padding: 0 8px; }
.stat b {
  display: block;
  font-size: clamp(1.8rem, 4.6vw, 2.6rem);
  font-weight: 700; letter-spacing: -0.03em; line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
/* Direct child only. The odometer builds a tree of spans inside the <b> beside this one,
   and a bare `.stat span` reaches straight into it and resets the font size. */
.stat > span { display: block; font-size: .88rem; color: var(--muted); margin-top: 8px; line-height: 1.4; }
@media (max-width: 620px) { .stats { grid-template-columns: 1fr; gap: 32px; } }

/* ---------- odometer ---------- */

/* Built by the script out of the number that is already in the markup, so the plain
   "15,334" is what ships and what shows if the script never runs. Each digit is a column
   of 0–9 that rolls to its own resting place. */
/* Every cell — the comma included — is the same clipped box, so the whole number keeps one
   baseline instead of the digits dropping away from the separator. */
.odo { --oh: 1.16em; display: inline-flex; }
.odo-d { display: block; height: var(--oh); overflow: hidden; }
.odo-col {
  display: block;
  transform: translateY(calc(var(--d, 0) * var(--oh) * -1));
  will-change: transform;
}
.odo-col > span { display: block; height: var(--oh); line-height: var(--oh); }

/* ---------- alternating feature rows ---------- */

.row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(32px, 6vw, 88px);
  align-items: center;
}
.row + .row { margin-top: 112px; }
.row .shot { max-width: 336px; width: 100%; justify-self: center; }
.row.flip .copy { order: 2; }
@media (max-width: 820px) {
  .row { grid-template-columns: 1fr; gap: 40px; }
  .row + .row { margin-top: 72px; }
  .row.flip .copy { order: 0; }
}

.row h2 + p { margin-top: 18px; color: var(--muted); font-size: 1.06rem; }

ul.ticks { list-style: none; margin: 24px 0 0; padding: 0; }
ul.ticks li {
  position: relative;
  padding-left: 30px;
  margin: 12px 0;
  color: var(--muted);
}
/* The tick was two rotated borders. It is now the same Phosphor `check` the rest of the
   page draws its icons from, carried as a mask so it still takes `--success` from CSS and
   still wipes on from the left. */
ul.ticks li::before {
  content: "";
  position: absolute; left: 0; top: .34em;
  width: 18px; height: 18px;
  background: var(--success);
  -webkit-mask: var(--icon-check) center / contain no-repeat;
          mask: var(--icon-check) center / contain no-repeat;
}
ul.ticks li strong { color: var(--ink); font-weight: 600; }

/* The tick draws itself on rather than fading in with the text around it. */
html.js .reveal ul.ticks li::before,
html.js .showcase-step ul.ticks li::before {
  clip-path: inset(0 100% 0 0);
  transition: clip-path .5s var(--ease);
  transition-delay: calc(.25s + var(--t, 0) * .09s);
}
html.js .reveal.in ul.ticks li::before,
html.js .showcase-step.is-active ul.ticks li::before { clip-path: inset(0); }

/* ---------- sticky showcase ---------- */

/* One phone held in the middle of the screen while the copy walks past it, swapping the
   screen inside the frame at each step. The stage is only sticky where there is a column
   beside it to scroll — see the narrow-screen block below, where it parks at the top
   instead and the steps run underneath. */
.showcase-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(32px, 6vw, 88px);
  align-items: start;
}
.showcase-stage {
  position: sticky;
  top: 70px;
  height: calc(100vh - 70px);
  height: calc(100svh - 70px);
  display: flex; align-items: center; justify-content: center;
}
.showcase-phone {
  position: relative;
  width: min(300px, 82%);
  /* Every capture is the same 800 × 1636 frame, so one ratio holds the box open for all
     of them and the absolutely positioned layers have somewhere to sit. */
  aspect-ratio: 800 / 1636;
}
.showcase-frame {
  position: absolute; inset: 0;
  display: block;
  opacity: 0;
  transform: scale(.97) translate3d(0, 10px, 0);
  transition: opacity .55s var(--ease), transform .8s var(--ease);
  will-change: opacity, transform;
}
.showcase-frame img { width: 100%; height: 100%; object-fit: contain; }
.showcase-frame.is-active { opacity: 1; transform: none; }

.showcase-steps { padding: 8vh 0; }
.showcase-step {
  min-height: 74vh;
  display: flex; flex-direction: column; justify-content: center;
}
html.js .showcase-step {
  opacity: .34;
  filter: blur(1.5px);
  transition: opacity .55s var(--ease), filter .55s var(--ease);
}
html.js .showcase-step.is-active { opacity: 1; filter: none; }

/* A rail down the left of the copy that fills as you move through the steps — the reader's
   position in the sequence, without a set of dots to interpret. */
.showcase-step { position: relative; padding-left: 26px; }
.showcase-step::before {
  content: "";
  position: absolute; left: 0; top: 12%; bottom: 12%;
  width: 2px; border-radius: 2px;
  background: var(--line);
}
.showcase-step::after {
  content: "";
  position: absolute; left: 0; top: 12%;
  width: 2px; border-radius: 2px;
  height: 0;
  background: linear-gradient(180deg, var(--accent), var(--accent-2));
  transition: height .6s var(--ease);
}
.showcase-step.is-active::after { height: 76%; }

@media (max-width: 860px) {
  .showcase-grid { grid-template-columns: 1fr; gap: 0; }
  /* Above the copy and pinned under the nav: the same idea, minus the second column. */
  .showcase-stage { height: 46vh; height: 46svh; top: 70px; z-index: var(--z-sticky); margin-bottom: 8px; }
  .showcase-phone { width: min(180px, 42%); }
  .showcase-steps { padding: 0 0 4vh; }
  .showcase-step { min-height: 0; padding: 40px 0 40px 22px; }
  html.js .showcase-step { opacity: 1; filter: none; }

  /* With one column the copy scrolls *under* the pinned phone, and the art is cut out on
     transparency — so without this the headings run straight through the device. A panel
     of the page colour, bled past the wrap's padding to both edges and faded out along its
     bottom so the copy slides away rather than stopping at a line. It sits at the back of
     the stage's own stacking context: over the steps, under the phone. */
  .showcase-stage::before {
    content: "";
    position: absolute;
    top: -70px; bottom: 0; left: -24px; right: -24px;
    z-index: -1;
    background: linear-gradient(180deg, var(--bg) calc(100% - 44px), transparent 100%);
  }
}

/* Not enough height to hold a phone and a paragraph at once. */
@media (max-height: 560px) {
  .showcase-stage { position: static; height: auto; padding: 40px 0; }
  .showcase-step { min-height: 0; padding-top: 32px; padding-bottom: 32px; }
  html.js .showcase-step { opacity: 1; filter: none; }
}

/* ---------- feature grid ---------- */

/*
  Nine capabilities used to sit in nine identical auto-fit cards. That is the most generic
  block a page like this can ship: every item claiming exactly the same weight, so none of
  them carries any, and the reader skims all nine instead of reading three.

  They are split here by what each one has to prove. Three are the app's actual arguments —
  the boundaries on the device, the signature handled like a statutory record, the route
  drawn along real roads — and those keep the elevation, the scale and the pointer
  lighting. The remaining six are true, worth stating, and get a line each with no chrome
  at all. Elevation means something again, because not everything has it now.

  The composition is deliberately not three-up: one tall panel on the left against two
  stacked beside it, so the eye enters at the tall one and the row has a direction.
*/
.grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 18px;
  margin-top: 56px;
}
.card { grid-column: span 7; }
.card.is-tall { grid-column: span 5; grid-row: span 2; }

/* Under about 900px there is no room for a 5/7 split that still holds a paragraph, so the
   three panels stack and the tall one stops being tall. */
@media (max-width: 900px) {
  .grid { grid-template-columns: 1fr; }
  .card, .card.is-tall { grid-column: auto; grid-row: auto; }
}

/* ---------- the six quieter capabilities ---------- */

/*
  No card, no border, no shadow — a hairline and the space around it do the whole job.
  Two columns on desktop so the list reads as a list rather than as six more panels.
*/
.minor {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: clamp(32px, 5vw, 72px);
  margin-top: 18px;
}
@media (max-width: 760px) { .minor { grid-template-columns: 1fr; } }

.minor-item {
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: 16px;
  align-items: start;
  padding: 22px 4px;
  border-top: 1px solid var(--line);
}
/* The top rule on the first item of each column would double up against the panels above,
   so the first row in each column goes without one. */
.minor-item:nth-child(1),
.minor-item:nth-child(2) { border-top: 0; padding-top: 8px; }
@media (max-width: 760px) {
  .minor-item:nth-child(2) { border-top: 1px solid var(--line); padding-top: 22px; }
}

.minor-item svg {
  width: 22px; height: 22px;
  margin-top: 2px;
  color: var(--accent);
  transition: transform .5s var(--spring);
}
.minor-item:hover svg { transform: scale(1.12) rotate(-4deg); }
.minor-item h3 { font-size: 1.02rem; margin: 0 0 5px; }
.minor-item p { margin: 0; color: var(--muted); font-size: .93rem; line-height: 1.5; }
/* `--px`/`--py` follow the pointer across the card; `--rx`/`--ry` are the tilt derived from
   the same position. All four sit at a neutral default, so a card that is never pointed at
   — and every card on a touch screen — is exactly the flat card it was before. */
/* The card is a glass pane: the surface, the rim and the specular all come from `.glass`,
   so what is left here is only geometry and the tilt. */
.card {
  display: flex; flex-direction: column;
  /* The container is softer than anything inside it. The tall panel is the largest object
     in the section and takes the largest curve; nothing on the page uses one radius for
     every box. */
  border-radius: 22px;
  padding: 30px 30px 32px;
  /* The scenes bleed past the padding box on purpose; without this their square corners
     show through the panel's rounded ones. */
  overflow: hidden;
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) translateY(var(--lift, 0px));
  transition: transform .5s var(--ease), box-shadow .5s var(--ease);
}
.card.is-tall { border-radius: 28px; padding: 34px 34px 36px; }
.card:hover { --lift: -4px; box-shadow: inset 0 0 34px var(--glass-bloom), var(--shadow-lg); }


/* The icon sits in a tinted well rather than floating loose above the heading — it gives
   the three panels a fixed optical anchor the six quiet rows deliberately do not have. */
.card .ico {
  display: grid; place-items: center;
  width: 46px; height: 46px;
  margin-bottom: 20px;
  border-radius: 14px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  transition: transform .5s var(--spring), background-color .4s ease;
}
.card.is-tall .ico { width: 54px; height: 54px; border-radius: 16px; }
.card:hover .ico {
  transform: scale(1.06) rotate(-3deg);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
/* Scoped to the well. An unscoped `.card svg` also catches the boundary mesh further down
   the panel and squeezes it to icon size.

   Phosphor draws filled outlines rather than strokes, so the icons take their colour from
   `color` through `fill="currentColor"` — setting `stroke` on these does nothing. */
.card .ico svg { width: 24px; height: 24px; color: var(--accent); }
.card.is-tall .ico svg { width: 28px; height: 28px; }

/*
  The tall panel is twice the height of its content, and a card with 300px of nothing in it
  reads as a mistake rather than as composure. So it holds the thing it is describing: a
  mesh of locality boundaries with one cell resolved and a point sitting inside it, which is
  literally what the feature does with a coordinate. Authored in the section's own grammar —
  the same hairlines and the same accent as the route band — rather than filled with a
  stock illustration.

  It bleeds to the card's edges on purpose, so the mesh reads as a map continuing past the
  panel instead of a picture placed inside it.
*/
/* Geometry, masking and the bleed now come from `.scene`, which this shares with the
   signature and route panels. What is left here is only the mesh's own drawing. */
.boundaries .cell { fill: none; stroke: color-mix(in srgb, var(--ink) 15%, transparent); stroke-width: 1.2; }
.boundaries .cell-on {
  fill: color-mix(in srgb, var(--accent) 14%, transparent);
  stroke: var(--accent); stroke-width: 1.8;
}
.boundaries .ping { fill: var(--accent); }
/* The one deliberate loop on the page: a coordinate being resolved is a repeating act, and
   the pulse is what says the panel is live rather than a diagram. It runs only while the
   panel is on screen, and the resolved cell and its point stay put when it stops. */
.boundaries .ping-ring { fill: none; stroke: var(--accent); stroke-width: 1.6; opacity: 0; }
html.js .scene.in-view .ping-ring { animation: ping 3.4s var(--ease) infinite; }
@keyframes ping {
  0%        { transform: scale(.3);  opacity: .9; }
  70%, 100% { transform: scale(3.1); opacity: 0; }
}

/* ---------- panel scenes ----------

   Each of the three panels ends in a small animated scene of the thing it is describing:
   a coordinate resolving against locality boundaries, a signature being recorded as
   geometry, a route drawing itself along the roads. They loop slowly and quietly, and
   every one of them is switched off wholesale under reduced motion further down.

   All three bleed to the panel's edges and fade in from the paragraph above, so they read
   as the panel's floor rather than as pictures dropped into it. */
.scene {
  display: block;
  width: calc(100% + 60px);
  margin: 24px -30px -32px;
  height: auto;
  mask-image: linear-gradient(180deg, transparent 0%, #000 30%);
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 30%);
}
/* The tall panel stretches to match the column beside it, so the mesh grows into whatever
   height is left rather than sitting pinned at the floor with a void above it. `slice`
   makes it cover that box the way a map should — extending further out, not letterboxed. */
.card.is-tall .scene {
  width: calc(100% + 68px);
  margin: 26px -34px -36px;
  flex: 1 1 auto;
  min-height: 190px;
  height: auto;
}
/* Stacked, the panel is no longer stretched by a column beside it, so the mesh would take
   its full intrinsic height — around 480px, most of a phone screen for one card. Capped,
   and `slice` crops rather than squashing it. */
@media (max-width: 900px) {
  .card.is-tall .scene { max-height: 250px; margin: 24px -30px -32px; width: calc(100% + 60px); }
}

/* SVG transforms need a box to resolve percentages against before `scale()` means
   anything sensible on a child element. */
.scene [class^="sig-"], .scene [class^="rt-"], .scene .ping-ring { transform-box: fill-box; transform-origin: center; }

/* --- the signature, recorded as geometry --- */

/*
  Every scene below follows the page's existing rule: the *finished* frame is the plain CSS,
  the starting frame is applied only under `html.js`, and the animation runs when the scene
  is actually on screen. So with no script, or with reduced motion asked for, each panel
  simply shows the completed drawing — a written signature with its points, a drawn route —
  rather than an empty box waiting on an observer.

  They play once on arrival and stop. Three scenes looping forever would compete with the
  sunset in the night band, which is the page's one authored moment, and would keep three
  compositors awake for the whole visit.
*/

.sig-rule { stroke: color-mix(in srgb, var(--ink) 14%, transparent); stroke-width: 1.2; }
.sig-path {
  fill: none; stroke: var(--accent); stroke-width: 2.6;
  stroke-linecap: round; stroke-linejoin: round;
  /* The path measures 720 units (the scrawl plus its crossing flourish); the dash is set
     just past that so the stroke starts
     hidden and the draw begins the instant the animation does. */
  stroke-dasharray: 725;
}
.sig-node { fill: var(--surface); stroke: var(--accent); stroke-width: 1.5; }

html.js .scene:not(.played) .sig-path { stroke-dashoffset: 725; }
html.js .scene:not(.played) .sig-node { opacity: 0; }

/* The hand writes, then the points it was stored as settle onto the stroke. The gap
   between the two is the whole argument of the panel, so the nodes wait for the pen. */
html.js .scene.in-view .sig-path {
  animation: sig-draw 1.7s cubic-bezier(.22, .62, .28, 1) both;
}
html.js .scene.in-view .sig-node {
  animation: sig-node .42s var(--spring) both;
  animation-delay: calc(1.28s + var(--n, 0) * .045s);
}
@keyframes sig-draw { to { stroke-dashoffset: 0; } }
@keyframes sig-node {
  from { opacity: 0; transform: scale(.3); }
  to   { opacity: 1; transform: none; }
}

/* --- the route, drawing itself --- */

.rt-line {
  fill: none; stroke: var(--accent); stroke-width: 3.4;
  stroke-linecap: round; stroke-linejoin: round;
  /* Path measures 303 units. */
  stroke-dasharray: 307;
}
.rt-grid { stroke: color-mix(in srgb, var(--ink) 7%, transparent); stroke-width: 1; }
.rt-pin  { fill: var(--accent); opacity: 1; }
.rt-pin-start { fill: var(--surface); stroke: var(--accent); stroke-width: 3; }

html.js .scene:not(.played) .rt-line { stroke-dashoffset: 307; }
html.js .scene:not(.played) .rt-pin  { opacity: 0; }

html.js .scene.in-view .rt-line {
  animation: rt-draw 1.55s cubic-bezier(.32, .7, .3, 1) both;
}
/* The destination pin lands only once the road has reached it. */
html.js .scene.in-view .rt-pin {
  animation: rt-pin .34s var(--spring) both;
  animation-delay: 1.5s;
}
@keyframes rt-draw { to { stroke-dashoffset: 0; } }
@keyframes rt-pin  { from { opacity: 0; transform: scale(.2); } to { opacity: 1; transform: none; } }

/* The head rides the same path the stroke is drawn along, so the bright point is always
   exactly where the road has got to, then leaves — it has no meaningful resting state. */
.rt-head, .rt-glow { opacity: 0; }
html.js .scene.in-view .rt-head,
html.js .scene.in-view .rt-glow {
  offset-path: path("M16 44 L28.6 44.0 Q40 44 47.0 35.0 L47.0 35.0 Q54 26 65.4 26.0 L73.0 26.0 Q86 26 92.6 37.2 L93.4 38.8 Q100 50 113.0 50.0 L124.8 50.0 Q134 50 141.0 44.0 L141.0 44.0 Q148 38 157.2 38.0 L171.0 38.0 Q184 38 189.2 49.9 L192.8 58.1 Q198 70 211.0 70.0 L224.6 70.0 Q236 70 243.0 61.0 L243.0 61.0 Q250 52 261.4 52.0 L280 52");
  offset-rotate: 0deg;
  animation: rt-ride 1.55s cubic-bezier(.32, .7, .3, 1) both;
}
.rt-head { fill: var(--accent-2); }
.rt-glow { fill: var(--accent-2); filter: blur(5px); }
@keyframes rt-ride {
  0%       { offset-distance: 0%;   opacity: 0; }
  8%       { opacity: 1; }
  82%      { opacity: 1; }
  100%     { offset-distance: 100%; opacity: 0; }
}
/* Without offset-path the head would sit stacked at the origin, which reads as a bug
   rather than as a missing flourish. */
@supports not (offset-path: path("M0 0 L1 1")) {
  .rt-head, .rt-glow { display: none; }
}

.card h3 { margin-bottom: 10px; font-size: 1.24rem; letter-spacing: -0.018em; }
.card.is-tall h3 { font-size: 1.5rem; letter-spacing: -0.024em; }
.card p { margin: 0; color: var(--muted); font-size: .98rem; line-height: 1.55; }
.card.is-tall p { font-size: 1.04rem; line-height: 1.6; max-width: 34ch; }

/* ---------- night section ---------- */

.night-band {
  background: linear-gradient(170deg, #10132b 0%, #090a16 100%);
  color: #eef0fb;
  position: relative;
  overflow: hidden;
}
.night-band::after {
  content: "";
  position: absolute; inset: 0;
  background: radial-gradient(900px 380px at 78% 12%, rgba(139, 139, 239, .30), transparent 62%);
  pointer-events: none;
}
.night-band .wrap { position: relative; z-index: 1; }
.night-band .eyebrow { color: #9d9dfa; }
.night-band p { color: #b9bed6; }
.night-band .sun-line {
  display: flex; flex-wrap: wrap; align-items: center; gap: 28px;
  margin-top: 44px; padding: 26px 30px;
  border-radius: 20px;
  /* Over the night sky this pane has real artwork behind it, so the blur has something to
     bend. The tint is warmed toward the band's own indigo rather than left neutral. */
  --glass-tint: rgba(120, 122, 220, .10);
  --glass-edge: rgba(255, 255, 255, .34);
  --glass-under: rgba(0, 0, 0, .4);
  --glass-spec: rgba(255, 255, 255, .2);
}
.sun-stat b {
  display: block;
  font-size: 1.9rem; font-weight: 700; letter-spacing: -0.03em; line-height: 1.1;
  font-variant-numeric: tabular-nums; color: #fff;
}
.sun-stat span { display: block; margin-top: 6px; font-size: .88rem; line-height: 1.4; color: #b9bed6; }
.sun-rule { width: 1px; align-self: stretch; background: rgba(255, 255, 255, .14); }
.night-band .sun-note {
  flex: 1 1 220px; margin: 0;
  font-size: .95rem; line-height: 1.5; color: #b9bed6;
}
@media (max-width: 620px) {
  .night-band .sun-line { gap: 22px; padding: 22px 24px; }
  .sun-rule { display: none; }
  .night-band .sun-note { flex-basis: 100%; }
}

/* ---------- privacy ---------- */

.privacy-statement {
  font-size: clamp(1.5rem, 3.4vw, 2.15rem);
  font-weight: 700; letter-spacing: -0.028em; line-height: 1.2;
  text-wrap: balance;
}
.privacy-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 28px; margin-top: 48px; text-align: left;
}
.privacy-grid h3 { font-size: 1rem; margin-bottom: 6px; }
.privacy-grid p { margin: 0; color: var(--muted); font-size: .95rem; }

/* ---------- companions ---------- */

.section-lede { margin-top: 18px; color: var(--muted); font-size: 1.06rem; }

/*
  Two rows shared by all three figures: the devices in the first, the captions in the
  second. Each figure spans both and adopts them as its own with `subgrid`, which is what
  puts every device on one baseline and starts every caption on one line — regardless of
  the fact that the middle caption runs to four lines and the others to three.

  Aligning the figures themselves instead (the obvious `align-items: end`) lines up the
  bottoms of the captions, and so pushes the device above a longer caption upwards. That is
  what had the Live Activity riding higher than the watch and the widget beside it.
*/
.companions {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: 1fr auto;
  gap: clamp(28px, 4vw, 52px);
  margin-top: 64px;
}
.companion {
  margin: 0;
  text-align: center;
  display: grid;
  grid-row: span 2;
  grid-template-rows: subgrid;
  row-gap: 24px;
}
/* A watch is far wider relative to its height than a phone, so the two are sized to carry
   a similar visual weight rather than a similar pixel width — the watch ends up wider and
   still reads as the smaller object. Both sit on the floor of the row they share. */
.companion .watch { max-width: 260px; margin: 0 auto; align-self: end; width: 100%; }
.companion .phone { max-width: 190px; margin: 0 auto; align-self: end; width: 100%; }
.companion figcaption {
  color: var(--muted);
  font-size: .92rem;
  max-width: 30ch;
  margin-left: auto; margin-right: auto;
}

/* Without subgrid the figures cannot share rows, so fall back to the old behaviour rather
   than to a broken one: bottoms aligned, captions wherever they land. */
@supports not (grid-template-rows: subgrid) {
  .companions { grid-template-rows: none; align-items: end; }
  .companion { display: block; grid-row: auto; }
  .companion figcaption { margin-top: 24px; }
}

@media (max-width: 860px) {
  /* One column: nothing to line up against, so each figure goes back to being a plain
     block and the shared rows are dropped. */
  .companions { grid-template-columns: 1fr; grid-template-rows: none; gap: 56px; }
  .companion { display: block; grid-row: auto; }
  .companion .watch { max-width: 300px; }
  .companion .phone { max-width: 240px; }
  .companion figcaption { margin-top: 24px; }
}

/* ---------- printout ---------- */

.print-shot { max-width: 380px; margin: 0 auto; }

/* ---------- disclaimer + footer ---------- */

.disclaimer {
  border-radius: 18px;
  padding: 22px 24px;
  font-size: .93rem;
  color: var(--muted);
  margin-top: 72px;
}
.disclaimer strong { color: var(--ink); }

footer {
  border-top: 1px solid var(--line);
  padding: 40px 0 56px;
  font-size: .87rem;
  color: var(--faint);
}
footer .links { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 18px; }
footer p { margin: 8px 0 0; max-width: 62ch; }

/* ---------- contact form ---------- */

.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  gap: 56px;
  align-items: start;
}
@media (max-width: 860px) { .contact-grid { grid-template-columns: 1fr; gap: 36px; } }

.form { display: grid; gap: 18px; margin-top: 4px; }
.form-row { display: grid; gap: 18px; grid-template-columns: 1fr 1fr; }
@media (max-width: 560px) { .form-row { grid-template-columns: 1fr; } }

/* Floating labels. The control comes first in the markup and the label after it, so the
   label can be driven from the control's own state — `:focus` and `:placeholder-shown` —
   with no script involved at all. */
.field { position: relative; display: block; }
.field .hint { display: block; margin-top: 7px; font-size: .82rem; color: var(--faint); }

.field input,
.field select,
.field textarea {
  font: inherit;
  font-size: 1rem;
  width: 100%;
  padding: 24px 14px 9px;
  color: var(--ink);
  background: var(--glass);
  -webkit-backdrop-filter: blur(14px) saturate(var(--glass-sat));
          backdrop-filter: blur(14px) saturate(var(--glass-sat));
  border: 1px solid transparent;
  border-radius: 12px;
  box-shadow: inset 0 1px 0 var(--glass-edge), inset 0 -1px 0 var(--glass-under);
  transition: border-color .25s ease, box-shadow .25s ease, background-color .25s ease;
  -webkit-appearance: none;
  appearance: none;
}
.field textarea { min-height: 156px; resize: vertical; line-height: 1.5; }
.field select {
  background-image: linear-gradient(45deg, transparent 50%, var(--faint) 50%),
                    linear-gradient(135deg, var(--faint) 50%, transparent 50%);
  background-position: calc(100% - 20px) calc(50% + 7px), calc(100% - 14px) calc(50% + 7px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: 40px;
}

.field > label {
  position: absolute; left: 15px; top: 15px;
  font-size: 1rem; font-weight: 500;
  color: var(--faint);
  letter-spacing: -0.008em;
  pointer-events: none;
  transform-origin: 0 0;
  transition: transform .3s var(--ease), color .25s ease;
}
.field input:focus ~ label,
.field input:not(:placeholder-shown) ~ label,
.field textarea:focus ~ label,
.field textarea:not(:placeholder-shown) ~ label,
.field select ~ label {
  transform: translateY(-9px) scale(.74);
}
.field input:focus ~ label,
.field textarea:focus ~ label,
.field select:focus ~ label { color: var(--accent); }

/* The placeholder is guidance for someone who is already typing, not a second label — so
   it waits until the field is actually focused. */
.field input::placeholder,
.field textarea::placeholder {
  color: var(--faint);
  opacity: 0;
  transition: opacity .25s ease .05s;
}
.field input:focus::placeholder,
.field textarea:focus::placeholder { opacity: 1; }

.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  outline: none;
  border-color: var(--accent);
  /* The insets have to be restated or the focus ring flattens the field's glass edges. */
  box-shadow:
    inset 0 1px 0 var(--glass-edge),
    inset 0 -1px 0 var(--glass-under),
    0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
}

/* Only ever after the field has been left — never while it is being typed into. */
.field input:not(:placeholder-shown):not(:focus):invalid,
.field textarea:not(:placeholder-shown):not(:focus):invalid { border-color: color-mix(in srgb, #d1443c 60%, var(--line)); }

/* Submitting hands off to Netlify and navigates, so the button's job is only to show that
   the press registered and to refuse a second one. */
.form button[data-sending] { pointer-events: none; opacity: .82; }
.form button .spinner {
  width: 15px; height: 15px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .38);
  border-top-color: #fff;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Honeypot — a real person never sees this, a bot fills it in. */
.form .gotcha { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form button {
  font: inherit;
  justify-self: start;
  border: 0;
  cursor: pointer;
}

.form-note { font-size: .86rem; color: var(--faint); margin: 0; }

.contact-aside {
  border-radius: 18px;
  padding: 26px 26px 28px;
}
.contact-aside h3 { margin-bottom: 6px; }
.contact-aside p { font-size: .95rem; color: var(--muted); }

/* Contact is its own page now, so its heading is the page's h1 rather than a section's h2.
   It takes the h2 size regardless: the h1 scale is sized for the hero, where a headline has
   the full width of the window, and it swamps a two-column layout. */
.contact-page h1 { font-size: clamp(1.9rem, 4.2vw, 2.9rem); letter-spacing: -0.028em; }

/* ---------- scroll-driven motion ---------- */

/* Everything below is written so the *finished* state is the CSS default. The script sets
   the starting state itself, and only when it is going to animate — so with JavaScript off,
   or with reduced motion asked for, the page is simply the completed version rather than a
   pile of invisible elements waiting for an event that never comes. */

.route-band { position: relative; background: var(--sunken); overflow: hidden; }

/* A surveyor's grid behind the road, faded out at the edges and drifting a little against
   the scroll so the road reads as sitting above something rather than on a flat panel. */
.route-band::before {
  content: "";
  position: absolute; inset: -10% -5%;
  pointer-events: none;
  background-image:
    linear-gradient(to right, color-mix(in srgb, var(--ink) 6%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--ink) 6%, transparent) 1px, transparent 1px);
  background-size: 46px 46px;
  mask-image: radial-gradient(ellipse 74% 62% at 50% 50%, #000 20%, transparent 78%);
  -webkit-mask-image: radial-gradient(ellipse 74% 62% at 50% 50%, #000 20%, transparent 78%);
  will-change: transform;
}
/* Pure CSS or nothing: there is no JavaScript fallback for this one, because a backdrop
   that does not drift is simply a backdrop. */
@supports (animation-timeline: view()) {
  .route-band::before {
    animation: grid-drift linear both;
    animation-timeline: view();
    animation-range: cover;
  }
  @keyframes grid-drift {
    from { transform: translate3d(0, -34px, 0); }
    to   { transform: translate3d(0, 34px, 0); }
  }
}
.route-band > .wrap { position: relative; }

.route-figure { margin: 52px 0 0; }
.route-figure svg { display: block; width: 100%; height: auto; overflow: visible; }

/* The bright point at the end of the line being drawn. Sized in user units because it
   lives inside the 1000 × 199 viewBox with everything else. */
.route-head { fill: var(--accent-2); opacity: 0; }
.route-glow { fill: var(--accent-2); opacity: 0; filter: blur(6px); }

/* Drawn twice: a wide casing in the page colour underneath, so where the road doubles back
   on itself the line reads as one road crossing another rather than a smudge. */
.route-casing {
  fill: none; stroke: var(--sunken); stroke-width: 13;
  stroke-linecap: round; stroke-linejoin: round;
}
.route-line {
  fill: none; stroke: var(--accent); stroke-width: 5.5;
  stroke-linecap: round; stroke-linejoin: round;
}
.route-pin { fill: var(--accent); stroke: var(--sunken); stroke-width: 4; }
.route-pin-start { fill: var(--sunken); stroke: var(--accent); stroke-width: 5; }
.route-label {
  fill: var(--muted); font-size: 20px; font-weight: 600;
  font-family: inherit; letter-spacing: .01em;
}

.route-readout {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 12px 44px;
  margin: 40px 0 0; padding: 0; list-style: none;
}
.route-readout div { text-align: center; }
.route-readout b {
  display: block;
  font-size: clamp(1.5rem, 3.2vw, 2.1rem); font-weight: 700;
  letter-spacing: -0.03em; line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.route-readout span { display: block; margin-top: 6px; font-size: .88rem; color: var(--muted); }

/* ---------- progress ring ---------- */

.ring-figure {
  position: relative;
  width: 178px; height: 178px;
  margin: 0 auto 52px;
}
.ring-figure svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-track { fill: none; stroke: var(--line); stroke-width: 13; }
.ring-fill {
  fill: none; stroke: var(--accent); stroke-width: 13; stroke-linecap: round;
}
.ring-centre {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
}
/* The counter swaps the digits out as it runs, so the line has to be allowed to keep its
   own width — left to wrap, "51 h 0 m" breaks into four lines inside the ring. */
.ring-centre b {
  display: block; font-size: 1.45rem; font-weight: 700;
  letter-spacing: -0.03em; font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Direct child only: the counters are spans too, and turning those into blocks stacks
   "51 h 0 m" down the middle of the ring one token per line. */
.ring-centre > span { display: block; margin-top: 4px; font-size: .82rem; color: var(--muted); }

/* ---------- the sun going down ---------- */

/* `--dusk` runs 0 (late afternoon) to 1 (dark) as the band crosses the viewport. It sits at
   1 by default, which is the night the section always was. */
.night-band { --dusk: 1; }
/* Deep enough that the band's near-white type keeps its contrast at every point in the
   transition — a literal sunset palette is far too light under this text. */
.night-band .sky {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(170deg, #1b2550 0%, #43315f 48%, #6b3a2e 100%);
  opacity: calc(1 - var(--dusk));
}
.night-band .sun {
  position: absolute; left: 74%; top: 6%;
  width: clamp(90px, 14vw, 150px); aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, #ffd9a3 0%, #ffb057 55%, rgba(255, 150, 70, 0) 72%);
  pointer-events: none;
  transform: translateY(calc(var(--dusk) * 300px));
  opacity: calc(1 - var(--dusk) * .9);
}
/* The same disc in the colour a low sun actually goes, faded in over the top as it drops,
   so it reddens on the way down instead of simply dimming. */
.night-band .sun::after {
  content: "";
  position: absolute; inset: -6%;
  border-radius: 50%;
  background: radial-gradient(circle, #ffb46e 0%, #f2683a 52%, rgba(214, 78, 40, 0) 74%);
  opacity: calc(var(--dusk) * 1.15);
}

/* The light the sun leaves along the bottom edge of the band after it has gone. */
.night-band .horizon {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 42%;
  pointer-events: none;
  background: linear-gradient(0deg, rgba(255, 138, 74, .34) 0%, rgba(255, 120, 80, .1) 34%, transparent 78%);
  opacity: calc((1 - var(--dusk)) * .9 + var(--dusk) * .12);
}

/* Stars are written by the script and arrive with the dark, each on its own slow cycle so
   the field never pulses in step. */
.night-band .stars { position: absolute; inset: 0; pointer-events: none; opacity: var(--dusk); }
.night-band .stars i {
  position: absolute;
  width: 2px; height: 2px;
  border-radius: 50%;
  background: #fff;
  /* The still frame is a lit star field; the twinkle is the part that needs the band to
     be on screen to be worth running. */
  opacity: .55;
}
html.js .stars.in-view i {
  animation: twinkle var(--dur, 5s) ease-in-out var(--delay, 0s) infinite;
}
@keyframes twinkle {
  0%, 100% { opacity: .22; transform: scale(.85); }
  50%      { opacity: .95; transform: scale(1); }
}

/* On a narrow screen the text column runs the full width, so the sun has nowhere to sit
   beside it — lift it above the heading rather than letting it hang behind the words. */
@media (max-width: 620px) {
  .night-band .sun {
    left: 66%; top: -3%;
    width: clamp(76px, 24vw, 110px);
  }
}

/* ---------- reveal ---------- */

/* Gated on `html.js` like every other starting state on this page. Without it the section
   that is meant to fade in is simply a section — which is what a reader with no JavaScript,
   or with reduced motion asked for, gets, and also what happens if the script below ever
   fails before it reaches the observer. */
html.js .reveal { opacity: 0; transform: translateY(18px); }
html.js .reveal.in {
  opacity: 1; transform: none;
  transition: opacity .8s var(--ease), transform .8s var(--ease);
  transition-delay: calc(var(--i, 0) * 110ms);
}

/* ---------- parallax, handed to the browser where it will take it ---------- */

/* This is the one piece of the scroll work that is purely a function of position, which
   means it can be a real animation on a view timeline instead of a transform written from
   JavaScript every frame — no per-frame layout read, and it runs off the main thread. The
   script checks for the same support and leaves these elements alone when it finds it. */
.par { will-change: transform; }
@supports (animation-timeline: view()) {
  .par {
    animation: parallax linear both;
    animation-timeline: view();
    animation-range: cover;
  }
  @keyframes parallax {
    from { transform: translate3d(0, calc(var(--par, 22px) * 1), 0); }
    to   { transform: translate3d(0, calc(var(--par, 22px) * -1), 0); }
  }
}


@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .btn:hover, .card:hover { transform: none; }
  .btn:hover::after { animation: none; }
  .night-band { --dusk: 1; }
  .night-band .sun { transform: none; }
  html.js .stars.in-view i { animation: none; }
  .night-band .stars i { opacity: .6; }
  html.js .aurora.in-view i { animation: none; will-change: auto; }
  /* Each scene settles on its finished frame — the signature fully written with the points
     it is stored as showing, the route drawn with both pins down — rather than vanishing.
     The travelling head and the pulse are the two pieces with no meaningful still state,
     so those are the two that go. Nothing that carries meaning is lost. */
  html.js .scene:not(.played) .sig-path, html.js .scene.in-view .sig-path,
  html.js .scene:not(.played) .rt-line,  html.js .scene.in-view .rt-line { stroke-dashoffset: 0; animation: none; }
  html.js .scene:not(.played) .sig-node, html.js .scene.in-view .sig-node,
  html.js .scene:not(.played) .rt-pin,   html.js .scene.in-view .rt-pin { opacity: 1; transform: none; animation: none; }
  html.js .scene.in-view .ping-ring { animation: none; }
  .boundaries .ping-ring { opacity: .45; }
  .rt-head, .rt-glow,
  html.js .scene.in-view .rt-head, html.js .scene.in-view .rt-glow { display: none; animation: none; }
  .glass::before { transition: none; }
  .par, .card, .showcase-frame { animation: none !important; transform: none !important; }
  .showcase-frame { transition: opacity .2s linear; }
  html.js .showcase-step { opacity: 1; filter: none; }
  html.js .reveal ul.ticks li::before,
  html.js .showcase-step ul.ticks li::before { clip-path: none; transition: none; }
}

/* ==========================================================================
   --- document pages ---

   Privacy, terms, the changelog, the thanks page and the 404 are all the same thing: one
   narrow column of prose under the same chrome as the landing page. They set `body.doc`,
   and every rule below is scoped to it, so the landing page's type scale — which is sized
   for a headline you read from across the room — cannot reach a paragraph of policy.

   These rules were `legal.css` until the site grew past two pages.
   ========================================================================== */

.doc main {
  max-width: 46rem;
  margin: 0 auto;
  padding: 3.5rem 1.5rem 4rem;
}

/* The base scale clamps h1 up to 4.4rem, which is a hero size. Prose wants a page title. */
.doc h1 { font-size: clamp(2.1rem, 5vw, 2.8rem); letter-spacing: -0.03em; margin: 0 0 .4rem; }
.doc h2 { font-size: 1.22rem; letter-spacing: -0.018em; margin: 2.6rem 0 .5rem; }
.doc h3 { margin: 1.7rem 0 .2rem; }

.doc p  { margin: 1rem 0 0; }
.doc ul { padding-left: 1.25rem; margin: .8rem 0 0; }
.doc li { margin: .4rem 0; }

.doc .updated { color: var(--muted); font-size: .92rem; margin: 0 0 2.2rem; }

.doc .lead {
  border-left: 3px solid var(--accent);
  padding: .2rem 0 .2rem 1.1rem;
  margin: 1.6rem 0 0;
  font-size: 1.06rem;
}

.doc .note,
.doc .callout {
  border-radius: .9rem;
  padding: 1rem 1.2rem;
  margin: 2rem 0 0;
  font-size: .96rem;
}
.doc .note {
  background: color-mix(in srgb, var(--accent) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
}
.doc .callout {
  background: color-mix(in srgb, var(--warn) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 26%, transparent);
}

/* The footer sits inside `main` on these pages, so it takes the same column as the text
   rather than running the full width the way the landing page's does. */
.doc footer {
  margin-top: 3.5rem;
  padding: 1.4rem 0 0;
  border-top: 1px solid var(--line);
  color: var(--faint);
  font-size: .87rem;
}
.doc footer .links { margin-bottom: .6rem; }

/* No sliding pill on these pages — there are no sections to track, so the current page is
   simply stated and stays stated. */
.doc .nav-links a[aria-current="page"] { color: var(--accent); }

/* ---------- the changelog ---------- */

/*
  One entry per released version, newest first. The version number is the heading and the
  date sits beside it rather than under it, so the eye can run down the left edge and find
  a version without reading anything.
*/
.release { margin-top: 2.9rem; }
.release + .release {
  padding-top: 2.4rem;
  border-top: 1px solid var(--line);
}

.doc .release h2 {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .7rem;
  font-size: 1.5rem; letter-spacing: -0.024em;
  margin: 0;
}
.release .when {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif;
  font-size: .86rem; font-weight: 500; letter-spacing: 0;
  color: var(--faint);
}

/* Each change is a short heading and a paragraph, the same shape as the App Store's What's
   New — because it is the same copy. */
.doc .release h3 { font-size: 1.04rem; margin: 1.8rem 0 .1rem; }
.doc .release h3 + p { margin-top: .35rem; }

.release .summary { color: var(--muted); margin-top: .8rem; }
