/* Hourroll — brand system v1, 14 August 2026.
   Built from "Hourroll Brand System.dc.html" (Claude Design, project
   02f8cb32-b076-431b-9c0d-3f891f5ea94f). The written spec, the marks, the font
   sources and the measured contrast table live in ../../../brand/.

   Three faces, self-hosted, latin + latin-ext, 130,420 B against a 163,840 B
   ceiling. Newsreader's opsz axis is pinned at 28 — see brand/fonts/build-fonts.py
   for why. wght stays variable because the dark-surface rule needs 600 on paper
   and 650 on ink. */

@font-face {
  font-family: 'Newsreader';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(/static/fonts/newsreader-var.1cd6caf0.woff2) format('woff2');
}
@font-face {
  font-family: 'Newsreader';
  font-style: italic;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/newsreader-italic.d8793b1d.woff2) format('woff2');
}
@font-face {
  font-family: 'Plus Jakarta Sans';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(/static/fonts/jakarta-var.c7b1e3e0.woff2) format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/jetbrains-mono-500.615a7673.woff2) format('woff2');
}

:root {
  /* light ground */
  --paper: #FAF8F4;
  --surface: #FFFFFF;
  --ink: #221E33;
  --muted: #57536B;

  /* indigo — the brand, one job each */
  --brand: #4338CA;
  --brand-press: #372DA6;
  --brand-soft: #EDEBFB;
  --brand-lift: #A9A2F7;      /* indigo for ink surfaces only */

  /* amber is a signal, not a second brand colour: spent vs left, and errors */
  --signal: #A34A06;          /* light surfaces only */
  --signal-bright: #F59E0B;   /* ink surfaces only */
  --signal-soft: #FFF3DF;
  --signal-soft-line: #F0D9AE;

  /* rules */
  --line: #E7E3DA;            /* divides */
  --line-mid: #D8D2C6;        /* section rules that carry weight */
  --line-strong: #938B7A;     /* the only border allowed on a form control */
  --hairline: #F0EDE6;        /* inside a data list */

  /* ink ground */
  --ink-raised: #2C2740;
  --ink-line: #3B3552;
  --ink-field-line: #6B6483;
  --on-ink: #F6F4F0;
  --on-ink-muted: #B4AEC4;

  --r-control: 11px;
  --r-card: 18px;
  --r-tile: 13px;
  /* Not in the brand package's radius set, which names three. It is already
     drawn there — .chip and .tag are both 999px — but only on labels, never on
     a control. Naming it makes the form's controls and the buttons pills. */
  --r-pill: 999px;

  --font-display: 'Newsreader', Georgia, 'Times New Roman', serif;
  --font-body: 'Plus Jakarta Sans', -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', 'SF Mono', ui-monospace, monospace;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

/* manipulation says none of these is a double-tap-to-zoom target, so a mobile
   browser fires the click without first waiting to see whether a second tap is
   coming. Pinch-zoom is a separate gesture and is untouched; summary is in the
   list for the FAQ.

   Honest about the size of the win: every page here already declares
   <meta name="viewport" content="width=device-width>, and both Chrome on
   Android and Safari on iOS have dropped the ~300ms wait on such pages for
   years. So this GUARANTEES the behaviour rather than delivering it — worth
   keeping as the belt to that braces, not worth crediting with a speed-up. */
a, button, input, select, textarea, summary, label { touch-action: manipulation; }

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

body {
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.65;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* 404 and privacy are shorter than the viewport; without this the footer
     floats and leaves a band of bare paper under it. */
  min-height: 100vh;
  /* dvh second, so a browser that does not know the unit keeps the vh above.
     On iOS Safari 100vh is the toolbar-less height, so the page is taller than
     what is visible and it jumps as the bar hides; dvh is the live one. */
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
main { flex: 1 0 auto; }
.site-header, .site-footer { flex: none; }
@media (min-width: 768px) { body { font-size: 1.03125rem; } }   /* 16.5px */

/* ---------- the grid: 16 / 20 / 40 / 72 / 144 px gutters ---------- */
.wrap { width: 100%; max-width: 1440px; margin-inline: auto; padding-inline: 16px; }
@media (min-width: 390px)  { .wrap { padding-inline: 20px; } }
@media (min-width: 768px)  { .wrap { padding-inline: 40px; } }
@media (min-width: 1024px) { .wrap { padding-inline: 72px; } }
@media (min-width: 1440px) { .wrap { padding-inline: 144px; } }

/* ---------- type ---------- */
h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.02em;
  text-wrap: pretty;
}
h1 { font-size: clamp(2.5rem, 1.53rem + 4.3vw, 4.25rem); line-height: 1.04; letter-spacing: -0.025em; }
h2 { font-size: clamp(2rem, 1.55rem + 2vw, 2.75rem); line-height: 1.08; letter-spacing: -0.022em; }
h3 { font-size: 1.3125rem; line-height: 1.2; letter-spacing: -0.012em; }
@media (min-width: 768px) { h3 { font-size: 1.4375rem; } }

p { max-width: 68ch; }
a { color: var(--brand); }
a:hover { color: var(--brand-press); }

/* Focus is a 2px brand ring with a 2px ground offset, on every surface.
   Never outline:none without a replacement. */
a:focus-visible, button:focus-visible, input:focus-visible, summary:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: 4px;
}
.problem :focus-visible, .cta :focus-visible, .founding :focus-visible,
.site-footer :focus-visible {
  outline-color: var(--brand-lift);
}

.skip {
  position: absolute; left: -9999px; top: 0;
  background: var(--ink); color: var(--on-ink); padding: 10px 16px; z-index: 100;
}
.skip:focus { left: 8px; top: 8px; }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Every section opens with one. Mono, caps, +0.14em, signal on light. */
.eyebrow {
  display: block;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.71875rem;                 /* 11.5px */
  text-transform: uppercase;
  letter-spacing: 0.13em;
  line-height: 1.5;
  color: var(--signal);
  margin-bottom: 14px;
}
@media (min-width: 768px) { .eyebrow { font-size: 0.75rem; letter-spacing: 0.14em; } }

.lede {
  font-size: 1.0625rem;
  line-height: 1.55;
  color: var(--muted);
  max-width: 44ch;
}
@media (min-width: 768px) { .lede { font-size: 1.25rem; } }

.section-intro { color: var(--muted); }

/* ---------- header ---------- */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: rgba(250, 248, 244, 0.94);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}
.site-header .wrap {
  display: flex; align-items: center; justify-content: space-between;
  min-height: 62px; gap: 12px;
}
@media (min-width: 1024px) { .site-header .wrap { min-height: 78px; } }

.logo {
  display: inline-flex; align-items: center; gap: 0.1825em;
  font-family: var(--font-display); font-weight: 600;
  font-size: 1.4375rem; letter-spacing: -0.018em; line-height: 1;
  color: var(--ink); text-decoration: none;
}
.logo svg { display: block; flex: none; }
@media (min-width: 1024px) { .logo { font-size: 1.625rem; } }

.site-nav { display: flex; align-items: center; gap: clamp(14px, 3vw, 34px); }
.site-nav .nav-links { display: flex; gap: 28px; }
.site-nav .nav-links a {
  color: var(--ink); text-decoration: none; font-size: 0.96875rem; padding: 12px 0;
}
.site-nav .nav-links a:hover { color: var(--brand); }
/* Below 768 the anchors would wrap or shrink under 44px. They go; the button
   stays, because it is the only action anyone needs. No hamburger. */
@media (max-width: 767px) { .site-nav .nav-links { display: none; } }
/* Four anchors, not three, since /pricing joined. At 768 the row is 688px of
   content and the four labels plus the logo and the button measure within ~3px
   of it at a 28px gap. 20px is the headroom; it comes back at 1024, where the
   gutters drop the content width by only 32px and the row has 200px spare. */
@media (min-width: 768px) and (max-width: 1023px) { .site-nav .nav-links { gap: 20px; } }

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 52px; padding: 0 26px;
  background: var(--brand); color: var(--paper);
  border: 0; border-radius: var(--r-pill);
  font-family: var(--font-body); font-size: 1rem; font-weight: 600;
  line-height: 1; text-decoration: none; cursor: pointer;
  transition: background-color 180ms ease, transform 180ms cubic-bezier(.2,.7,.3,1);
}
.btn:hover { background: var(--brand-press); color: var(--paper); }
/* Lift, not grow: 1px of travel reads on a pill and cannot reflow anything,
   where a scale would blur the label mid-transition on a low-DPI screen. */
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn:disabled { opacity: 0.6; cursor: default; transform: none; }
/* The global focus rule forces border-radius:4px so a ring hugs a square
   control. On a pill that squares off the ring; these put the radius back. */
.btn:focus-visible { border-radius: var(--r-pill); }
.btn-small {
  min-height: 44px; padding: 0 17px; border-radius: var(--r-pill);
  font-size: 0.90625rem;
}
/* The page's one action, now that the form is behind it. It has to carry the
   weight the three fields used to. */
.btn-lg { min-height: 58px; padding: 0 32px; font-size: 1.0625rem; }
/* The secondary CTA, and it only ever appears on the ink/gradient hero — so it
   is the on-ink variant: paper ground, ink label. #F6F4F0 on #221E33 measures
   14.9:1, and the label stays ink on hover rather than flipping to a colour that
   would have to be re-measured against a moving gradient. */
.btn-quiet { background: var(--on-ink); color: var(--ink); }
.btn-quiet:hover { background: #fff; color: var(--ink); }
@media (min-width: 1024px) {
  .btn { min-height: 56px; }
  .btn-small { min-height: 44px; padding: 0 21px; font-size: 0.96875rem; }
  .btn-lg { min-height: 62px; padding: 0 38px; font-size: 1.125rem; }
}

/* Full width until there is room not to be: a lone button on a phone reads as
   the thing to do, and a 350px-wide tap target cannot be missed. */
.cta-row { margin-top: 26px; }
@media (min-width: 768px) { .cta-row { margin-top: 34px; } }
.cta-row .btn { width: 100%; }
@media (min-width: 520px) { .cta-row .btn { width: auto; } }

/* ---------- hero ---------- */
/* Reworked 2026-08-27 to the chosen landing reference
   (../2026-08-26_Hourroll_Landing-Design-Reference.md): the hero moved from the
   paper ground to the brand's own indigo->ink gradient, centred, with the client
   card pulled down across the seam into .problem.

   Why a gradient and not a photograph: the reference is translated from a
   photography-led source, and Hourroll has no photograph worth the space. The
   card does the job the photograph was doing. The scrim stays regardless — it is
   what keeps type legible over a busy ground. */
.hero {
  /* isolation, because the two pseudo-elements below sit at z-index:-1 and would
     otherwise fall behind the nearest stacking context — the body — and vanish
     under its background. z-index:1 is what actually lets the card overhang into
     .problem: without it the overlap only works while .problem stays unpositioned,
     and the first `position: relative` added there would put the card behind it. */
  position: relative;
  isolation: isolate;
  z-index: 1;
  color: #fff;
  text-align: center;
  background: linear-gradient(157deg, var(--ink) 0%, var(--brand) 100%);
  padding: 44px 0;
}
@media (min-width: 768px)  { .hero { padding: 76px 0 0; } }
@media (min-width: 1024px) { .hero { padding: 104px 0 0; } }

/* Two soft washes and a vertical scrim. inset:0 on both, so neither escapes the
   hero even though the card below deliberately does. */
.hero::before,
.hero::after { content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.hero::before {
  opacity: 0.5;
  background:
    radial-gradient(60% 55% at 78% 18%, rgba(169, 162, 247, 0.5), transparent 70%),
    radial-gradient(48% 44% at 12% 88%, rgba(245, 158, 11, 0.22), transparent 70%);
}
.hero::after {
  background: linear-gradient(180deg, rgba(34, 30, 51, 0.42) 0%,
                                      rgba(34, 30, 51, 0.12) 45%,
                                      rgba(34, 30, 51, 0.5) 100%);
}

/* One centred column now, not the 1.22/0.78 split — the card moved out from
   beside the copy to underneath it.

   flow-root, not block. The card below hangs into .problem with a negative
   bottom margin, and .hero has no bottom padding or border to stop it: under
   plain `block` that margin COLLAPSES straight through .wrap and out of .hero,
   so it shortened the hero and pulled .problem up instead of making the card
   overlap it. Measured 2026-08-27: hero.bottom == card.bottom, overhang 0px, at
   768/1024/1440. The old two-column hero was display:grid, whose margins never
   collapse with its children, which is why this only appeared with the rework.
   flow-root establishes a block formatting context and changes nothing else. */
.hero .wrap { display: flow-root; }
.hero .form-col { max-width: 46rem; margin-inline: auto; }

.hero h1 { margin-top: 14px; color: #fff; }
/* cqi, not vw. The headline's problem was never how wide the WINDOW is, it was
   how wide its own column is — .form-col is the container. Measured across
   320-1440 on 2026-08-15 at the old two-column hero, and RE-MEASURED 2026-08-27
   after the column became a single centred 46rem: the cap binds from ~597px of
   column instead of ~1000px, so desktop saturates at 4.25rem exactly as before,
   and 320px still lands on the 2rem floor. The max-width in ch below is what
   holds the line count now that the column is wider than the headline wants.

   The 2rem floor only binds below ~350px of column. Do not raise it without
   re-measuring 320px, where it is the difference between 2 lines and 4. */
.hero h1 { font-size: clamp(2rem, 11.4cqi, 4.25rem); }
@media (min-width: 768px) { .hero h1 { margin-top: 22px; } }
/* ch resolves against the element's own font-size, so this cap counts HEADLINE
   characters. Putting it on .form-col instead would count 16.5px body ones and
   collapse the line. */
@media (min-width: 768px) { .hero h1 { max-width: 17ch; margin-inline: auto; } }
/* Indigo fails on ink at 2.04:1. brand-lift is the only correct mark here. */
.hero h1 em { font-style: italic; font-weight: 600; color: var(--brand-lift); }
.hero .lede { margin-top: 18px; margin-inline: auto; max-width: 56ch; color: rgba(255, 255, 255, 0.9); }
@media (min-width: 768px) { .hero .lede { margin-top: 24px; } }
.hero .cta-row { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.hero .form-note { color: rgba(255, 255, 255, 0.86); }

/* The eyebrow becomes a chip on this ground: an amber dot carries the status and
   the pill lifts the mono off the gradient. Amber is a signal here, not decoration. */
.hero .eyebrow {
  /* flex-start, not center: the label wraps to two lines under about 430px, and
     centring put the status dot in the gutter between them instead of beside the
     first line. The margin below re-centres it on line one, which is also where
     it lands when the label fits on a single line. */
  display: inline-flex; align-items: flex-start; gap: 9px;
  padding: 8px 15px; border-radius: var(--r-pill, 999px);
  background: rgba(34, 30, 51, 0.42);
  color: #fff;
}
.hero .eyebrow::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--signal-bright); flex: none; margin-top: 0.42em;
}

/* The card straddles the seam into .problem, which is why .hero has no bottom
   padding and no overflow clip. z-index keeps it above the band it overlaps. */
.hero .mock { position: relative; z-index: 3; max-width: 27.5rem; margin-inline: auto; margin-top: 32px; }
@media (min-width: 768px)  { .hero .mock { margin-top: 44px; margin-bottom: -104px; } }
@media (min-width: 1024px) { .hero .mock { margin-top: 52px; margin-bottom: -120px; } }

/* ---------- the waitlist form ---------- */
/* Grid, not flex, since the form grew to three fields. DOM order is
   name, teaches, email, button and auto-placement lays that out as
   [name][teaches] over [email][button] — so visual order, DOM order and tab
   order are the same thing and there is no focus-order trap to test for.
   The honeypot is position:absolute, which makes it out-of-flow and therefore
   not a grid item, so it claims no cell. Hidden inputs are display:none and
   claim none either. */
/* A container query, not a media query: the form pairs up when ITS column has
   room, which is not the same question as how wide the window is. Measured at
   1024px the hero splits into text + mock, the form's column falls to 398px, and
   a viewport-keyed breakpoint had already put it in two columns — the email
   field came out 115px wide and clipped its placeholder to "you@exam". */
.form-col { container-type: inline-size; }
.waitlist-form {
  display: grid; gap: 10px; grid-template-columns: 1fr;
  max-width: 520px; margin-top: 26px;
}
@media (min-width: 768px) { .waitlist-form { margin-top: 36px; } }
@container (min-width: 500px) {
  /* 1fr auto: column two is as wide as the button, and the select fills it.
     500px is the point where 1fr still leaves the email field over 200px, which
     holds you@example.com at 16px with room to spare. */
  .waitlist-form { grid-template-columns: 1fr auto; }
}
.waitlist-form.is-hidden { display: none; }

/* .field, not input[type=...]: a type selector would have silently missed the
   text input and the select, and would have caught the honeypot. */
/* Pills, at the same 1px --line-strong boundary the square controls carried.
   The boundary is what WCAG 1.4.11 measures, and it is unchanged at 3.38:1 on
   --surface and 6.66:1 on --ink-raised — the radius moves, the contrast does
   not. Padding goes 15 -> 20px because a pill's corner eats its own inset. */
.waitlist-form .field {
  min-width: 0;
  height: 52px; padding: 0 20px;
  font-family: var(--font-body); font-size: 16px; color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--r-pill);
  transition: border-color 160ms ease;
}
.waitlist-form .field::placeholder { color: var(--muted); opacity: 1; }
.waitlist-form .field:hover { border-color: var(--ink); }
.waitlist-form .field:focus-visible {
  border-color: var(--brand); outline: 2px solid var(--brand); outline-offset: 2px;
  border-radius: var(--r-pill);
}
@media (min-width: 768px) {
  .waitlist-form .field { height: 56px; padding: 0 22px; }
}

/* The chevron is drawn per surface, never once. A single data: URI freezes its
   hex, so the ink copy below is not a duplicate — it is the whole reason the
   arrow is visible on both grounds. */
.waitlist-form select.field {
  appearance: none; -webkit-appearance: none;
  padding-right: 46px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2357536B' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 18px center;
  background-size: 18px 18px;
}
/* Nothing chosen yet reads as a prompt, not as an answer. Browsers without
   :has() show it in the value colour, which is cosmetic and not a failure. */
.waitlist-form select.field:has(option[value=""]:checked) { color: var(--muted); }

/* error: four cues, one of which is colour. There is no red in this system. */
.waitlist-form.is-invalid input[type="email"] {
  border: 2px solid var(--signal);
  padding-right: 50px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23A34A06' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7.5v6M12 16.6v.2'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 18px center;
  background-size: 20px 20px;
}

.form-note { font-size: 0.84375rem; color: var(--muted); margin-top: 12px; }
@media (min-width: 768px) { .form-note { font-size: 0.875rem; margin-top: 14px; } }

/* Success removes the form rather than disabling it, and the panel takes the
   field's place so nothing below moves. That is where CLS 0 is won. */
.form-done, .form-error { display: none; margin-top: 26px; }
@media (min-width: 768px) { .form-done, .form-error { margin-top: 36px; } }
.form-done.is-shown, .form-error.is-shown { display: flex; }

/* --r-card, not the old 14px: against pill controls a 14px panel reads
   under-rounded, and the card radius is already in the brand set. */
.form-done {
  align-items: flex-start; gap: 14px; max-width: 520px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: 20px;
  font-size: 1.03125rem; line-height: 1.5; color: var(--ink);
}
.form-done .tick {
  width: 38px; height: 38px; flex: none; border-radius: 50%;
  background: var(--brand-soft); color: var(--brand);
  display: grid; place-items: center;
}
.form-error {
  align-items: flex-start; gap: 9px; max-width: 520px;
  background: var(--signal-soft); border: 1px solid var(--signal-soft-line);
  border-radius: var(--r-card); padding: 13px 15px;
  font-size: 0.96875rem; line-height: 1.5; color: var(--signal);
}
.form-error svg { flex: none; margin-top: 2px; }

/* ---------- the form assembles itself ---------- */
/* Ported from a React/framer-motion waitlist component: a staggered blur-in on
   entry, and a blurred swap between the form and its confirmation. The idea
   travels; the code does not. framer-motion + canvas-confetti + React is ~100 KB
   gzipped against a 150 KB budget for the whole page, and this site has no
   bundler and makes zero external requests. Eleven keyframes and two classes
   buy the same motion for 1.4 KB.

   Everything below animates opacity, filter and transform only — never a
   property that reflows. That is deliberate: CLS on this page is 0 and an
   entrance that moved layout would spend it.

   The whole block is gated on no-preference rather than relying on the global
   `animation: none !important` reset. That reset alone would leave the tick
   holding its stroke-dashoffset and the check would never draw. */
@media (prefers-reduced-motion: no-preference) {

  @keyframes rise {
    from { opacity: 0; filter: blur(8px); transform: translateY(9px); }
    to   { opacity: 1; filter: blur(0);   transform: translateY(0); }
  }
  @keyframes sink {
    from { opacity: 1; filter: blur(0);   transform: translateY(0); }
    to   { opacity: 0; filter: blur(7px); transform: translateY(-8px); }
  }
  @keyframes pop  { from { opacity: 0; transform: scale(0.6); } to { opacity: 1; transform: scale(1); } }
  @keyframes draw { to { stroke-dashoffset: 0; } }

  /* The four visible controls are children 2, 4, 6 and 9: DOM order is label,
     name, label, teaches, label, email, honeypot, hidden source, button. Same
     order the grid's auto-placement already depends on — see the note above
     .waitlist-form. Change the field order and both break together, which is
     the point: they are one decision, not two. */
  /* backwards, NOT both. `both` holds the last keyframe at animation-origin
     priority, which outranks every normal declaration for the life of the page
     — measured: it pinned the button at translateY(0) and .btn:hover's lift
     never rendered again. `backwards` covers the delay, then lets go. */
  .waitlist-form > :nth-child(2),
  .waitlist-form > :nth-child(4),
  .waitlist-form > :nth-child(6),
  .waitlist-form > :nth-child(9),
  .form-note {
    animation: rise 600ms cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
  }
  .waitlist-form > :nth-child(2) { animation-delay:  40ms; }
  .waitlist-form > :nth-child(4) { animation-delay: 110ms; }
  .waitlist-form > :nth-child(6) { animation-delay: 180ms; }
  .waitlist-form > :nth-child(9) { animation-delay: 250ms; }
  .form-note                     { animation-delay: 330ms; }

  /* The scroll-triggered variant that used to live here is gone with the CTA's
     inline form. Nothing needs holding until it scrolls into view any more:
     the only form on the page is inside a closed <dialog>, and a display:none
     element does not run its animations at all — they start from the beginning
     when it is shown. So the stagger above now plays on open, which is when
     someone is actually looking at it, without a line of extra code. */

  /* the swap. mode="wait": the form clears before the panel arrives, so the
     two never overlap and neither has to be taken out of flow to avoid it. */
  .waitlist-form.is-leaving {
    animation: sink 240ms cubic-bezier(0.4, 0, 1, 1) both;
    pointer-events: none;
  }
  .form-done.is-shown, .form-error.is-shown {
    animation: rise 520ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
  .form-done.is-shown .tick { animation: pop 400ms cubic-bezier(0.2, 0.8, 0.3, 1) 140ms both; }
  /* 20.5 units of path, rounded up. Set here and not on the SVG so the mark
     renders as a plain tick when motion is reduced. */
  .form-done.is-shown .tick path {
    stroke-dasharray: 22; stroke-dashoffset: 22;
    animation: draw 440ms cubic-bezier(0.4, 0, 0.2, 1) 300ms both;
  }
}

/* ---------- hero visual: the live count ---------- */
.mock { display: grid; gap: 10px; justify-items: stretch; margin-top: 30px; }
@media (min-width: 1024px) { .mock { gap: 14px; margin-top: 6px; } }

.client-card {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 16px; padding: 20px;
  display: flex; flex-direction: column; gap: 18px;
}
@media (min-width: 1024px) {
  .client-card { border-radius: 20px; padding: 26px 28px 28px; gap: 24px; }
}
.client-card .card-head { display: flex; align-items: center; gap: 11px; }
.avatar {
  width: 40px; height: 40px; flex: none; border-radius: 50%;
  background: var(--brand-soft); color: var(--brand);
  display: grid; place-items: center;
  font-family: var(--font-mono); font-weight: 500; font-size: 0.78125rem;
  letter-spacing: 0.04em;
}
.card-head .who { display: flex; flex-direction: column; gap: 2px; }
.card-head .who strong { font-size: 1rem; font-weight: 600; }
.card-head .who span {
  font-family: var(--font-mono); font-size: 0.6875rem;
  letter-spacing: 0.03em; text-transform: uppercase; color: var(--muted);
}
@media (min-width: 1024px) {
  .avatar { width: 46px; height: 46px; font-size: 0.875rem; }
  .card-head .who strong { font-size: 1.0625rem; }
  .card-head .who span { font-size: 0.75rem; }
}

.ring-row { display: flex; align-items: center; gap: 20px; }
@media (min-width: 1024px) { .ring-row { gap: 30px; } }
.ring { position: relative; display: block; width: 92px; height: 92px; flex: none; }
@media (min-width: 1024px) { .ring { width: 112px; height: 112px; } }
.ring svg { display: block; width: 100%; height: 100%; }
.ring .ring-label {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  font-family: var(--font-mono);
}
.ring .ring-label strong { font-weight: 500; font-size: 1.5625rem; line-height: 1; }
.ring .ring-label span {
  font-size: 0.53125rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted); margin-top: 2px;
}
@media (min-width: 1024px) {
  .ring .ring-label strong { font-size: 1.875rem; }
  .ring .ring-label span { font-size: 0.59375rem; }
}

.facts { flex: 1; display: flex; flex-direction: column; }
.facts div {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 14px; padding: 8px 0; border-bottom: 1px solid var(--hairline);
}
.facts div:last-child { border-bottom: 0; }
/* nowrap: the dd already had it, the dt did not, and the narrower mock column
   made "Valid to" break after "Valid" — a two-line label beside a one-line
   value, in a list whose whole job is to line up. */
.facts dt {
  font-family: var(--font-mono); font-size: 0.625rem;
  letter-spacing: 0.11em; text-transform: uppercase; color: var(--muted);
  white-space: nowrap;
}
.facts dd { font-family: var(--font-mono); font-weight: 500; font-size: 0.8125rem; white-space: nowrap; }
@media (min-width: 1024px) {
  .facts div { padding: 11px 0; }
  .facts dt { font-size: 0.6875rem; letter-spacing: 0.12em; }
  .facts dd { font-size: 0.875rem; }
}

.chip {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 9px 14px; font-size: 0.8125rem; line-height: 1.4;
}
.chip svg { flex: none; }
.chip .mono { font-family: var(--font-mono); font-size: 0.71875rem; }
@media (min-width: 1024px) {
  .mock .chip { justify-self: start; }
  .chip { gap: 9px; padding: 9px 15px 9px 13px; font-size: 0.875rem; }
  .chip .mono { font-size: 0.78125rem; }
}
/* The 36px indent is a wide-screen flourish and it only fits on a wide screen.
   Widening the text column to 1.22fr takes the mock to 321px at 1024, and
   36px of that is the difference between the link chip sitting on one line and
   wrapping to two. It comes back at 1280, where the mock is 412px. */
@media (min-width: 1280px) {
  .chip-cal { margin-left: 36px; }
  .chip-link { margin-left: 36px; }
}

/* ---------- sections ---------- */
section { padding: 44px 0; }
@media (min-width: 768px)  { section { padding: 72px 0; } }
@media (min-width: 1024px) { section { padding: 88px 0; } }
.problem, .features, .how { padding: 44px 0; }
@media (min-width: 768px)  { .problem, .features, .how { padding: 72px 0; } }
@media (min-width: 1024px) { .problem, .features, .how { padding: 96px 0; } }
/* The hero's client card and its two chips hang into .problem from 768 up (see
   the hero block), so this band's top padding carries the overhang plus the
   breathing room below it: 104px + ~56 at 768, 120px + ~56 from 1024. Measured,
   not guessed — the overhang is set by .hero .mock's negative bottom margin.

   Two things this rule has already been wrong about:
   it MUST come after the `padding: 96px 0` shorthand above, because the
   shorthand rewrites padding-top and silently discarded an earlier copy of this
   rule, which put the read-only chip on the paragraph's first line. And it was
   briefly ~232px while the overhang was actually 0 (the negative margin was
   collapsing out of .hero), which left a band of dead ink above the heading. */
@media (min-width: 768px)  { .problem { padding-top: 160px; } }
@media (min-width: 1024px) { .problem { padding-top: 176px; } }

/* why this exists — ink */
.problem { background: var(--ink); color: var(--on-ink); }
.problem .eyebrow { color: var(--signal-bright); }
.problem h2 { color: var(--on-ink); font-weight: 650; }
@media (min-width: 1024px) { .problem h2 { font-size: 3.25rem; line-height: 1.06; letter-spacing: -0.025em; } }
.problem .wrap { display: grid; gap: 16px; }
@media (min-width: 1024px) {
  .problem .wrap { grid-template-columns: 0.85fr 1.15fr; gap: 80px; align-items: start; }
  .problem .wrap > div:last-child { padding-top: 8px; }
}
.problem p { font-size: 1.03125rem; line-height: 1.62; color: var(--on-ink-muted); max-width: 62ch; }
.problem p + p {
  margin-top: 16px; color: var(--on-ink);
  border-left: 2px solid var(--signal-bright); padding-left: 16px;
}
@media (min-width: 1024px) {
  .problem p { font-size: 1.21875rem; line-height: 1.6; }
  .problem p + p { margin-top: 22px; padding-left: 22px; }
}

/* what it does — white */
.features { background: var(--surface); border-block: 1px solid var(--line); }
.features .section-head { display: flex; flex-direction: column; gap: 12px; }
.features .section-head p { font-size: 1rem; line-height: 1.6; max-width: 44ch; }
@media (min-width: 1024px) {
  .features .section-head {
    flex-direction: row; align-items: flex-end; justify-content: space-between;
    gap: 60px; border-bottom: 1px solid var(--line); padding-bottom: 28px;
  }
  .features .section-head p { font-size: 1.0625rem; line-height: 1.55; }
}
.features .grid { display: grid; gap: 12px; margin-top: 24px; }
@media (min-width: 768px)  { .features .grid { grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 32px; } }
@media (min-width: 1024px) { .features .grid { grid-template-columns: repeat(3, 1fr); margin-top: 40px; } }
.feature {
  background: var(--paper); border: 1px solid var(--line); border-radius: 16px;
  padding: 20px; display: flex; flex-direction: column; gap: 12px;
}
@media (min-width: 1024px) { .feature { border-radius: var(--r-card); padding: 28px 28px 30px; gap: 16px; } }
.feature .icon {
  width: 42px; height: 42px; flex: none; border-radius: 12px;
  background: var(--brand-soft); color: var(--brand);
  display: grid; place-items: center;
}
@media (min-width: 1024px) { .feature .icon { width: 46px; height: 46px; border-radius: var(--r-tile); } }
.feature p { font-size: 0.96875rem; line-height: 1.62; color: var(--muted); }

/* how it works — paper */
.how .steps { display: flex; flex-direction: column; gap: 22px; margin-top: 24px; }
@media (min-width: 768px) {
  .how .steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; margin-top: 44px; }
}
.step { border-top: 2px solid var(--brand); padding-top: 16px; display: flex; flex-direction: column; gap: 9px; }
@media (min-width: 1024px) { .step { padding-top: 20px; gap: 14px; } }
.step::before {
  content: counter(step, decimal-leading-zero);
  font-family: var(--font-mono); font-weight: 500; font-size: 0.78125rem;
  letter-spacing: 0.12em; color: var(--signal);
}
.steps { counter-reset: step; }
.step { counter-increment: step; }
.step h3 { font-size: 1.375rem; line-height: 1.2; letter-spacing: -0.014em; }
@media (min-width: 1024px) { .step h3 { font-size: 1.5625rem; } }
.step p { font-size: 1rem; line-height: 1.62; color: var(--muted); }
.how .after {
  margin-top: 22px; font-family: var(--font-display); font-weight: 400;
  font-size: 1.3125rem; line-height: 1.35; letter-spacing: -0.012em; max-width: 34ch;
}
@media (min-width: 1024px) { .how .after { margin-top: 44px; font-size: 1.625rem; max-width: 46ch; } }

/* who it's for — white */
.audience { background: var(--surface); border-block: 1px solid var(--line); }
.audience .wrap { display: grid; gap: 12px; }
@media (min-width: 1024px) {
  .audience .wrap { grid-template-columns: 0.9fr 1.1fr; gap: 80px; align-items: start; }
  .audience .wrap > div:last-child { display: flex; flex-direction: column; gap: 26px; padding-top: 6px; }
}
.audience .tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
@media (min-width: 1024px) { .audience .tags { gap: 10px; margin-top: 0; } }
.tag {
  background: var(--paper); border: 1px solid var(--line); border-radius: 999px;
  padding: 10px 15px; font-size: 0.90625rem; line-height: 1.4;
}
@media (min-width: 1024px) { .tag { padding: 11px 18px; font-size: 0.96875rem; } }
.tag.accent {
  background: var(--signal-soft); border-color: var(--signal-soft-line);
  color: var(--signal); font-weight: 600;
}
.audience p { margin-top: 8px; font-size: 1rem; line-height: 1.62; color: var(--muted); max-width: 60ch; }
@media (min-width: 1024px) { .audience p { margin-top: 0; font-size: 1.0625rem; } }

/* boundaries — paper */
.boundaries ul { list-style: none; display: grid; gap: 20px; margin-top: 24px; }
@media (min-width: 768px)  { .boundaries ul { grid-template-columns: repeat(3, 1fr); gap: 28px; margin-top: 44px; } }
.boundaries li {
  display: flex; flex-direction: column; gap: 10px;
  border-top: 1px solid var(--line-mid); padding-top: 16px;
}
@media (min-width: 1024px) { .boundaries li { gap: 14px; padding-top: 22px; } }
.boundaries li svg { flex: none; color: var(--brand); }
/* The promise in each boundary is tinted, so the three of them skim without
   being read — the reference's move, and the reason it is indigo and not amber:
   amber is a signal (time spent against time left, and errors), never emphasis. */
.boundaries li strong {
  font-size: 1.03125rem; font-weight: 600; line-height: 1.4;
  color: var(--brand); background: var(--brand-soft);
  border-radius: 4px; padding: 1px 5px; margin-inline: -5px;
  box-decoration-break: clone; -webkit-box-decoration-break: clone;
  align-self: start;
}
.boundaries li span { font-size: 0.96875rem; line-height: 1.62; color: var(--muted); }
@media (min-width: 1024px) {
  .boundaries li strong { font-size: 1.09375rem; }
  .boundaries li span { font-size: 1rem; }
}

/* fair questions — white */
.faq { background: var(--surface); border-block: 1px solid var(--line); }
.faq .wrap { display: grid; gap: 4px; }
@media (min-width: 1024px) {
  .faq .wrap { grid-template-columns: 0.72fr 1.28fr; gap: 80px; align-items: start; }
}
.faq .qs { display: flex; flex-direction: column; }
.faq details { border-top: 1px solid var(--line); }
.faq details:last-of-type { border-bottom: 1px solid var(--line); }
.faq summary {
  cursor: pointer; list-style: none;
  display: flex; justify-content: space-between; align-items: center; gap: 16px;
  min-height: 44px; padding: 16px 0;
  font-size: 1.03125rem; font-weight: 600;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "+"; font-family: var(--font-mono); font-size: 1.0625rem;
  color: var(--brand); flex: none;
}
.faq details[open] summary::after { content: "\2212"; }
.faq details p {
  padding-bottom: 16px; font-size: 0.96875rem; line-height: 1.6;
  color: var(--muted); max-width: 66ch;
}
@media (min-width: 1024px) {
  .faq summary { padding: 22px 0; gap: 24px; font-size: 1.125rem; }
  .faq summary::after { font-size: 1.125rem; }
  .faq details p { padding-bottom: 22px; font-size: 1.03125rem; line-height: 1.62; }
}

/* ---------- final cta — ink ---------- */
.cta { background: var(--ink); color: var(--on-ink); padding: 48px 0; }
@media (min-width: 768px)  { .cta { padding: 72px 0; } }
@media (min-width: 1024px) { .cta { padding: 96px 0 100px; } }
.cta .eyebrow { color: var(--signal-bright); }
.cta h2 { color: var(--on-ink); font-weight: 650; }
@media (min-width: 1024px) { .cta h2 { font-size: 3.25rem; line-height: 1.06; letter-spacing: -0.025em; } }
.cta .section-intro { color: var(--on-ink-muted); font-size: 1.03125rem; line-height: 1.55; max-width: 46ch; }
.cta .wrap { display: grid; gap: 14px; }
@media (min-width: 1024px) {
  .cta .wrap { grid-template-columns: 1fr 1fr; gap: 80px; align-items: end; }
  .cta .section-intro { font-size: 1.1875rem; }
  .cta .cta-row { margin-top: 0; }
}
/* Rule 2 for dark surfaces: the primary button inverts. An indigo fill on ink
   measures 1.9:1 against its ground and disappears. */
.cta .btn { background: var(--on-ink); color: var(--ink); }
.cta .btn:hover { background: #FFFFFF; color: var(--ink); }
/* Roughly forty lines of ink-ground form styling used to live here — the field
   background, the placeholder, the hover and focus borders, a second copy of
   the select chevron drawn in --on-ink-muted, and the error ring redrawn in
   --signal-bright because the paper amber is too dark on --ink-raised. All of
   it existed because a form sat on this ink section. The form is now in a
   modal on paper, so every one of those rules matched nothing. Deleted rather
   than left: dead CSS that still looks authoritative is how the --ink-field-line
   contrast bug survived a full brand pass.

   If a form ever returns to an ink surface, the rules are in git, and the two
   traps they encode are worth re-reading first: use background-COLOR, never the
   shorthand, or it resets background-image and erases the chevron and the error
   icon; and --ink-field-line measures 2.57:1 on --ink-raised, which fails the
   3:1 WCAG 1.4.11 asks of a control boundary. */

/* ---------- pricing ---------- */
/* One page, both grounds: paper hero, white plan cards, ink founding banner,
   white questions, ink close. Every ink rule below is the three dark-surface
   rules applied, not re-decided — eyebrow to signal-bright, display to 650,
   the button inverted, elevation as a 1px line. */

.pricing-hero { padding: 40px 0 32px; }
@media (min-width: 768px)  { .pricing-hero { padding: 68px 0 52px; } }
@media (min-width: 1024px) { .pricing-hero { padding: 92px 0 68px; } }
/* 17ch, and the clamp tuned to it: the headline is one short sentence and it
   should break to two lines, not four on a phone and one long ruler at 1440. */
.pricing-hero h1 {
  margin-top: 14px; max-width: 17ch;
  font-size: clamp(2.25rem, 1.35rem + 3.75vw, 3.75rem);
}
@media (min-width: 768px) { .pricing-hero h1 { margin-top: 20px; } }
.pricing-hero .lede { margin-top: 18px; max-width: 46ch; }

/* the plans — white, same head treatment as the features grid */
.plans { background: var(--surface); border-block: 1px solid var(--line); }
.plans .section-head { display: flex; flex-direction: column; gap: 12px; }
.plans .section-head p { font-size: 1rem; line-height: 1.6; max-width: 46ch; }
@media (min-width: 1024px) {
  .plans .section-head {
    flex-direction: row; align-items: flex-end; justify-content: space-between;
    gap: 60px; border-bottom: 1px solid var(--line); padding-bottom: 28px;
  }
  .plans .section-head p { font-size: 1.0625rem; line-height: 1.55; }
}

/* Two cards today and three when the Starter rung is switched on in
   pricing-data.json. Both layouts live here on purpose: flipping that flag has
   to be a data change, not a data change plus a stylesheet change. */
.plan-grid { display: grid; gap: 12px; margin-top: 24px; }
@media (min-width: 768px)  { .plan-grid { gap: 20px; margin-top: 32px; grid-template-columns: 1fr 1fr; } }
@media (min-width: 1024px) {
  .plan-grid { margin-top: 40px; }
  .plan-grid.cols-2 { max-width: 940px; }
  .plan-grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
}
.plan {
  background: var(--paper); border: 1px solid var(--line); border-radius: 16px;
  padding: 22px; display: flex; flex-direction: column; gap: 14px;
}
@media (min-width: 1024px) { .plan { border-radius: var(--r-card); padding: 30px 30px 32px; } }
/* Solo is the one being sold. A 2px indigo edge on the white ground, not a
   badge and not a shout: nothing else in this system shouts either. The border
   grows by 1px, so the padding drops by 1 and the card keeps its box. */
.plan.is-featured { background: var(--surface); border: 2px solid var(--brand); padding: 21px; }
@media (min-width: 1024px) { .plan.is-featured { padding: 29px 29px 31px; } }
.plan h3 { font-size: 1.3125rem; }
@media (min-width: 1024px) { .plan h3 { font-size: 1.4375rem; } }

/* Mono, because the brand system's Count step says every number is mono, and a
   price is the one number on this page anybody came to read. */
.plan-price {
  display: flex; align-items: baseline; gap: 8px; margin-top: -4px;
  font-family: var(--font-mono); font-weight: 500;
  font-size: 2.625rem; line-height: 1; letter-spacing: -0.02em;
}
@media (min-width: 1024px) { .plan-price { font-size: 3.125rem; } }
.plan-price .per {
  font-family: var(--font-body); font-weight: 400; font-size: 0.9375rem;
  letter-spacing: 0; color: var(--muted);
}
/* The -0.01em pays for L-5. Mono digits are wider than Jakarta's, so putting
   the numerals in mono made this line 307px of text against a 304px box at 390
   and a 303px box at 1280 — it wrapped, and "free" ended up alone on a second
   row on the card the pricing page most wants read. Measured, not guessed:
   -0.01em across the whole line gives back 6px and lands on 301px, which is
   exactly the width this line had BEFORE L-5 and the width the card was drawn
   around. Spread over ~41 characters that is 0.145px each and invisible;
   squeezing the same 6px out of the 11 numerals alone needs -0.03em, which is
   not. 1440 never wrapped; 360/768/1024 were already two lines before L-5 and
   still are. */
.plan-annual { margin-top: -8px; font-size: 0.90625rem; line-height: 1.5; color: var(--muted); letter-spacing: -0.01em; }
/* Same rule as .plan-price above, applied one line down: the brand system's
   Count row says every number a client sees is mono, and these are the same
   prices written a second way. Only the numerals — the words around them stay
   Jakarta. The spans are written by tools/pricing.py from pricing-data.json;
   this is CSS and never touches a figure. */
.plan-annual [data-price] { font-family: var(--font-mono); font-weight: 500; }
.plan-line { font-size: 0.96875rem; line-height: 1.62; color: var(--muted); }
.plan-note { font-size: 0.875rem; line-height: 1.55; color: var(--muted); }

.plan-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.plan-list li { position: relative; padding-left: 26px; font-size: 0.96875rem; line-height: 1.5; }
/* A drawn tick, not a character: no glyph to go missing from a subset font and
   nothing for a screen reader to read out three times per card. */
.plan-list li::before {
  content: ""; position: absolute; left: 1px; top: 0.36em;
  width: 10px; height: 6px;
  border-left: 2px solid var(--brand); border-bottom: 2px solid var(--brand);
  transform: rotate(-45deg);
}
/* The button lands on the floor of the card whatever the card above it says,
   so the two (or three) buttons sit on one line across the grid. */
.plan .btn { margin-top: auto; align-self: stretch; }

.plan-extras { margin-top: 26px; display: flex; flex-direction: column; gap: 10px; }
@media (min-width: 768px) { .plan-extras { margin-top: 34px; gap: 12px; } }
.plan-extras p { font-size: 0.96875rem; line-height: 1.6; color: var(--muted); max-width: 64ch; }
.plan-extras .extra-trial { color: var(--ink); }
.plan-extras .extra-grand {
  color: var(--ink);
  font-family: var(--font-display); font-weight: 600;
  font-size: 1.1875rem; line-height: 1.35; letter-spacing: -0.012em;
}
@media (min-width: 1024px) { .plan-extras .extra-grand { font-size: 1.3125rem; } }
.plan-extras .extra-rail { font-size: 0.90625rem; }

/* founding 100 — ink */
.founding { background: var(--ink); color: var(--on-ink); }
.founding .eyebrow { color: var(--signal-bright); }
.founding h2 { color: var(--on-ink); font-weight: 650; }
@media (min-width: 1024px) { .founding h2 { font-size: 3.25rem; line-height: 1.06; letter-spacing: -0.025em; } }
.founding .wrap { display: grid; gap: 16px; }
@media (min-width: 1024px) {
  .founding .wrap { grid-template-columns: 0.85fr 1.15fr; gap: 80px; align-items: start; }
  .founding .wrap > div:last-child { padding-top: 8px; }
}
.founding p { font-size: 1.03125rem; line-height: 1.62; color: var(--on-ink-muted); max-width: 62ch; }
@media (min-width: 1024px) { .founding p { font-size: 1.1875rem; line-height: 1.6; } }
/* the two prices the offer is actually about */
.founding .num { color: var(--on-ink); font-family: var(--font-mono); font-weight: 500; }
.founding .founding-why {
  margin-top: 16px; color: var(--on-ink);
  border-left: 2px solid var(--signal-bright); padding-left: 16px;
}
@media (min-width: 1024px) { .founding .founding-why { margin-top: 22px; padding-left: 22px; } }
/* Rule 2 for dark surfaces: the primary button inverts. */
.founding .btn { background: var(--on-ink); color: var(--ink); }
.founding .btn:hover { background: #FFFFFF; color: var(--ink); }

/* ---------- the waitlist modal ---------- */
/* Two ways in, one appearance. [open] is showModal(); :target is the same
   dialog opened by the browser following <a href="#join"> with scripting off.
   :target gets no top layer and therefore no ::backdrop, so its scrim is
   painted on the element itself and it needs an explicit z-index — the [open]
   copy needs neither, because the top layer sits above the whole document. */
.join-modal {
  padding: 0; border: 0; margin: 0; background: transparent; color: var(--ink);
  inset: 0; width: 100%; max-width: none; height: 100%; max-height: none;
  overflow: auto;
  /* This element is the scroller (overflow:auto above). Without contain, a
     flick that reaches the end of the panel keeps going and scrolls the page
     underneath it — which the :target open cannot prevent any other way,
     having no top layer and no inert background. */
  overscroll-behavior: contain;
}
.join-modal[open],
.join-modal:target {
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.join-modal:target { position: fixed; z-index: 200; background: rgba(34, 30, 51, 0.55); }
.join-modal::backdrop { background: rgba(34, 30, 51, 0.55); }

/* margin:auto on a flex child, not align-items:center on the parent. Centring
   via align-items clips the TOP of an overflowing child and makes it
   unreachable — on a 360x640 phone in landscape this panel is taller than the
   viewport, and the email field is what gets cut off. */
.join-panel {
  container-type: inline-size;
  position: relative;
  margin: auto;
  width: min(100%, 540px);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: 30px 22px 26px;
}
@media (min-width: 520px) { .join-panel { padding: 34px 32px 30px; } }
/* No shadow, deliberately. Nothing in this system casts one — the client card,
   the feature tiles and the chips all separate with a 1px line — and the scrim
   behind the panel already does the lifting a modal shadow normally does. */

.join-panel h2 { font-size: 1.75rem; line-height: 1.14; letter-spacing: -0.018em; }
@media (min-width: 520px) { .join-panel h2 { font-size: 2rem; } }
.join-intro { margin-top: 10px; font-size: 1rem; line-height: 1.55; color: var(--muted); max-width: 42ch; }

/* 44x44, and offset so it never lands on the eyebrow. */
.join-close {
  position: absolute; top: 8px; right: 8px;
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center;
  color: var(--muted); text-decoration: none;
  transition: background-color 160ms ease, color 160ms ease;
}
.join-close:hover { background: var(--paper); color: var(--ink); }
/* The confetti canvas, created by the script only on a successful signup and
   removed when the burst ends or the dialog closes. absolute so it is never a
   flex item and cannot shift the centred panel — CLS on this page is 0 and a
   celebration is not what should spend it. pointer-events:none so the close
   button, the backdrop and everything else still receive their own clicks. */
.confetti {
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 1;
}
.join-panel .eyebrow { padding-right: 44px; }
.join-panel .waitlist-form { margin-top: 22px; }
.join-panel .form-done, .join-panel .form-error { margin-top: 22px; }

/* ---------- footer ---------- */
.site-footer {
  background: var(--ink); color: var(--on-ink-muted);
  border-top: 1px solid var(--ink-line);
  padding: 28px 0 34px;
}
@media (min-width: 1024px) { .site-footer { padding: 38px 0 44px; } }
.site-footer .wrap {
  display: flex; flex-direction: column; gap: 12px;
  font-size: 0.875rem; line-height: 1.55;
}
@media (min-width: 1024px) {
  .site-footer .wrap { flex-direction: row; align-items: center; justify-content: space-between; gap: 40px; font-size: 0.9375rem; }
  .site-footer p:first-of-type { flex: 1; }
}
.site-footer .logo { color: var(--on-ink); font-size: 1.25rem; }
@media (min-width: 1024px) { .site-footer .logo { font-size: 1.375rem; } }
.site-footer a { color: var(--brand-lift); }
.site-footer a:hover { color: var(--on-ink); }

/* ---------- standalone pages (404, privacy) ---------- */
.page { padding: 56px 0 64px; }
@media (min-width: 1024px) { .page { padding: 88px 0 96px; } }
.page-404 { padding: 72px 0 88px; }
@media (min-width: 1024px) { .page-404 { padding: 120px 0 140px; } }
.page h1 { font-size: clamp(2.25rem, 1.6rem + 2.9vw, 3.75rem); }
.page-privacy h1 { font-size: clamp(2rem, 1.5rem + 2.2vw, 3rem); }
.page .lede { margin-top: 18px; }
.page-privacy p { font-size: 1.0625rem; line-height: 1.68; color: var(--muted); max-width: 62ch; }
.page-privacy p + p { margin-top: 16px; }
.page .back { display: inline-block; margin-top: 18px; padding: 6px 0; font-size: 1.0625rem; }

/* ---------- the mark ---------- */
/* Five filled sectors on one ring, three struck and two standing as thin track.
   No strokes anywhere. On ink it is brand-lift: indigo #4338CA on ink measures
   1.9:1 and vanishes. One drawing on this site, header and footer alike: the
   icon variant is for the 16px favicon, which cannot carry five stations.

   Size comes out of the lockup rule rather than a pixel guess, and then out of
   the floor. The horizontal lockup is mark inked diameter = 1.25 C, C =
   Newsreader 600 cap height = 0.70em; the file is 64 units wide with 4 of
   padding a side, so the inked diameter is 56/64 of the box and the box lands on
   exactly 1em of the wordmark. THE FLOOR OUTRANKS THE RATIO. The primary needs
   24px of inked diameter, which is a 27.43px box, rounded here to the 28px the
   app sidebar already uses. This site's wordmark runs 20-26px, so the floor
   binds at every breakpoint and max() is what stops the ratio shrinking the mark
   through it.

   That floor is a measurement, not a preference. Rendered at 1x and read back
   pixel by pixel, a 28px box paints the five stations with their gaps intact on
   both grounds; at the 20-23px the bare ratio asked for, the gaps fall to about
   1px and the drawing starts closing into a donut, which is the v1 reading the
   redraw exists to leave behind.

   The ring centre sits on the cap-height midline, not the box centre. Measured
   on the shipped face at 400px: cap top 0.045em, baseline 0.7325em, so the
   midline is 0.38875em down a line-height:1 box and the centre is 0.5em.
   align-items:center centres the margin box, so half of a 0.2225em bottom
   margin lifts the mark the missing 0.111em. */
.logo .mark { color: var(--brand); width: max(1em, 28px); height: max(1em, 28px);
  margin-bottom: 0.2225em; }
.site-footer .logo .mark { color: var(--brand-lift); }
