/* ==========================================================================
   DATUM — base: reset, typography, layout primitives, shared devices.
   Every value is a token from tokens.css. Render-blocking on every page.
   ========================================================================== */

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

html {
  /* One ground, declared. Without this Chrome on Android is free to auto-invert the page,
     and every platform picks its own palette for form controls and scrollbars. */
  color-scheme: light;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* The classic scrollbar is 15px on Windows and Linux, so without a reserved gutter a
     short page and a long page are different widths and the layout jumps between them. */
  scrollbar-gutter: stable;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--space-lg));
}

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

body {
  margin: 0;
  background: var(--surface-page);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-stretch: var(--font-stretch-body);
  line-height: var(--lh-body);
  font-variant-numeric: var(--font-numeric);
  /* Deliberately not clipped. A clip region over a 20,000px document costs a full-page
     composite, and `hidden` here would make the body a scroll container, which is how
     Safari loses position: sticky on the header. The two elements that bleed past the
     container clip themselves, in cta.css and about.css. */
}

h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--font-display);
  font-stretch: var(--font-stretch-display);
  font-weight: var(--weight-display);
  text-wrap: balance;
}

h1 { font-size: var(--text-display); line-height: var(--lh-display); letter-spacing: var(--track-display); }
h2 { font-size: var(--text-h2); line-height: var(--lh-heading); letter-spacing: var(--track-heading); }
h3 { font-size: var(--text-h3); line-height: var(--lh-subheading); letter-spacing: var(--track-subheading); }
h4 { font-size: var(--text-h4); line-height: var(--lh-tight); letter-spacing: var(--track-tight); }

p { margin: 0 0 var(--stack-gap); max-width: var(--measure-body); }
p:last-child { margin-bottom: 0; }

ul, ol { margin: 0; padding: 0; list-style: none; }

a { color: inherit; text-decoration-color: var(--accent); text-underline-offset: 3px; }
a:hover { text-decoration-color: var(--accent-hover); }

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

/* Every content image is wrapped in <picture> for its WebP set. display: contents keeps
   the <img> itself as the grid or flex item, so no layout rule had to change for it. */
picture { display: contents; }

button, input, select, textarea { font: inherit; color: inherit; }

:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
  border-radius: var(--radius-control);
}

/* Windows High Contrast paints no box-shadow, so the ring above disappears entirely and
   the keyboard user is left with nothing. System colours are what a forced-colors block
   is required to use. */
@media (forced-colors: active) {
  :focus-visible {
    outline: 3px solid CanvasText;
    outline-offset: 2px;
  }
}

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

.skip {
  position: absolute; left: var(--space-md); top: var(--space-md);
  z-index: var(--z-overlay);
  padding: var(--space-xs) var(--space-md);
  background: var(--surface-accent); color: var(--text-on-accent);
  font-family: var(--font-data); font-size: var(--text-data);
  border-radius: var(--radius-control);
  transform: translateY(-200%);
}
.skip:focus { transform: none; }

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

.wrap {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns-sm), minmax(0, 1fr));
  gap: var(--grid-gap);
}

@media (min-width: 640px) {
  .grid { grid-template-columns: repeat(var(--grid-columns-md), minmax(0, 1fr)); }
}

@media (min-width: 900px) {
  .grid { grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr)); }
}

.section { padding-block: var(--section-gap); }

/* ---- material: grain and the survey grid ---------------------------------- */

/* The paper tooth. The fixed layer no longer blends — a viewport-sized blend over a
   20,000px page cost a full-page recomposite every frame. The blend now lives on the
   paper sections themselves, which are band-sized and cheap, and the fixed layer is
   left as plain alpha for the areas between them. */
.grain {
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  background-image: var(--grain-image);
  background-size: var(--grain-tile) var(--grain-tile);
  opacity: var(--grain-opacity);
}

.grained { position: relative; isolation: isolate; }

.grained::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--z-below);
  pointer-events: none;
  background-image: var(--grain-image);
  background-size: var(--grain-tile) var(--grain-tile);
  opacity: calc(var(--grain-opacity) * 2);
  mix-blend-mode: var(--grain-blend);
}

/* No mix-blend-mode on this layer. A fixed, viewport-sized blended layer over a
   19,000px page with 36 photographs and layered shadows forced a full-page
   re-composite on every frame: Chrome could not produce a screenshot at all
   until it was removed. Straight alpha at the same opacity costs nothing and
   reads the same at 6%. The ink bands keep their own blended overlay, which is
   band-sized and cheap. */

.grid-paper { position: relative; }

/* ::after, not ::before: .grained already owns ::before, and a section that is both
   (Process, Pricing, FAQ) had one pseudo-element doing two jobs — the grid won the
   background and inherited the grain's opacity, so both effects disappeared. */
.grid-paper::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--z-below);
  pointer-events: none;
  opacity: 1;
  /* token-exempt: the survey grid is a drawn rule set, not a decorative gradient */
  background-image: repeating-linear-gradient(
    to right,
    var(--edge-grid) 0 1px,
    transparent 1px calc(100% / 12)
  );
}

/* ---- photographic plates. Dimensions are reserved by aspect-ratio, so the
   frame holds its place before the file arrives and CLS stays 0. ------------- */

.ph {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
  background: var(--surface-sunken);
  border: var(--border-hairline);
  border-radius: var(--radius-plate);
}

.ph--photo { background: var(--surface-disabled); }
.ph--ink { background: var(--surface-ink-raised); border: var(--border-on-ink); }

/* ---- the bold move: pinned plates ---------------------------------------- */

.plate { position: relative; margin: 0; }

.plate--before {
  padding: var(--plate-mount);
  background: var(--surface-raised);
  border: var(--border-plate);
  box-shadow: var(--shadow-plate);
}

.plate__pin {
  position: absolute;
  top: calc(-1 * var(--space-sm));
  left: calc(-1 * var(--space-sm));
  z-index: var(--z-string);
  color: var(--accent);
}

.plate__label {
  display: block;
  padding-top: var(--space-xs);
  font-family: var(--font-data);
  font-size: var(--text-data);
  line-height: var(--lh-data);
  color: var(--text-secondary);
}

.plate__label-tag {
  display: inline-block;
  margin-right: var(--space-xs);
  padding: 2px var(--space-xs);
  background: var(--surface-accent);
  color: var(--text-on-accent);
  font-family: var(--font-data);
  font-size: var(--text-label);
  font-weight: var(--weight-data-strong);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  border-radius: var(--radius-control);
}

/* ---- dimension strings: the decorative layer, always carrying a figure ----- */

.string {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  margin: 0;
  font-family: var(--font-data);
  font-size: var(--text-data);
  color: var(--text-accent);
}

.string::before,
.string::after {
  content: "";
  flex: 1 1 auto;
  height: var(--border-width-hair);
  background: var(--accent);
  transform-origin: left center;
  transition: transform var(--dur-slow) var(--ease-draw);
}

.string::after { transform-origin: right center; }

/* Draw: the hairline runs out from the datum side, then the figure arrives. */
.js .string[data-in]::before,
.js .string[data-in]::after { transform: scaleX(0); }

.js .string[data-in] .string__figure {
  opacity: 0;
  transition: opacity var(--dur-tick) var(--ease-draw) var(--dur-fast);
}

.js .string[data-in].is-in::before,
.js .string[data-in].is-in::after { transform: scaleX(1); }

.js .string[data-in].is-in .string__figure { opacity: 1; }

.string__figure {
  flex: 0 1 auto;
  padding-inline: var(--string-knockout);
}

/* A dimension figure only holds one line where there is room for one. Below
   900px it wraps: nowrap here pushed the hero caption 94px past the viewport. */
@media (min-width: 900px) {
  .string__figure { flex: 0 0 auto; white-space: nowrap; }
}

/* ---- controls ------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--space-lg);
  font-family: var(--font-data);
  font-size: var(--text-data);
  font-weight: var(--weight-data-strong);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  text-decoration: none;
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-slide),
              color var(--dur-fast) var(--ease-slide),
              border-color var(--dur-fast) var(--ease-slide);
}

.btn--accent {
  background: var(--surface-accent);
  border-color: var(--surface-accent);
  color: var(--text-on-accent);
  box-shadow: var(--shadow-control);
}
.btn--accent:hover { background: var(--surface-accent-hover); border-color: var(--surface-accent-hover); }

.btn--ghost {
  background: transparent;
  border-color: var(--accent);
  color: var(--text-accent);
}
.btn--ghost:hover { background: var(--accent-wash); }

.btn--ghost-ink {
  background: transparent;
  border-color: var(--edge-on-ink-strong);
  color: var(--text-on-ink);
}
.btn--ghost-ink:hover { border-color: var(--accent-on-ink); color: var(--text-on-ink-accent); }

.btn--block { width: 100%; }

.pill {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-sm);
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  border: var(--border-on-ink);
  border-radius: var(--radius-pill);
}

.pill--early,
.pill--ontime { color: var(--text-on-ink-accent); border-color: var(--accent-on-ink); }
.pill--late { color: var(--text-on-ink-secondary); }

.icon {
  width: var(--icon-size);
  height: var(--icon-size);
  fill: none;
  stroke: currentColor;
  stroke-width: var(--icon-stroke);
  stroke-linecap: butt;
  stroke-linejoin: miter;
}
.icon--sm { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.icon--xs { width: var(--icon-size-sm); height: var(--icon-size-sm); }

.sprite { position: absolute; }

/* ---- entrances -----------------------------------------------------------
   Four behaviours, from docs/motion-spec.md: lock (one straight axis, stops dead),
   stamp (stepped opacity, no movement), draw (a line from the datum side), mask
   (lines rise inside an overflow-hidden parent). Opacity and transform only.
   Every hidden state is scoped under .js, so with JS off nothing is ever invisible.
   -------------------------------------------------------------------------- */

.js [data-in] {
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease-settle);
}

.js [data-in="lock-y"] {
  opacity: 0;
  transform: translateY(var(--space-md));
  transition-property: opacity, transform;
  transition-duration: var(--dur-slow);
}

/* A horizontal entrance parks its element 32px outside its own box, and below 900px
   there is no slack to absorb that: the document was 12px wider than the viewport on
   every page, before any interaction. Under 900px both horizontal locks fall back to
   the vertical one, which travels inside the layout. */
.js [data-in="lock-x"],
.js [data-in="lock-x-right"] {
  opacity: 0;
  transform: translateY(var(--space-md));
  transition-property: opacity, transform;
  transition-duration: var(--dur-slow);
}

@media (min-width: 900px) {
  .js [data-in="lock-x"] { transform: translateX(calc(-1 * var(--space-xl))); }
  .js [data-in="lock-x-right"] { transform: translateX(var(--space-xl)); }
}

.js [data-in="lock-y"].is-in,
.js [data-in="lock-x"].is-in,
.js [data-in="lock-x-right"].is-in { opacity: 1; transform: none; }

/* A readout does not fade. steps(1) after a staggered delay is the stamp. */
.js [data-in="stamp"] [data-in-child] {
  opacity: 0;
  transition: opacity var(--dur-tick) steps(1);
  transition-delay: calc(var(--i, 0) * var(--stagger-tight));
}

.js [data-in="stamp"].is-in [data-in-child] { opacity: 1; }

.js [data-in="mask"] {
  overflow: hidden;
  /* Descenders on the last line fall below a 0.95 line box. Without this the mask cuts
     the tail off every g, y and p in a heading, at every width, for good. */
  padding-bottom: var(--space-xs);
  margin-bottom: calc(-1 * var(--space-xs));
}

/* A mask clips. Travel is a full line box and there is no opacity channel — with
   opacity in the mix the overflow never had anything to hide and what shipped was
   the fade-up the motion spec bans as a section default. */
.js [data-in="mask"] > * {
  display: block;
  transform: translateY(100%);
  transition: transform var(--dur-base) var(--ease-settle);
}

.js [data-in="mask"].is-in > * { transform: none; }

/* Wipe. Not clip-path on the observed element: a clip-path on the target zeroes its
   own intersection ratio, so the IntersectionObserver never fires and the band never
   arrives. A paper-coloured shutter that retracts to the right does the same thing
   visually and leaves the observed box alone. */
.js [data-in="wipe"] { position: relative; }

.js [data-in="wipe"]::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--z-raised);
  pointer-events: none;
  background: var(--surface-page);
  transform-origin: right center;
  transition: transform var(--dur-slow) var(--ease-slide);
}

.js [data-in="wipe"].is-in::after { transform: scaleX(0); }

@media (prefers-reduced-motion: reduce) {
  .js [data-in],
  .js [data-in] > *,
  .js [data-in] [data-in-child] {
    opacity: 1;
    transform: none;
    clip-path: none;
    transition: none;
  }
}


/* ---- hover and press, from docs/motion-spec.md ---------------------------
   Entrance is never the exit: every hover-out runs at --dur-fast on --ease-exit,
   which is why each rule states the out state first and the :hover state second.
   -------------------------------------------------------------------------- */

.btn__icon {
  transition: transform var(--dur-fast) var(--ease-exit);
}

.btn:hover .btn__icon {
  transform: translateX(var(--space-2xs));
  transition: transform var(--dur-tick) var(--ease-slide);
}

.btn:active { transform: translateY(1px); box-shadow: none; }

/* View transitions (MO1). The header and the datum line hold still while the sheet
   beneath them swaps; browsers without support just navigate. */
@view-transition { navigation: auto; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none; }
}
