@charset "utf-8";

/* ============================================================
   CORE — reset, tokens, type scale, layout primitives.

   The previous build shipped a 240 KB compiled Tailwind bundle of
   which the markup used maybe three percent. Everything the page
   needs is now written by hand, in two files, against one token
   set. Nothing below is generated.
   ============================================================ */

/* ── type faces ────────────────────────────────────────────────
   The platform UI face carries display and body. It already ships
   optical sizing, tracking tables and legibility tuning for the
   device it renders on, which is exactly what a display face at
   9rem needs and what no self-hosted webfont here supplies.

   Geist Mono is kept — the one font file still loaded — and scoped
   to technical metadata: dates, counts, section indices, labels.
   That is where a tabular, engineered voice reads as precision
   rather than as costume.
   ──────────────────────────────────────────────────────────── */

@font-face {
  font-family: "Geist Mono";
  src: url("../fonts/GeistMonoVF.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  /* Only the glyphs metadata actually uses. Keeps the subset the
     browser must decode small and avoids blocking on CJK ranges. */
  unicode-range: U+0020-007E, U+00B0, U+2013, U+2014, U+2192, U+2022;
}

/* ── reset ─────────────────────────────────────────────────── */

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

* { margin: 0; padding: 0; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Clears the floating nav plus a breath of air when a hash lands. */
  scroll-padding-top: calc(var(--nav-h) + 2.5rem);

  /* ── the scrollbar ──────────────────────────────────────────
     Taken out of the composition, not out of the browser. Every
     input still works exactly as it did — wheel, trackpad, swipe,
     Space/PgDn, Home/End, find-in-page, anchor jumps — because
     nothing here touches overflow or hijacks an event. Only the
     paint is suppressed. Wayfinding moves to the frame rail on
     the right and the hairline at the top, which say the same
     thing in the language of the rest of the page.             */
  scrollbar-width: none;              /* Firefox */
  -ms-overflow-style: none;           /* legacy Edge */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar { width: 0; height: 0; display: none; }

img, svg, video, canvas, picture {
  display: block;
  max-width: 100%;
}

button, input, textarea, select {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  background: none;
  border: 0;
}

button { cursor: pointer; -webkit-tap-highlight-color: transparent; }

ul, ol { list-style: none; }

a {
  color: inherit;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}

:where(h1, h2, h3, h4, h5, h6) { font-size: inherit; font-weight: inherit; }

/* ── tokens ────────────────────────────────────────────────── */

:root {
  /* Fonts */
  --font-ui:
    -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text",
    "Segoe UI Variable Display", "Segoe UI", Roboto, "Helvetica Neue",
    Arial, system-ui, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* ── Fluid type scale ──────────────────────────────────────
     Tracking is size-specific by design: display type reads too
     loose as it grows and wants negative tracking; body sits near
     zero; small uppercase labels want positive tracking to stay
     legible. A single letter-spacing value would be wrong at one
     end of this range or the other.                             */

  --t-display:  clamp(2.75rem, 10.5vw, 8.5rem);
  --t-display-lh: 0.94;
  --t-display-ls: -0.045em;

  --t-1:  clamp(2.125rem, 6vw, 4.5rem);
  --t-1-lh: 1.02;
  --t-1-ls: -0.035em;

  --t-2:  clamp(1.75rem, 4.2vw, 3rem);
  --t-2-lh: 1.08;
  --t-2-ls: -0.028em;

  --t-3:  clamp(1.3125rem, 2.3vw, 1.75rem);
  --t-3-lh: 1.22;
  --t-3-ls: -0.018em;

  --t-lead: clamp(1.125rem, 1.55vw, 1.4375rem);
  --t-lead-lh: 1.5;
  --t-lead-ls: -0.012em;

  --t-body: clamp(1rem, 0.95vw, 1.0625rem);
  --t-body-lh: 1.62;
  --t-body-ls: -0.004em;

  --t-small: 0.9375rem;
  --t-small-lh: 1.55;

  --t-label: 0.6875rem;
  --t-label-ls: 0.14em;

  /* ── Rhythm ────────────────────────────────────────────────
     One gutter and one section padding, both fluid, both used
     everywhere. Section spacing is the loudest signal of quality
     in a layout like this, so it is defined once and never
     overridden per-section.                                     */

  --gutter:   clamp(1.25rem, 5vw, 3.5rem);
  --pad-y:    clamp(5rem, 12vh, 9.5rem);
  --pad-y-sm: clamp(3.5rem, 8vh, 6rem);

  /* One content column for the whole document — navigation, hero,
     every section, footer. Three different maxima was the previous
     build's most visible flaw: the wordmark and the section copy
     started at different x. */
  --content: 1180px;
  --measure: 62ch;     /* comfortable reading measure */

  --nav-h: 56px;

  /* Radii — three steps, used intentionally. */
  --r-sm: 8px;
  --r-md: 16px;
  --r-lg: 28px;

  /* ── Motion ────────────────────────────────────────────────
     Two curves and five durations for the entire site. The
     built-in CSS easings are too weak to read as deliberate;
     these are the strong variants.

     Division of labour: CSS transitions own predetermined motion
     (hover, state, reveal) because they run off the main thread
     and stay smooth while the page is still working. Springs in
     JS own anything a pointer can grab, because only a spring
     carries velocity through an interruption.

     Everything a user can trigger repeatedly stays under 300ms.
     Only the scroll reveal — seen once, on a marketing surface —
     is allowed past it.                                         */

  --ease-out:    cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);

  --dur-press:  160ms;   /* press feedback                     */
  --dur-ui:     200ms;   /* hover, colour, small state change  */
  --dur-panel:  260ms;   /* larger state, morphs, card hover   */
  --dur-reveal: 620ms;   /* reveal: opacity                    */
  --dur-settle: 780ms;   /* reveal: movement, settles after    */
  --dur-theme:  400ms;   /* whole-surface brightness change    */
}

/* ── palettes ──────────────────────────────────────────────── */

/* ── the dark composition ──────────────────────────────────────
   Dark is not a mode here. It is the room the film is shown in,
   and it is what every visitor gets unless they have explicitly
   asked for the other one.

   Two decisions carry the whole palette:

   1 · The ground is #050505, not #000000. A true-black page has
       nowhere below it to go, so the first surface step has to be
       bright enough to see — which is how dark themes end up with
       gray cards floating on black. Lifting the ground a hair buys
       three legible surface steps above it, all still reading as
       black in a dark room.

   2 · The foreground ramp is solid values, not white at low alpha.
       Alpha was the actual cause of the mobile legibility problem:
       every gray in the old build was computed against whatever
       the animated field happened to be painting underneath, so
       secondary copy measured 4:1 in a dark passage and less in a
       bright one. These are measured against --bg and hold.

         --fg    18.7:1     --fg-3   7.1:1
         --fg-2  12.2:1     --fg-4   4.6:1  ← the floor for text

       --fg-5 is below the text floor by design and is only ever
       allowed to draw a rule, a tick or a dot.
   ──────────────────────────────────────────────────────────── */

:root,
[data-theme="dark"] {
  color-scheme: dark;

  --bg:        #050505;   /* ground */
  --bg-elev:   #0a0a0b;   /* alternating band */
  --bg-raise:  #121214;   /* cards */
  --bg-high:   #1a1a1d;   /* a card on a card */

  --fg:        #f5f5f7;
  --fg-2:      #c8c8ce;
  --fg-3:      #9b9ba3;
  --fg-4:      #77777f;
  --fg-5:      #45454b;

  --hair:      rgb(255 255 255 / 0.13);
  --hair-soft: rgb(255 255 255 / 0.07);

  --accent:      #ff453a;   /* the identity red, at Apple's dark value */
  --accent-text: #ff6b62;   /* lifted for small text on the ground */
  --accent-dim:  rgb(255 69 58 / 0.16);

  --glass:      rgb(8 8 9 / 0.72);
  --glass-edge: rgb(255 255 255 / 0.10);
  --scrim:      rgb(0 0 0 / 0.68);

  --shadow-md: 0 4px 18px rgb(0 0 0 / 0.55);
  --shadow-lg: 0 24px 70px rgb(0 0 0 / 0.72);
}

/* ── the phone ─────────────────────────────────────────────────
   A phone is held at arm's length, is frequently outdoors, and is
   very often at half brightness to save battery. None of that is
   true of the monitor this was designed on, and on a true-black
   design the first thing that costs is separation: surfaces vanish
   into the ground and the whole page reads as one dark sheet you
   cannot pick anything out of.

   So on a phone the ground stays deep but everything above it is
   lifted decisively — surfaces become visibly lighter panels,
   borders get real presence, and the whole text ramp brightens a
   step. The hierarchy is preserved; it is just all shifted up into
   a range a phone can actually render in daylight.              */

@media (max-width: 899px) {
  :root, [data-theme="dark"] {
    --bg-elev:   #111114;   /* alternating band — now clearly a band */
    --bg-raise:  #191920;   /* cards lift off the ground             */
    --bg-high:   #24242b;   /* a card on a card                      */

    --fg:        #ffffff;
    --fg-2:      #dedee4;
    --fg-3:      #b4b4bc;
    --fg-4:      #8e8e96;
    --fg-5:      #5e5e66;

    --hair:      rgb(255 255 255 / 0.22);
    --hair-soft: rgb(255 255 255 / 0.12);
  }
}

/* Light is kept as a deliberate alternative for anyone who asks
   for it — it is no longer what an unfamiliar visitor is handed.
   It is not an inversion either: Apple's paper, pure white ground,
   #f5f5f7 bands, #1d1d1f ink, and the red dropped to a value that
   can carry text. The ramp is solid here too, for the same reason
   it is solid above. */

[data-theme="light"] {
  color-scheme: light;

  --bg:        #ffffff;
  --bg-elev:   #f5f5f7;
  --bg-raise:  #ffffff;
  --bg-high:   #ffffff;

  --fg:        #1d1d1f;
  --fg-2:      #4b4b50;
  --fg-3:      #6e6e75;
  --fg-4:      #86868b;
  --fg-5:      #c4c4c9;

  --hair:      rgb(0 0 0 / 0.12);
  --hair-soft: rgb(0 0 0 / 0.06);

  --accent:      #d70015;
  --accent-text: #d70015;
  --accent-dim:  rgb(215 0 21 / 0.10);

  --glass:      rgb(255 255 255 / 0.72);
  --glass-edge: rgb(0 0 0 / 0.07);
  --scrim:      rgb(0 0 0 / 0.30);

  --shadow-md: 0 4px 18px rgb(0 0 0 / 0.08);
  --shadow-lg: 0 24px 70px rgb(0 0 0 / 0.13);
}

/* ── document ──────────────────────────────────────────────── */

body {
  font-family: var(--font-ui);
  font-size: var(--t-body);
  line-height: var(--t-body-lh);
  letter-spacing: var(--t-body-ls);
  font-optical-sizing: auto;
  color: var(--fg);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* clip, not hidden: hidden on <body> would make the whole
     document a scroll container and break position: sticky. */
  overflow-x: clip;
  /* Theme change is a large surface brightness jump; ease it. */
  transition: background-color var(--dur-theme) var(--ease-in-out),
              color var(--dur-theme) var(--ease-in-out);
}

/* ── layout primitives ─────────────────────────────────────── */

.shell {
  width: 100%;
  max-width: calc(var(--content) + var(--gutter) * 2);
  margin-inline: auto;
  /* The gutter is the floor, the notch is the override. On a
     landscape phone the safe area is wider than the gutter and
     wins; everywhere else it resolves to 0 and the gutter wins. */
  padding-inline:
    max(var(--gutter), env(safe-area-inset-left))
    max(var(--gutter), env(safe-area-inset-right));
}

.section { padding-block: var(--pad-y); }
.section--tight { padding-block: var(--pad-y-sm); }
.section--elev { background: var(--bg-elev); }

/* Hairline between bands, drawn on the element rather than as a
   border so it can be inset to the content column. */
.section--ruled { border-top: 1px solid var(--hair-soft); }

/* ── type utilities ────────────────────────────────────────── */

.display {
  font-size: var(--t-display);
  line-height: var(--t-display-lh);
  letter-spacing: var(--t-display-ls);
  font-weight: 600;
  text-wrap: balance;
}

.h1 {
  font-size: var(--t-1);
  line-height: var(--t-1-lh);
  letter-spacing: var(--t-1-ls);
  font-weight: 600;
  text-wrap: balance;
}

.h2 {
  font-size: var(--t-2);
  line-height: var(--t-2-lh);
  letter-spacing: var(--t-2-ls);
  font-weight: 600;
  text-wrap: balance;
}

.h3 {
  font-size: var(--t-3);
  line-height: var(--t-3-lh);
  letter-spacing: var(--t-3-ls);
  font-weight: 600;
}

.lead {
  font-size: var(--t-lead);
  line-height: var(--t-lead-lh);
  letter-spacing: var(--t-lead-ls);
  color: var(--fg-2);
  text-wrap: pretty;
}

.body { color: var(--fg-2); text-wrap: pretty; }
.small { font-size: var(--t-small); line-height: var(--t-small-lh); color: var(--fg-3); }

.measure { max-width: var(--measure); }

/* Small mono label. The one place uppercase and wide tracking are
   allowed, and the one place the mono face appears. */
.label {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: 500;
  line-height: 1;
  letter-spacing: var(--t-label-ls);
  text-transform: uppercase;
  color: var(--fg-3);
  font-variant-numeric: tabular-nums;
}

.label--accent { color: var(--accent-text); }

.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* ── accessibility ─────────────────────────────────────────── */

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: fixed;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 300;
  padding: 0.7rem 1.1rem;
  border-radius: var(--r-sm);
  background: var(--fg);
  color: var(--bg);
  font-size: var(--t-small);
  font-weight: 550;
  transform: translateY(-200%);
}
.skip-link:focus-visible { transform: none; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 3px;
}
/* Pointer interaction never draws the ring; only keyboard does. */
:focus:not(:focus-visible) { outline: none; }

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

/* ── reveal primitives ─────────────────────────────────────────
   Four variants, one system. Every entrance on this page is one of
   these — nothing hand-rolls its own, so the whole document
   arrives in a single motion language.

     (default)  copy, list rows, metadata — rises into place
     mask       display type — a line reveal, wiped up off its
                baseline, the way a title card resolves
     lift       cards — a touch of scale, so they arrive with depth
     frame      visuals — clipped open from the bottom, no movement

   Entering, so: ease-out. Opacity finishes first and the movement
   settles after it, which is what makes the arrival read as
   weight rather than as a fade.

   The runtime sets only `transition-delay`. Everything else lives
   here, so the values are in one place and reviewable.
   ──────────────────────────────────────────────────────────── */

[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition:
    opacity   var(--dur-reveal) var(--ease-out),
    transform var(--dur-settle) var(--ease-out),
    clip-path var(--dur-settle) var(--ease-out),
    filter    var(--dur-reveal) var(--ease-out);
  will-change: opacity, transform;
}

[data-reveal="mask"] { transform: translate3d(0, 0.12em, 0); }
[data-reveal="lift"] { transform: translate3d(0, 22px, 0) scale(0.985); }
[data-reveal="frame"] { transform: none; }

/* ── why the clip is armed late ──────────────────────────────
   A fully clipped element has an empty intersection rectangle, so
   IntersectionObserver reports it as not intersecting — for ever.
   Putting the closed clip in the resting state deadlocks the
   reveal: the element can never become visible enough to trigger
   the thing that would make it visible.

   So the clip is armed by the runtime one frame before the reveal
   runs, not held in the resting state. Nothing is lost visually:
   the resting state is already opacity 0, and opacity does not
   affect intersection.

   110% rather than 100%: descenders and the leading below the
   baseline have to clear the mask too, or the reveal opens on a
   sliver of the tallest glyphs.
   ──────────────────────────────────────────────────────────── */

[data-reveal="mask"].is-arming  { clip-path: inset(0 0 110% 0); }
[data-reveal="frame"].is-arming { clip-path: inset(0 0 100% 0); }

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

/* Only the variants that arm a clip get one reset — giving every
   revealed element a clipping context would cut off focus rings
   and the live-dot's pulse ring. */
[data-reveal="mask"].is-revealed,
[data-reveal="frame"].is-revealed { clip-path: inset(0 0 0 0); }

/* Blur-to-sharp, on the two hero lines only, at desktop widths
   only. Blur is the one genuinely expensive property in this file,
   so it is spent on the first thing a visitor sees resolving —
   twice per visit — rather than sprinkled over every heading. */
@media (min-width: 900px) {
  .hero__line[data-reveal="mask"] { filter: blur(6px); }
  .hero__line[data-reveal="mask"].is-revealed { filter: blur(0); }
}

/* ── reduced motion ────────────────────────────────────────────
   Not "no feedback" — a gentler, non-vestibular equivalent.
   Movement is dropped; the opacity change that tells you content
   arrived is kept.                                              */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* Re-permit the one transition that carries meaning. Movement,
     clipping and blur all go; the opacity change that tells you
     content arrived stays. */
  [data-reveal],
  [data-reveal="mask"],
  [data-reveal="lift"],
  [data-reveal="frame"],
  [data-reveal].is-revealed,
  [data-reveal="mask"].is-revealed,
  [data-reveal="frame"].is-revealed,
  .hero__line[data-reveal="mask"] {
    transform: none;
    clip-path: none;
    filter: none;
  }
  [data-reveal].is-revealed { transition: opacity 220ms linear !important; }
}

/* ── reduced transparency ──────────────────────────────────────
   Frost the glass into a solid. Blur is the expensive half and
   the half that hurts legibility, so it goes first.             */

@media (prefers-reduced-transparency: reduce) {
  :root, [data-theme="dark"] { --glass: #0a0a0c; }
  [data-theme="light"]       { --glass: #ffffff; }

  .glass,
  .glass-strong {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }

  /* Someone who has asked for less translucency has not asked for
     a blurred headline either. */
  .hero__line[data-reveal="mask"],
  .hero__line[data-reveal="mask"].is-revealed { filter: none; }
}

/* ── increased contrast ────────────────────────────────────────
   Raise the foreground ramp and give translucent surfaces a
   defined edge.                                                 */

@media (prefers-contrast: more) {
  :root, [data-theme="dark"] {
    --fg-2: #e4e4e9;
    --fg-3: #c2c2c9;
    --fg-4: #a3a3ab;
    --fg-5: #6e6e76;
    --hair: rgb(255 255 255 / 0.28);
    --hair-soft: rgb(255 255 255 / 0.18);
  }
  [data-theme="light"] {
    --fg-2: #2c2c30;
    --fg-3: #45454a;
    --fg-4: #5c5c62;
    --fg-5: #8e8e96;
    --hair: rgb(0 0 0 / 0.30);
    --hair-soft: rgb(0 0 0 / 0.20);
  }
  .glass, .glass-strong { border-color: var(--fg-4); }
}

/* ── print ─────────────────────────────────────────────────── */

@media print {
  .site-nav, .hero-canvas, .theme-toggle, .nav-sheet, .skip-link { display: none !important; }
  [data-reveal] { opacity: 1 !important; transform: none !important; }
  body { background: #fff; color: #000; }
}
