/* ===========================================================================
   Tend.ai — public site design system.
   ---------------------------------------------------------------------------
   This is the marketing surface at tendai.africa, and it is deliberately NOT
   the console/compiler identity. Those are light, editorial and dense because
   they are worked in daily. This is dark and atmospheric because it is read
   once.

   Every motion value here is measured, not invented: the easing curves, the
   4500ms crossfade, the blur/travel pairs on the reveal family and the four
   card-stack keyframes were read off a live reference implementation. Do not
   "tidy" the numbers — the choreography depends on the odd ones (the 55%,
   35%, 22% and 88% hold points especially).
   =========================================================================== */

@property --ta-grad-y {
  syntax: '<percentage>';
  inherits: true;
  initial-value: 18%;
}

:root {
  /* Ground. --ink is the near-black the whole page sits on; it is not pure
     black so the blurred colour layers have something to lift off. */
  --ink: #17171a;
  --ink-deep: #101013;
  --paper: #f0f0f0;
  --black: #000;
  --white: #fff;
  --transparent: transparent;

  /* Text on dark. The muted grey is the kicker/label colour. */
  --fg: #fff;
  --fg-muted: #a1a1a1;
  --fg-dim: rgba(255, 255, 255, 0.55);
  --fg-faint: rgba(255, 255, 255, 0.45);
  --hairline: rgba(255, 255, 255, 0.12);
  --hairline-strong: rgba(255, 255, 255, 0.2);

  /* Aurora stops. */
  --blue-500: #3b82f6;
  --blue-400: #60a5fa;
  --blue-300: #93c5fd;
  --indigo-300: #a5b4fc;
  --violet-200: #ddd6fe;

  --radius: 0.625rem;
  --radius-card: 48px;
  --radius-stack: 28px;

  --font-display: 'Plus Jakarta Sans', ui-sans-serif, system-ui, sans-serif;
  --font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
  --font-mono: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* Motion. Two curves do nearly all the work: expo-out for anything large or
     slow, and the standard curve for hover-scale colour changes. */
  --ease-expo: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-std: cubic-bezier(0.4, 0, 0.2, 1);
  --dur-atmosphere: 4500ms;
  --dur-slow: 900ms;
  --dur-mid: 500ms;
  --dur-hover: 300ms;
  --dur-quick: 150ms;
  /* The coverage grid's own pair. Much slower than a hover because the whole
     grid moves, not one card — the tracks have to read as mass being pushed
     around, and mass does not arrive in a tenth of a second.

     The curve is a plain cubic ease-out, deliberately NOT --ease-expo: an
     expo curve is 55% of the way through the move in the first 10% of its
     time, which lands as a snap followed by a long crawl however long you
     make the duration. This one ramps. */
  --ease-focus: cubic-bezier(0.215, 0.61, 0.355, 1);
  --dur-focus: 820ms;

  --gutter: clamp(20px, 5vw, 64px);
  --shell: 1400px;
}

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

html {
  background: var(--ink);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100%;
  background: var(--ink);
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

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

a {
  color: inherit;
  text-decoration: none;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

:focus-visible {
  outline: 2px solid var(--blue-300);
  outline-offset: 3px;
  border-radius: 4px;
}

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

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

/* ===========================================================================
   ATMOSPHERE
   The aurora is two repeating linear gradients at 100deg — one a white/clear
   comb that cuts the colour band into ribbons, one the colour band itself —
   oversized (300%/200%) and blurred, then slid sideways forever. The blur is
   what turns hard stripes into light.
   =========================================================================== */

/* Absolute, not fixed, and only a little taller than the first screen. Fixed
   dragged the full-strength aurora down the entire 6800px page, which washed
   every section below the hero in blue and cost a composited repaint the whole
   way. Anchored to the top it behaves like what it is — the light in the room
   the hero is standing in — and everything past it sits on flat ink, which is
   also what the reference does. */
.atmos {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 165vh;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

.atmos__aurora {
  --comb: repeating-linear-gradient(
    100deg,
    var(--black) 0%,
    var(--black) 7%,
    var(--transparent) 10%,
    var(--transparent) 12%,
    var(--black) 16%
  );
  --band: repeating-linear-gradient(
    100deg,
    var(--blue-500) 10%,
    var(--indigo-300) 15%,
    var(--blue-300) 20%,
    var(--violet-200) 25%,
    var(--blue-400) 30%
  );
  position: absolute;
  inset: -20%;
  background-image: var(--comb), var(--band);
  background-size: 300%, 200%;
  background-position: 50% 50%, 50% 50%;
  filter: blur(10px) invert(0);
  opacity: 0.62;
  mix-blend-mode: normal;
  animation: aurora 60s linear infinite;
  will-change: background-position;
}

/* A radial mask keeps the aurora as a glow behind the content rather than a
   full-bleed wash, which is what stops the headline going illegible. */
.atmos__aurora::after {
  content: '';
  position: absolute;
  inset: 0;
  /* Bottom-weighted rather than centre-weighted. A radial vignette centred on
     the viewport crushes a dark band straight through the headline; sinking it
     downward instead keeps colour behind the hero and still gives the footer a
     solid ground to sit on. */
  /* --ta-grad-y is a registered custom property, so the browser interpolates
     it as a percentage rather than swapping two strings. That is what lets the
     mask's waist drift — an unregistered var here would just snap. */
  background: linear-gradient(
    180deg,
    rgba(23, 23, 26, 0.12) 0%,
    rgba(23, 23, 26, 0.3) calc(27% + var(--ta-grad-y)),
    rgba(23, 23, 26, 0.72) 78%,
    var(--ink) 100%
  );
  animation: ta-gradient-drift 24s var(--ease-std) infinite alternate;
}

/* The two colour blobs. Both sit at opacity 0 and are raised by the audience
   choice — a 4.5s expo-out fade, slow enough that it reads as the room
   changing temperature rather than a state toggle. */
.atmos__blob {
  position: absolute;
  width: 90vw;
  height: 90vh;
  filter: blur(100px);
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity var(--dur-atmosphere) var(--ease-expo);
  will-change: opacity;
}

.atmos__blob--warm {
  left: -10vw;
  top: -20vh;
  background: radial-gradient(
    65% 65%,
    rgba(244, 114, 182, 0.7) 0%,
    rgba(217, 70, 239, 0.45) 30%,
    rgba(147, 51, 234, 0.2) 60%,
    rgba(0, 0, 0, 0) 90%
  );
}

.atmos__blob--cool {
  right: -10vw;
  top: -10vh;
  background: radial-gradient(
    70% 60%,
    rgba(34, 211, 238, 0.7) 0%,
    rgba(14, 165, 233, 0.45) 30%,
    rgba(37, 99, 235, 0.2) 60%,
    rgba(0, 0, 0, 0) 90%
  );
}

body[data-audience='contractors'] .atmos__blob--cool,
body[data-audience='partners'] .atmos__blob--warm {
  opacity: 1;
}

/* ===========================================================================
   TYPE SCALE
   Letter-spacing carries the hierarchy: display sizes go negative, small
   labels go far positive. That contrast is the whole typographic idea.
   =========================================================================== */

.kicker {
  font-family: var(--font-sans);
  font-size: clamp(15px, 1.4vw, 20px);
  font-weight: 400;
  letter-spacing: 1px;
  color: var(--fg-muted);
  margin: 0;
}

.micro {
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 300;
  letter-spacing: 3.6px;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin: 0;
}

.tiny {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 2.8px;
  text-transform: uppercase;
  color: var(--fg-faint);
}

.eyebrow {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--fg);
}

.display {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 0.98;
  letter-spacing: -0.03em;
  margin: 0;
  font-size: clamp(2.75rem, 7.5vw, 5.75rem);
}

.display--sm {
  font-size: clamp(2rem, 4.4vw, 3.25rem);
  line-height: 1.04;
  letter-spacing: -0.025em;
}

.lede {
  font-family: var(--font-sans);
  font-size: clamp(1rem, 1.5vw, 1.25rem);
  color: var(--fg-dim);
  max-width: 46ch;
  margin: 0;
}

.mono {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--fg-faint);
}

/* ===========================================================================
   NAV — a glass pill. backdrop-filter is the expensive part, so it is dropped
   on narrow viewports where it buys least and costs most.
   =========================================================================== */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 18px var(--gutter);
  transition: transform var(--dur-mid) var(--ease-expo),
    background-color var(--dur-hover) var(--ease-std);
}

.nav[data-hidden='true'] {
  transform: translateY(-110%);
}

.wordmark {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -0.65px;
  text-transform: lowercase;
  color: var(--fg);
  white-space: nowrap;
}

.nav__pill {
  display: none;
  align-items: center;
  gap: 2px;
  padding: 6px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: rgba(23, 23, 26, 0.55);
  backdrop-filter: blur(12px);
}

.nav__link {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 500;
  padding: 8px 14px;
  border-radius: 999px;
  color: var(--fg);
  transition: background-color var(--dur-hover) var(--ease-std),
    color var(--dur-hover) var(--ease-std);
}

.nav__link:hover {
  background: rgba(255, 255, 255, 0.09);
}

.nav__right {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* The circle-to-pill button. A 48px circle whose width animates to 100% on
   hover while the label slides in behind the arrow. `all 500ms ease` is what
   the reference uses; it is imprecise but the imprecision is the feel. */
.cta {
  position: relative;
  display: block;
  width: 252px;
  height: auto;
  padding: 4px;
  border: 1px solid var(--hairline-strong);
  border-radius: 999px;
  background: rgba(23, 23, 26, 0.4);
  backdrop-filter: blur(12px);
  outline: none;
  font-family: var(--font-display);
  cursor: pointer;
}

.cta__circle {
  display: block;
  width: 48px;
  height: 48px;
  overflow: hidden;
  border-radius: 999px;
  background: var(--paper);
  transition: all var(--dur-mid) ease;
}

.cta:hover .cta__circle,
.cta:focus-visible .cta__circle {
  width: 100%;
}

.cta__icon {
  position: absolute;
  top: 50%;
  left: 16px;
  transform: translate(0, -50%);
  color: var(--ink);
  transition: all var(--dur-mid) ease;
}

.cta:hover .cta__icon,
.cta:focus-visible .cta__icon {
  transform: translate(calc(252px - 78px), -50%);
}

.cta__label {
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: 16px;
  transform: translate(-50%, -50%);
  text-align: center;
  font-size: 16px;
  font-weight: 500;
  letter-spacing: -0.4px;
  color: var(--fg);
  white-space: nowrap;
  transition: all var(--dur-mid) ease;
}

.cta:hover .cta__label,
.cta:focus-visible .cta__label {
  margin-left: 0;
  color: var(--ink);
}

.burger {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--hairline);
  background: rgba(23, 23, 26, 0.45);
  backdrop-filter: blur(12px);
  transition: background-color var(--dur-hover) var(--ease-std);
}

.burger:hover {
  background: rgba(255, 255, 255, 0.1);
}

@media (min-width: 1080px) {
  .nav__pill {
    display: flex;
  }
  .burger {
    display: none;
  }
}

/* ===========================================================================
   THE GATE
   A full-viewport fork that asks who the reader is before the site says
   anything. It does not scroll; choosing an answer dissolves it.
   =========================================================================== */

/* Not 100svh: a full-viewport first screen with the content behind it is an
   interstitial, and it pushed the whole page out of the first frame. At 88svh
   the top of the hero shows below the fold line, which is also the affordance
   that tells a reader the page continues. */
.gate {
  position: relative;
  z-index: 10;
  min-height: 88svh;
  display: grid;
  place-items: center;
  padding: 96px var(--gutter) 48px;
}

.gate__inner {
  width: 100%;
  max-width: 1180px;
  text-align: center;
}

.gate__kicker {
  margin-bottom: clamp(28px, 5vh, 56px);
}

.gate__choices {
  display: grid;
  gap: clamp(20px, 4vw, 56px);
  grid-template-columns: 1fr;
}

@media (min-width: 820px) {
  .gate__choices {
    grid-template-columns: 1fr 1fr;
  }
}

.choice {
  position: relative;
  display: block;
  width: 100%;
  padding: clamp(18px, 2.4vw, 30px) clamp(14px, 2vw, 26px);
  border-radius: var(--radius-card);
  text-align: center;
  transition: background-color var(--dur-hover) var(--ease-std),
    transform var(--dur-hover) var(--ease-expo);
}

.choice::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(
    60% 60%,
    rgba(255, 255, 255, 0.7) 0%,
    rgba(255, 255, 255, 0.15) 35%,
    rgba(0, 0, 0, 0) 65%
  );
  opacity: 0;
  mix-blend-mode: soft-light;
  transition: opacity 220ms ease-out;
  pointer-events: none;
}

.choice:hover::after,
.choice:focus-visible::after {
  opacity: 1;
}

.choice:hover {
  transform: translateY(-2px);
}

.choice__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(2rem, 5vw, 3.5rem);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--fg);
  margin: 0;
}

.choice__sub {
  margin: 14px auto 0;
  max-width: 34ch;
  font-size: 14px;
  color: var(--fg-dim);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--dur-hover) var(--ease-std),
    transform var(--dur-hover) var(--ease-expo);
}

.choice:hover .choice__sub,
.choice:focus-visible .choice__sub {
  opacity: 1;
  transform: none;
}

.gate__foot {
  margin-top: clamp(36px, 7vh, 76px);
  display: grid;
  justify-items: center;
  gap: 14px;
}

.gate__hint {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: rgba(23, 23, 26, 0.5);
  backdrop-filter: blur(12px);
  font-size: 14px;
  /* Two animations, two jobs: `preset-hint` is the arrival — held back until
     the choices have landed, so it reads as an afterthought rather than a
     third option — and `pill-pulse` is the standing halo once it is there. */
  animation: ta-preset-hint 900ms var(--ease-expo) 900ms both,
    ta-pill-pulse 3.2s var(--ease-expo) 1800ms infinite;
}

.gate__hint b {
  font-weight: 500;
  color: rgba(255, 255, 255, 0.85);
}

.gate__hint span {
  font-weight: 300;
  color: var(--fg-dim);
}

/* ===========================================================================
   MAIN — revealed once the gate is answered.
   =========================================================================== */

/* The site is always rendered. It used to be display:none until a click set
   `data-audience`, which meant a crawler — which never clicks — saw the gate
   and nothing else: 639 characters, and an <h1> with no layout box. The gate
   still does its job as the first screen, but it is now a section the page
   scrolls past rather than a wall the content sits behind. */
.site {
  position: relative;
  z-index: 10;
  display: block;
}

/* Views arrive from the right, they do not fade up from below. The gate is
   lifted out of flow for the length of its exit so the two can cross over one
   another; `data-gone` is what finally takes it out of the document, and JS
   sets it on animationend (or immediately, when there is nothing to animate). */
.gate[data-dismissed='true'] {
  position: fixed;
  inset: 0;
  z-index: 20;
  pointer-events: none;
  animation: ta-page-out 640ms var(--ease-expo) both;
}

.gate[data-gone='true'] {
  display: none;
}

.site[data-entering='true'] {
  animation: ta-page-in 780ms var(--ease-expo) both;
}

.section {
  padding-block: clamp(72px, 12vh, 160px);
}

.section__head {
  display: grid;
  gap: 18px;
  margin-bottom: clamp(36px, 6vh, 72px);
  max-width: 62ch;
}

.hero {
  padding-top: clamp(120px, 18vh, 200px);
  padding-bottom: clamp(48px, 8vh, 96px);
}

.hero__kicker {
  margin-bottom: 22px;
}

.hero__lede {
  margin-top: 28px;
}

/* The showreel plate. 48px radius matches the card-stack's resting radius so
   the two systems look like the same object family. */
.plate {
  position: relative;
  margin-top: clamp(40px, 7vh, 88px);
  border-radius: var(--radius-card);
  border: 1px solid var(--hairline);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.03));
  aspect-ratio: 16 / 9;
  overflow: hidden;
  display: grid;
  place-items: center;
}

.plate__button {
  display: grid;
  gap: 10px;
  justify-items: center;
  padding: 22px 26px;
  border-radius: 18px;
  background: rgba(16, 16, 19, 0.72);
  backdrop-filter: blur(8px);
  transition: transform var(--dur-hover) var(--ease-expo),
    background-color var(--dur-hover) var(--ease-std);
}

.plate__button:hover {
  transform: scale(1.04);
  background: rgba(16, 16, 19, 0.85);
}

/* Word-stagger paragraph. Each word is its own span so the blur can land per
   word; the em-relative travel means it scales with the type size. */
.wordblock {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 2.6vw, 2.1rem);
  font-weight: 500;
  line-height: 1.32;
  letter-spacing: -0.02em;
  max-width: 34ch;
  margin: 0;
}

.wordblock .w {
  display: inline-block;
  white-space: pre;
}

/* ---------------------------------------------------------------------------
   THE ARTEFACT
   The alert, drawn. Text inside inline SVG inherits the document's fonts, so
   this is styled with the same tokens as the rest of the page rather than with
   presentation attributes — one place to change the type, and it stays legible
   when the SVG scales.
   --------------------------------------------------------------------------- */

.artefact {
  margin: clamp(36px, 6vh, 72px) 0 0;
  max-width: 720px;
}

.artefact__scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.artefact__svg {
  width: 100%;
  /* Scaled to a 390px screen, a 720-unit viewBox renders 16px body text at
     under 8px. The card is an illustration of a real message, so shrinking it
     past legibility defeats it — it keeps a floor width and scrolls inside its
     own container, the same trade the flow diagram makes. */
  min-width: 460px;
  height: auto;
  display: block;
  filter: drop-shadow(0 40px 80px rgba(0, 0, 0, 0.55));
}

.artefact__cap {
  margin-top: 22px;
}

.art-bubble {
  fill: rgba(255, 255, 255, 0.05);
  stroke: rgba(255, 255, 255, 0.09);
}

.art-chip {
  fill: rgba(147, 197, 253, 0.1);
  stroke: rgba(147, 197, 253, 0.3);
}

.art-rule {
  stroke: rgba(255, 255, 255, 0.1);
}

.art-edge {
  fill: none;
  stroke: rgba(255, 255, 255, 0.14);
}

.art-dot {
  fill: var(--blue-300);
}

.art-name {
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 600;
  fill: var(--fg);
}

.art-title {
  font-family: var(--font-display);
  font-size: 25px;
  font-weight: 600;
  letter-spacing: -0.02em;
  fill: var(--fg);
}

.art-body {
  font-family: var(--font-sans);
  font-size: 16px;
  fill: rgba(255, 255, 255, 0.82);
}

.art-meta {
  font-family: var(--font-sans);
  font-size: 13px;
  fill: var(--fg-faint);
}

.art-mono {
  font-family: var(--font-mono);
  font-size: 14px;
  fill: var(--fg-dim);
}

.art-value {
  fill: var(--fg);
  font-size: 17px;
}

.art-time {
  font-family: var(--font-mono);
  font-size: 12px;
  fill: var(--fg-faint);
}

.art-chip-label {
  font-family: var(--font-mono);
  font-size: 12.5px;
  letter-spacing: 0.06em;
  fill: var(--blue-300);
}

/* SVG has no `text-align`; anchoring at the end is the equivalent, and keeps
   the right-hand column aligned to the card's inner edge at every scale. */
.art-right {
  text-anchor: end;
}

/* ---------------------------------------------------------------------------
   THE CARD STACK
   Four numbered services. Authored as a plain list and *upgraded* into a
   scroll-driven stack by JS — so with scripting off, reduced motion on, or a
   viewport too short to pin against, this is still four readable rows and
   nothing is hidden behind an animation that never runs.

   The choreography is split across four elements because all four stack-*
   keyframes touch a different property and would otherwise overwrite each
   other on one box:

     .stack__card   ta-stack-cover    arrives from translateX(100%)   hold 35%
     .stack__inner  ta-stack-recede   recedes to scale(.86)/48px      hold 55%
     .stack__veil   ta-stack-dismiss  darkens the covered card        hold 22%
     .stack__body   ta-stack-advance  lifts the incoming copy in      hold 88%

   Each is a paused 1s animation scrubbed by a negative animation-delay, so
   scroll position drives the frame directly and the browser still composites
   it. --enter and --exit are written by main.js.
   --------------------------------------------------------------------------- */

/* Fallback presentation: the numbered list this section has always been. */
.stack {
  border-top: 1px solid var(--hairline);
}

.stack__card {
  border-bottom: 1px solid var(--hairline);
}

.stack__inner {
  position: relative;
  transition: background-color var(--dur-hover) var(--ease-std);
}

.stack__card:hover .stack__inner {
  background: rgba(255, 255, 255, 0.03);
}

.stack__body {
  display: grid;
  gap: 8px 28px;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  padding-block: clamp(22px, 3.4vh, 38px);
}

.stack__veil {
  display: none;
}

/* Stack presentation. `on` is set by JS only, and only when it can scrub. */
.stack[data-stack='on'] {
  position: relative;
  border-top: 0;
  /* One viewport to read the first card in, then 80vh of travel per hand-off.
     Four cards -> 340vh, which is the section carrying its own weight rather
     than four rows going past in one screen. */
  height: calc(100vh + (var(--n, 1) - 1) * 80vh);
}

.stack[data-stack='on'] .stack__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  place-items: center;
  /* The incoming card starts a full width to the right of the frame; without
     this it would widen the page instead of waiting off-stage. */
  overflow: hidden;
}

.stack[data-stack='on'] .stack__view {
  position: relative;
  width: 100%;
  max-width: 960px;
  height: min(64vh, 560px);
  /* The clip belongs here, not on the pin. A card waiting off-stage is exactly
     one card-width to the right, which is still inside a 1280px pin around a
     960px card — so with the clip on the pin, the next two cards sit visibly
     parked in the right margin. Clipping to the card frame is what makes them
     arrive from somewhere rather than slide out of a queue. */
  overflow: hidden;
  border-radius: var(--radius-stack);
}

.stack[data-stack='on'] .stack__card {
  position: absolute;
  inset: 0;
  border-bottom: 0;
  z-index: var(--z, 0);
  animation: ta-stack-cover 1s linear both paused;
  animation-delay: calc(var(--enter, 1) * -1s);
  will-change: transform;
}

.stack[data-stack='on'] .stack__inner {
  position: absolute;
  inset: 0;
  overflow: hidden;
  display: grid;
  align-content: end;
  padding: clamp(28px, 4.5vw, 64px);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-stack);
  /* Opaque, and lighter than the page. A translucent card lets the one behind
     it show through, and two sets of copy over each other at 8% opacity is
     unreadable at exactly the moment the reader is looking at it. */
  background: linear-gradient(160deg, #26262c 0%, #1b1b20 55%, #141418 100%);
  box-shadow: 0 40px 90px -20px rgba(0, 0, 0, 0.7);
  transform-origin: 50% 100%;
  animation: ta-stack-recede 1s linear both paused;
  animation-delay: calc(var(--exit, 0) * -1s);
  will-change: transform, border-radius;
}

.stack[data-stack='on'] .stack__body {
  grid-template-columns: 1fr;
  gap: 14px;
  padding-block: 0;
  max-width: 56ch;
  animation: ta-stack-advance 1s linear both paused;
  animation-delay: calc(var(--enter, 1) * -1s);
}

.stack[data-stack='on'] .stack__veil {
  display: block;
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--ink);
  pointer-events: none;
  animation: ta-stack-dismiss 1s linear both paused;
  animation-delay: calc(var(--exit, 0) * -1s);
}

.stack[data-stack='on'] .row__body {
  grid-column: auto;
  font-size: clamp(15px, 1.4vw, 18px);
}

.stack[data-stack='on'] .row__title {
  font-size: clamp(2rem, 4.4vw, 3.25rem);
}

.row__num {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--fg-faint);
  letter-spacing: 0.08em;
}

.row__title {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  font-weight: 600;
  letter-spacing: -0.025em;
  margin: 0;
}

.row__body {
  grid-column: 2;
  margin: 6px 0 0;
  color: var(--fg-dim);
  font-size: 15px;
  max-width: 52ch;
}

/* Capability columns. */
.caps {
  display: grid;
  gap: clamp(28px, 4vw, 56px);
  grid-template-columns: 1fr;
  margin-top: clamp(44px, 7vh, 88px);
}

@media (min-width: 760px) {
  .caps {
    grid-template-columns: repeat(3, 1fr);
  }
}

.caps__group h3 {
  margin: 0 0 16px;
}

.caps__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
}

.caps__list li {
  font-size: 15px;
  color: rgba(255, 255, 255, 0.82);
  padding-bottom: 10px;
  border-bottom: 1px solid var(--hairline);
}

/* Tile grid — coverage cards. */
.tiles {
  display: grid;
  gap: 16px;
  grid-template-columns: 1fr;
}

@media (min-width: 680px) {
  .tiles {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1040px) {
  .tiles {
    grid-template-columns: repeat(3, 1fr);
  }
}

.tile {
  padding: 26px;
  border-radius: var(--radius);
  border: 1px solid var(--hairline);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02));
  display: grid;
  gap: 12px;
  align-content: start;
  transition: transform var(--dur-hover) var(--ease-expo),
    border-color var(--dur-hover) var(--ease-std);
}

.tile:hover {
  transform: translateY(-3px);
  border-color: var(--hairline-strong);
}

/* Live status dot. `pulse` is the only ambient motion below the fold, so it
   is slow and low-contrast — a source being watched, not a notification. */
.dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 7px;
  border-radius: 999px;
  background: var(--blue-300);
  vertical-align: middle;
  animation: ta-pulse 2.4s var(--ease-std) infinite;
}

.tile__label {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}

.tile h3 {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  margin: 0;
}

.tile p {
  margin: 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--fg-dim);
}

/* ---------------------------------------------------------------------------
   THE FOCUSED PULSE GRID
   ---------------------------------------------------------------------------
   Hovering a coverage tile does not lift it over its neighbours. It takes room
   from them: the hovered column widens, its row deepens, and the rest of the
   grid gives up exactly that much, so the block occupies the same box before,
   during and after. Nothing below it moves.

   Everything here is gated on [data-pulse], which main.js only sets once it has
   measured the grid and confirmed a fine pointer, more than one column and no
   stated motion preference. Without that attribute this whole block is inert
   and the plain hover above is what runs.
   --------------------------------------------------------------------------- */

.tiles[data-pulse] {
  transition: grid-template-columns var(--dur-focus) var(--ease-focus),
    grid-template-rows var(--dur-focus) var(--ease-focus);
}

.tiles[data-pulse] .tile {
  /* Grid items floor at min-content by default, which would stop a compressed
     track from actually compressing. */
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  transition: border-color var(--dur-focus) var(--ease-focus),
    background var(--dur-focus) var(--ease-focus),
    padding var(--dur-focus) var(--ease-focus),
    gap var(--dur-focus) var(--ease-focus),
    opacity var(--dur-focus) var(--ease-focus);
}

/* The card earns room in the layout, so it has no business floating above it
   as well. */
.tiles[data-pulse] .tile:hover {
  transform: none;
}

/* The deeper row is spent on the card, not left as a void under it: the
   focused tile takes more padding and more space between its own parts, so the
   room it took from its neighbours reads as breathing room. */
.tiles[data-pulse] .tile[data-focus] {
  padding: 32px;
  gap: 16px;
  border-color: var(--hairline-strong);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.085), rgba(255, 255, 255, 0.03));
}

/* The dimming is what makes it read as focus rather than as a wobble. It is
   deliberately shallow — these are still readable, just not the subject. */
.tiles[data-pulse][data-focused] .tile:not([data-focus]) {
  opacity: 0.55;
}

.tiles[data-pulse] .tile p {
  transition: line-height var(--dur-focus) var(--ease-focus),
    font-size var(--dur-focus) var(--ease-focus),
    color var(--dur-focus) var(--ease-focus);
}

/* The focused card's copy opens up; the compressed ones close down, which is
   both the read (this one, not those) and part of how they survive the narrower
   column without their last line falling off. */
.tiles[data-pulse] .tile[data-focus] p {
  line-height: 1.78;
  color: rgba(255, 255, 255, 0.74);
}

.tiles[data-pulse][data-focused] .tile:not([data-focus]) p {
  font-size: 13.4px;
  line-height: 1.46;
}

/* Measuring writes and clears track lists inside one frame, and reads the
   result synchronously. Every rule above it is transitioned, so a measurement
   taken through one reads the value the grid is animating FROM rather than the
   value it is measuring for — which is why this sits last in the block and
   must stay last: it ties on specificity with the rules it has to beat. */
.tiles.is-measuring,
.tiles.is-measuring .tile,
.tiles.is-measuring .tile p {
  transition: none;
}

/* ---------------------------------------------------------------------------
   THE FLOW DIAGRAM
   Wide by nature. Scaling it down to a phone puts the labels under 5px, so it
   keeps a floor width and scrolls inside its own container instead — the page
   itself must never scroll sideways.
   --------------------------------------------------------------------------- */

.flow {
  margin: clamp(32px, 5vh, 56px) 0 0;
}

.flow__scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  padding-bottom: 4px;
}

.flow__svg {
  width: 100%;
  min-width: 720px;
  height: auto;
  display: block;
}

.flow-box {
  fill: rgba(255, 255, 255, 0.035);
  stroke: var(--hairline);
}

.flow-box--live {
  fill: rgba(147, 197, 253, 0.06);
  stroke: rgba(147, 197, 253, 0.28);
}

.flow-box--quiet {
  fill: transparent;
  stroke: rgba(255, 255, 255, 0.09);
}

.flow-pill {
  fill: rgba(255, 255, 255, 0.05);
  stroke: var(--hairline);
}

.flow-line {
  stroke: rgba(255, 255, 255, 0.28);
  stroke-width: 1.5;
  fill: none;
}

.flow-line--muted {
  stroke: rgba(255, 255, 255, 0.14);
  stroke-dasharray: 4 4;
}

.flow-arrowhead {
  fill: rgba(255, 255, 255, 0.42);
}

.flow-rule {
  stroke: var(--hairline);
}

.flow-dot {
  fill: var(--blue-300);
}

.flow-step {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 2.4px;
  fill: var(--fg-faint);
}

.flow-step--live {
  fill: var(--blue-300);
}

.flow-count {
  font-family: var(--font-display);
  font-size: 40px;
  font-weight: 600;
  letter-spacing: -0.03em;
  fill: var(--fg);
}

.flow-label {
  font-family: var(--font-sans);
  font-size: 15px;
  fill: rgba(255, 255, 255, 0.86);
}

.flow-pill-label {
  font-family: var(--font-sans);
  font-size: 13px;
  fill: rgba(255, 255, 255, 0.8);
}

.flow-small {
  font-family: var(--font-sans);
  font-size: 12.5px;
  fill: var(--fg-faint);
}

.flow-small--muted {
  fill: rgba(255, 255, 255, 0.3);
}

.flow-tick {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  fill: var(--fg-faint);
}

.flow-tick--warn {
  fill: rgba(147, 197, 253, 0.65);
}

/* Draft state — used where there is genuinely nothing to show yet. Saying so
   is better than inventing a testimonial. */
.draft {
  padding: clamp(32px, 6vh, 64px);
  border: 1px dashed var(--hairline-strong);
  border-radius: var(--radius-card);
  text-align: center;
  display: grid;
  gap: 12px;
  justify-items: center;
}

.draft p {
  margin: 0;
  color: var(--fg-dim);
  max-width: 46ch;
}

/* Closing CTA. */
.close {
  padding-block: clamp(88px, 16vh, 190px);
  text-align: center;
}

.close__actions {
  margin-top: clamp(28px, 5vh, 52px);
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  justify-content: center;
  align-items: center;
}

.linkish {
  font-family: var(--font-sans);
  font-size: 15px;
  color: var(--fg-dim);
  border-bottom: 1px solid var(--hairline-strong);
  padding-bottom: 2px;
  transition: color var(--dur-hover) var(--ease-std),
    border-color var(--dur-hover) var(--ease-std);
}

.linkish:hover {
  color: var(--fg);
  border-color: var(--fg);
}

/* Footer. */
.foot {
  position: relative;
  z-index: 10;
  border-top: 1px solid var(--hairline);
  padding-block: clamp(48px, 8vh, 88px) 32px;
}

.foot__cols {
  display: grid;
  gap: clamp(28px, 4vw, 48px);
  grid-template-columns: repeat(2, 1fr);
}

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

.foot__cols h3 {
  margin: 0 0 14px;
}

.foot__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
}

.foot__list a {
  font-size: 15px;
  color: rgba(255, 255, 255, 0.78);
  transition: color var(--dur-quick) var(--ease-std);
}

.foot__list a:hover {
  color: var(--fg);
}

.foot__base {
  margin-top: clamp(40px, 7vh, 76px);
  padding-top: 24px;
  border-top: 1px solid var(--hairline);
  display: flex;
  flex-wrap: wrap;
  gap: 14px 24px;
  align-items: center;
  justify-content: space-between;
}

/* Cookie notice. */
.cookies {
  position: fixed;
  z-index: 60;
  left: var(--gutter);
  right: var(--gutter);
  bottom: 20px;
  margin-inline: auto;
  max-width: 680px;
  padding: 18px 20px;
  border: 1px solid var(--hairline-strong);
  border-radius: 18px;
  background: rgba(16, 16, 19, 0.9);
  backdrop-filter: blur(12px);
  display: grid;
  gap: 14px;
  transform: translateY(150%);
  transition: transform var(--dur-slow) var(--ease-expo);
}

.cookies[data-shown='true'] {
  transform: none;
}

.cookies p {
  margin: 0;
  font-size: 14px;
  color: var(--fg-dim);
}

.cookies__row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.btn {
  padding: 10px 18px;
  border-radius: 999px;
  border: 1px solid var(--hairline-strong);
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 500;
  transition: background-color var(--dur-hover) var(--ease-std),
    color var(--dur-hover) var(--ease-std);
}

.btn:hover {
  background: rgba(255, 255, 255, 0.1);
}

.btn--solid {
  background: var(--paper);
  color: var(--ink);
  border-color: var(--paper);
}

.btn--solid:hover {
  background: var(--white);
}

/* ===========================================================================
   REVEAL FAMILY
   Blur and travel scale together with the size of the thing arriving: a tile
   rises 14px with no blur, a headline letter rises 60px through 14px of blur.
   Elements start hidden only when JS is present to un-hide them, so the page
   still reads with scripting off.
   =========================================================================== */

.js [data-reveal] {
  opacity: 0;
}

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

.js [data-reveal='rise'].is-in {
  animation: ta-rise-in var(--dur-slow) var(--ease-expo) both;
}

.js [data-reveal='rise-blur'].is-in {
  animation: ta-rise-blur-in var(--dur-slow) var(--ease-expo) both;
}

.js [data-reveal='tile'].is-in {
  animation: ta-tile-in 700ms var(--ease-expo) both;
}

.js [data-reveal='words'] {
  opacity: 1;
}

.js [data-reveal='words'] .w {
  opacity: 0;
}

.js [data-reveal='words'].is-in .w {
  animation: ta-word-in 820ms var(--ease-expo) both;
  animation-delay: var(--d, 0ms);
}

.js [data-reveal='letters'] {
  opacity: 1;
}

/* The word wrapper is what keeps a split headline from breaking mid-word.
   Without it every letter is its own inline-block and the line can break
   between any two of them — "Botswana Contr / actors". The letters animate;
   the wrapper only holds them together. */
.js [data-reveal='letters'] .w,
.js [data-reveal='words'] .w {
  display: inline-block;
  white-space: pre;
}

.js [data-reveal='letters'] .l {
  display: inline-block;
  opacity: 0;
  white-space: pre;
}

.js [data-reveal='letters'].is-in .l {
  animation: ta-letter-in 1s var(--ease-expo) both;
  animation-delay: var(--d, 0ms);
}

@keyframes aurora {
  0% {
    background-position: 50% center, 50% center;
  }
  100% {
    background-position: 350% center, 350% center;
  }
}

@keyframes ta-rise-in {
  0% {
    opacity: 0;
    transform: translateY(12px);
  }
  100% {
    opacity: 1;
    transform: none;
  }
}

@keyframes ta-rise-blur-in {
  0% {
    opacity: 0;
    filter: blur(6px);
    transform: translateY(14px);
  }
  100% {
    opacity: 1;
    filter: blur(0);
    transform: none;
  }
}

@keyframes ta-tile-in {
  0% {
    opacity: 0;
    translate: 0 14px;
  }
  100% {
    opacity: 1;
    translate: 0;
  }
}

@keyframes ta-word-in {
  0% {
    opacity: 0;
    filter: blur(12px);
    transform: translateY(0.35em);
  }
  100% {
    opacity: 1;
    filter: blur(0);
    transform: none;
  }
}

@keyframes ta-letter-in {
  0% {
    opacity: 0;
    filter: blur(14px);
    transform: translateY(60px);
  }
  100% {
    opacity: 1;
    filter: blur(0);
    transform: none;
  }
}

@keyframes ta-pill-pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.18);
  }
  100% {
    box-shadow: 0 0 0 12px rgba(255, 255, 255, 0);
  }
}

@keyframes ta-pulse {
  0% {
    opacity: 0.35;
    box-shadow: 0 0 0 0 rgba(147, 197, 253, 0.5);
  }
  50% {
    opacity: 1;
    box-shadow: 0 0 0 4px rgba(147, 197, 253, 0);
  }
  100% {
    opacity: 0.35;
    box-shadow: 0 0 0 0 rgba(147, 197, 253, 0);
  }
}

@keyframes ta-preset-hint {
  0% {
    opacity: 0;
    transform: translateY(10px) scale(0.96);
    filter: blur(6px);
  }
  100% {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}

/* The aurora mask's waist, drifting. Registered as <percentage> at the top of
   this file, which is the only reason it interpolates instead of snapping. */
@keyframes ta-gradient-drift {
  0% {
    --ta-grad-y: 14%;
  }
  100% {
    --ta-grad-y: 26%;
  }
}

/* ---------------------------------------------------------------------------
   THE STACK KEYFRAMES
   One system, four parts. Every one of these is scrubbed by scroll position
   rather than played on a clock, so the percentages below are *positions in
   the hand-off*, not moments in time. The hold points are the whole trick:
   each part finishes well before the hand-off does and then sits still, which
   is what makes four separate animations read as one movement.

   Do not round these numbers off.
   --------------------------------------------------------------------------- */

/* The card being covered. Falls back and away — scale(.86), corners opening
   to 48px — and is fully receded by 55%, so it has settled behind the new
   card long before that card finishes arriving. */
@keyframes ta-stack-recede {
  0% {
    transform: scale(1) translateY(0);
    border-radius: var(--radius-stack);
  }
  55% {
    transform: scale(0.86) translateY(-3%);
    border-radius: 48px;
  }
  100% {
    transform: scale(0.86) translateY(-3%);
    border-radius: 48px;
  }
}

/* The card arriving. Crosses from a full width off-stage right and is home by
   35% — early, because the rest of the hand-off is the old card settling, and
   a card still sliding while the one behind it has stopped reads as a lag. */
@keyframes ta-stack-cover {
  0% {
    transform: translate3d(100%, 0, 0);
  }
  35% {
    transform: translate3d(0, 0, 0);
  }
  100% {
    transform: translate3d(0, 0, 0);
  }
}

/* The covered card's copy, going. Out by 22% — the earliest of the four —
   because two sets of text overlapping through a translucent card edge is the
   one thing that makes the whole stack look broken rather than layered. */
@keyframes ta-stack-dismiss {
  0% {
    opacity: 0;
  }
  22% {
    opacity: 0.55;
  }
  100% {
    opacity: 0.78;
  }
}

/* The arriving card's copy. Latest of the four at 88%, so the text lands after
   the card it is written on has stopped moving. */
@keyframes ta-stack-advance {
  0% {
    opacity: 0;
    transform: translateY(28px);
    filter: blur(6px);
  }
  88% {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
  100% {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}

/* Same hold point, no blur. Paired with the 760px gate below. */
@keyframes ta-stack-advance-flat {
  0% {
    opacity: 0;
    transform: translateY(28px);
  }
  88% {
    opacity: 1;
    transform: none;
  }
  100% {
    opacity: 1;
    transform: none;
  }
}

/* ---------------------------------------------------------------------------
   PAGE TRANSITIONS
   Views are side by side, not stacked: a new view crosses in from the right
   and the one it replaces is pushed left and out of focus. Same direction as
   the card stack, on purpose — the whole site moves along one axis.
   --------------------------------------------------------------------------- */
@keyframes ta-page-in {
  0% {
    opacity: 0;
    transform: translate3d(100%, 0, 0);
  }
  35% {
    opacity: 1;
  }
  100% {
    opacity: 1;
    transform: translate3d(0, 0, 0);
  }
}

@keyframes ta-page-out {
  0% {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1);
    filter: blur(0);
  }
  100% {
    opacity: 0;
    transform: translate3d(-24%, 0, 0) scale(0.94);
    filter: blur(10px);
  }
}

/* ===========================================================================
   PERFORMANCE AND ACCESSIBILITY GATES
   The audience for the product this site sells is a low-end Android on an
   intermittent connection. blur(100px) over a 90vw box and a blurred 300%
   gradient are the two most expensive things on the page, so both come off
   below the tablet breakpoint. The look degrades; the framerate does not.
   =========================================================================== */

/* Below 640px the wordmark, a 252px pill and the burger do not fit on one
   line, and the burger is the one that gets pushed off the edge. The pill
   collapses to its circle: the arrow still reads as "go", the accessible name
   still says where, and the hero and closing sections both carry a full-width
   WhatsApp button anyway, so nothing is actually lost. */
@media (max-width: 640px) {
  .cta {
    width: 56px;
  }
  .cta__label {
    display: none;
  }
  .cta:hover .cta__circle,
  .cta:focus-visible .cta__circle {
    width: 48px;
  }
  .cta:hover .cta__icon,
  .cta:focus-visible .cta__icon {
    transform: translate(0, -50%);
  }
}

@media (max-width: 760px) {
  .atmos__aurora {
    filter: none;
    opacity: 0.28;
    animation-duration: 120s;
  }
  /* The drift animates a registered custom property, which forces the whole
     165vh gradient to repaint on every frame. Cheap on a laptop, not on the
     phone this product is actually read on. */
  .atmos__aurora::after {
    animation: none;
  }
  .atmos__blob {
    filter: blur(60px);
    width: 120vw;
    height: 60vh;
  }
  .nav__pill,
  .cta,
  .burger,
  .gate__hint,
  .plate__button,
  .cookies {
    backdrop-filter: none;
  }
  /* The stack still runs here, but not the expensive parts of it. The blur in
     `stack-advance` is recomputed on every scroll frame, and a 90px shadow
     under a card that is simultaneously being scaled is the second worst thing
     on this page for a low-end Android. The movement survives; the garnish
     does not. */
  .stack[data-stack='on'] .stack__body {
    animation-name: ta-stack-advance-flat;
  }
  .stack[data-stack='on'] .stack__inner {
    box-shadow: none;
    /* Bottom-anchored copy reads as deliberate on a wide card. On a phone the
       same rule leaves two thirds of the card empty above four lines of text,
       which reads as a loading state. */
    align-content: center;
  }
  .stack[data-stack='on'] .stack__view {
    height: min(56vh, 420px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .atmos__aurora {
    animation: none;
  }
  .atmos__blob {
    transition-duration: 1ms;
  }
  .js [data-reveal],
  .js [data-reveal] .w,
  .js [data-reveal] .l {
    opacity: 1 !important;
    animation: none !important;
    filter: none !important;
    transform: none !important;
    translate: none !important;
  }
  .gate,
  .cookies,
  .cta__circle,
  .cta__icon,
  .cta__label {
    transition-duration: 1ms;
  }
  /* The stack never arms itself under reduced motion (main.js returns before
     setting data-stack), so this only has to cover the ambient loops and the
     page transitions, which are CSS-driven. */
  .atmos__aurora::after,
  .dot,
  .gate__hint {
    animation: none;
  }
  .gate[data-dismissed='true'],
  .site[data-entering='true'] {
    animation-duration: 1ms;
  }
}

/* ===========================================================================
   WORM TRAIL
   A canvas built by worm-cursor.js, sitting between the atmosphere (z 0) and
   the page content (`.site`, z 10) so headlines read over the top of the
   ribbon. Every section on this page has a transparent background, so one
   fixed layer here covers the whole document rather than just the hero.

   The gooey edge is an SVG filter set on the element by the script, not here —
   it carries numbers the script also uses for its own geometry, and splitting
   them across two files is how they drift apart.
   =========================================================================== */

.worm {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 5;
  pointer-events: none;
  /* The canvas is sized in its own attributes every resize; this only stops a
     stale backing store from stretching for the frame in between. */
  width: 100%;
  height: 100%;
}

/* No pointer to follow, so no ribbon. The script does not mount here either;
   this is the belt to that brace. */
@media (hover: none), (pointer: coarse) {
  .worm {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .worm {
    display: none;
  }
}

/* ===========================================================================
   CURSOR RETICLE
   The dot is created by cursor.js, so these rules only describe it. Position
   and scale are written to the transform every frame; nothing here
   transitions, because a transition on top of a spring fights the spring.
   =========================================================================== */

.reticle {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 50;
  width: 18px;
  height: 18px;
  border-radius: 999px;
  background: #fff;
  /* `difference` is what makes one dot legible over both the near-black
     ground and a white plate without ever choosing a colour for it. */
  mix-blend-mode: difference;
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
}

.reticle-label {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 50;
  transform: translate3d(-100px, -100px, 0);
  mix-blend-mode: difference;
  pointer-events: none;
  will-change: transform;
}

.reticle-label span {
  display: block;
  margin-left: -50%;
  white-space: nowrap;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: #fff;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 220ms var(--ease-expo), transform 220ms var(--ease-expo);
}

.reticle-label[data-shown='true'] span {
  opacity: 1;
  transform: none;
}

/* No pointer to follow on a touch screen, and no reticle either. */
@media (hover: none), (pointer: coarse) {
  .reticle,
  .reticle-label {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .reticle,
  .reticle-label {
    display: none;
  }
}

/* ===========================================================================
   DOCUMENT PAGES
   The reference and explainer pages at /coverage/, /gates/, /ppra-codes/ and
   the rest. They share the homepage's ground and type, and deliberately share
   none of its choreography: nothing here starts hidden, because these pages are
   entered from a search result and have to be complete on arrival.
   =========================================================================== */

/* The homepage's aurora is 165vh of blurred gradient anchored to a hero. A
   reference page has no hero to light, so it gets a short, quiet band that
   stops well above the content. */
.atmos--page {
  height: 60vh;
}

.atmos--page .atmos__aurora {
  opacity: 0.34;
}

.site--doc {
  display: block;
}

.doc__head {
  padding-top: clamp(112px, 16vh, 168px);
  padding-bottom: clamp(20px, 3vh, 36px);
  display: grid;
  gap: 16px;
  max-width: min(100%, 1400px);
}

.doc__lede {
  max-width: 58ch;
}

.doc__body {
  padding-top: clamp(28px, 4vh, 48px);
}

.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--fg-faint);
  margin-bottom: 6px;
}

.crumbs a {
  color: var(--fg-dim);
  transition: color var(--dur-quick) var(--ease-std);
}

.crumbs a:hover {
  color: var(--fg);
}

.crumbs__sep {
  color: rgba(255, 255, 255, 0.22);
}

.nav__link.is-current {
  color: var(--fg);
  background: rgba(255, 255, 255, 0.08);
}

/* Long-form prose. Measure is capped for reading, not for layout. */
.prose {
  max-width: 68ch;
  display: grid;
  gap: 18px;
}

.prose p {
  margin: 0;
  color: var(--fg-dim);
  font-size: clamp(15px, 1.35vw, 17px);
  line-height: 1.62;
}

.prose a {
  color: var(--blue-300);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.doc__h2 {
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 2vw, 1.5rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--fg);
  margin: 18px 0 0;
}

.doc__after {
  margin-top: clamp(36px, 6vh, 72px);
}

/* A tile that is a link needs to say so without becoming a button. */
.tile--link {
  text-decoration: none;
  position: relative;
}

.tile--link:hover {
  border-color: var(--hairline-strong);
  transform: translateY(-3px);
}

.tile__more {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--blue-300);
  margin-top: 4px;
}

.doc__nav {
  margin-top: clamp(40px, 7vh, 80px);
  padding-top: 24px;
  border-top: 1px solid var(--hairline);
}

.doc__navlist {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 28px;
}

.doc__navlist a {
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
  font-family: var(--font-display);
  font-weight: 500;
  color: var(--fg-dim);
  transition: color var(--dur-quick) var(--ease-std);
}

.doc__navlist a:hover {
  color: var(--fg);
}

.doc__navlist .mono {
  color: var(--fg-faint);
}

/* ---------------------------------------------------------------------------
   STAT ROW
   Used where the figures are the point of the page — the four appendices, the
   four pricing facts. Not a general-purpose card.
   --------------------------------------------------------------------------- */
.codes__stats {
  display: grid;
  gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  background: var(--hairline);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: clamp(28px, 4vh, 44px);
}

.stat {
  background: var(--ink-deep);
  padding: 18px 20px;
  display: grid;
  gap: 2px;
  align-content: start;
}

.stat__n {
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 3vw, 2.1rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.stat__k {
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-dim);
}

.stat__s {
  font-size: 12.5px;
  color: var(--fg-faint);
  line-height: 1.4;
}

/* ---------------------------------------------------------------------------
   THE CODE REGISTER
   397 rows. The table is authored into the HTML in full so the page is
   complete without JS; the controls only narrow what is displayed.
   --------------------------------------------------------------------------- */
.codes__controls {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 18px;
  align-items: center;
  margin-bottom: 16px;
}

.codes__search {
  flex: 1 1 260px;
}

.codes__search input {
  width: 100%;
  padding: 11px 15px;
  border-radius: 999px;
  border: 1px solid var(--hairline);
  background: rgba(255, 255, 255, 0.04);
  color: var(--fg);
  font: inherit;
  font-size: 15px;
}

.codes__search input::placeholder {
  color: var(--fg-faint);
}

.codes__search input:focus-visible {
  border-color: var(--blue-300);
}

.codes__filters {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.pill {
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid var(--hairline);
  background: transparent;
  color: var(--fg-dim);
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 500;
  transition: background-color var(--dur-quick) var(--ease-std),
    color var(--dur-quick) var(--ease-std);
}

.pill:hover {
  color: var(--fg);
}

.pill.is-on {
  background: var(--paper);
  border-color: var(--paper);
  color: var(--ink);
}

.codes__count {
  margin: 0;
  white-space: nowrap;
}

/* A 6-column table cannot shrink to 390px without becoming unreadable, so it
   scrolls in its own container and the page never scrolls sideways. */
.codes__scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}

table.codes {
  width: 100%;
  min-width: 720px;
  border-collapse: collapse;
  font-size: 14px;
}

table.codes th {
  position: sticky;
  top: 0;
  z-index: 1;
  text-align: left;
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-faint);
  background: var(--ink-deep);
  padding: 12px 14px;
  border-bottom: 1px solid var(--hairline);
}

table.codes td {
  padding: 10px 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.055);
  color: var(--fg-dim);
  vertical-align: top;
}

table.codes tr:last-child td {
  border-bottom: 0;
}

table.codes tbody tr:hover td {
  background: rgba(255, 255, 255, 0.03);
  color: var(--fg);
}

table.codes .mono {
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}

.codes__flags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.flag {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.04em;
  padding: 2px 7px;
  border-radius: 3px;
  white-space: nowrap;
}

.flag--grade {
  background: rgba(147, 197, 253, 0.12);
  color: var(--blue-300);
}

.flag--citizen {
  background: rgba(221, 214, 254, 0.12);
  color: var(--violet-200);
}

.codes__empty {
  margin: 18px 0 0;
  color: var(--fg-faint);
  font-size: 15px;
}

.gaps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px;
}

.gaps li {
  color: var(--fg-dim);
  font-size: 15px;
}

.gaps b {
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--fg);
  display: inline-block;
  min-width: 9ch;
}

/* ---------------------------------------------------------------------------
   THE SOURCE DIRECTORY
   --------------------------------------------------------------------------- */
.srcgroup {
  margin-bottom: clamp(32px, 5vh, 56px);
}

.srcgroup__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--hairline);
}

.srcgroup__head .doc__h2 {
  margin: 0;
}

.srcgroup__note {
  margin: 12px 0 16px;
  color: var(--fg-faint);
  font-size: 14px;
  max-width: 62ch;
}

.srclist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1px;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}

.src {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 14px;
  background: rgba(255, 255, 255, 0.025);
  border-radius: 6px;
}

.src__name {
  font-size: 14.5px;
  color: rgba(255, 255, 255, 0.86);
}

.src__state {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.05em;
  padding: 2px 7px;
  border-radius: 3px;
  white-space: nowrap;
}

.src__state--live {
  background: rgba(147, 197, 253, 0.12);
  color: var(--blue-300);
}

.src__state--verifying {
  background: rgba(255, 255, 255, 0.07);
  color: var(--fg-faint);
}

/* A qualifying note, on both the homepage and the document pages. */
.note {
  margin: clamp(24px, 3vh, 40px) 0 0;
  max-width: 68ch;
  font-size: 14px;
  color: var(--fg-faint);
  line-height: 1.6;
}

.note a {
  color: var(--blue-300);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.note b {
  color: var(--fg-dim);
  font-weight: 500;
}

.close--doc {
  border-top: 1px solid var(--hairline);
  margin-top: clamp(48px, 8vh, 96px);
}

/* Headings and tiles that link. The link is the heading, so it inherits the
   heading's type entirely and only signals itself on hover — an underlined
   display heading reads as a mistake at 5rem. */
.display a,
.tile h3 a,
.row__title a {
  color: inherit;
  text-decoration: none;
  transition: color var(--dur-quick) var(--ease-std);
}

.display a:hover,
.tile h3 a:hover,
.row__title a:hover {
  color: var(--blue-300);
}

/* ===========================================================================
   DIRECTIONAL PAGE TRANSITIONS
   ---------------------------------------------------------------------------
   Native cross-document view transitions. view-transition.js decides the
   direction and writes it to `data-vt-dir` on <html>; everything else is here.

   The mental model is a stack of cards, seen slightly from below. Forward: the
   page you are leaving falls back to scale(.86), its corners open to 48px and
   it dims, while the page you are going to slides in over it from the right.
   Back: the top card lifts a little, slides off right, and the card underneath
   un-shrinks into place.

   Three declarations do more work than they look like they do:

     mix-blend-mode: normal   The UA composites the two root snapshots with
                              `plus-lighter` so they cross-fade. Two opaque
                              layers stacked is what makes it read as cards
                              rather than a dissolve. Without this line the
                              whole effect is a fade with some scaling in it.
     transform-origin 50% 90% The recede pivots near the bottom of the screen,
                              so the outgoing page falls away from the reader
                              rather than shrinking toward the middle.
     overflow: clip           Snapshots are painted at their captured size; a
                              scaled one would otherwise bleed past the frame.

   The hold points are the choreography and are not adjustable: the recede is
   finished at 35% but keeps dimming to 100%, the cover leaves at 35% so it
   crosses a page that has already settled, the dismiss lands at 88% so the
   last 12% is nothing but the incoming page arriving.
   =========================================================================== */

@view-transition {
  navigation: auto;
}

/* Behind both snapshots for the moment mid-slide when neither covers the
   whole viewport. Anything but the page ground flashes as a seam. */
:root::view-transition {
  background-color: var(--ink);
}

:root::view-transition-group(root),
:root::view-transition-old(root),
:root::view-transition-new(root) {
  animation-duration: 1.4s;
  animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
}

:root::view-transition-old(root),
:root::view-transition-new(root) {
  overflow: clip;
  transform-origin: 50% 90%;
  mix-blend-mode: normal;
}

/* --- Forward. The default, and what an unknown route resolves to. --------- */

:root::view-transition-old(root) {
  z-index: 1;
  animation-name: ta-vt-recede;
}

:root::view-transition-new(root) {
  z-index: 2;
  animation-name: ta-vt-cover;
}

@keyframes ta-vt-recede {
  0% {
    transform: translate(0) scale(1);
    border-radius: 0;
    box-shadow: rgba(0, 0, 0, 0) 0 0;
    filter: brightness(1);
  }
  35% {
    transform: translate(0) scale(0.86);
    border-radius: 48px;
    box-shadow: rgba(0, 0, 0, 0.6) 0 24px 70px -20px;
    filter: brightness(1);
  }
  100% {
    transform: translate(0) scale(0.86);
    border-radius: 48px;
    box-shadow: rgba(0, 0, 0, 0.6) 0 24px 70px -20px;
    filter: brightness(0.62);
  }
}

/* The leading-edge shadow is carried for the whole animation, including while
   the panel is still off-screen, so the shadow arrives before the page does. */
@keyframes ta-vt-cover {
  0%,
  35% {
    transform: translate(100%);
    box-shadow: rgba(0, 0, 0, 0.55) -26px 0 64px -12px;
  }
  100% {
    transform: translate(0);
    box-shadow: rgba(0, 0, 0, 0.55) -26px 0 64px -12px;
  }
}

/* --- Back. Z-order inverted: the page being left is now the top card. -----
   Note the asymmetry with forward, which is deliberate. Forward parks the old
   page at scale(.86) and 48px; back only lifts it to scale(.94) and 28px. The
   destination of a recede and the origin of a dismiss are different depths,
   because one is a card being buried and the other is a card being picked up. */

:root[data-vt-dir='left']::view-transition-old(root) {
  z-index: 2;
  animation-name: ta-vt-dismiss;
}

:root[data-vt-dir='left']::view-transition-new(root) {
  z-index: 1;
  animation-name: ta-vt-advance;
}

@keyframes ta-vt-dismiss {
  0% {
    transform: translate(0) scale(1);
    border-radius: 0;
    box-shadow: rgba(0, 0, 0, 0) 0 0;
  }
  22% {
    transform: translate(0) scale(0.94);
    border-radius: 28px;
    box-shadow: rgba(0, 0, 0, 0.55) -26px 0 64px -12px;
  }
  88%,
  100% {
    transform: translate(100%) scale(0.94);
    border-radius: 28px;
    box-shadow: rgba(0, 0, 0, 0.55) -26px 0 64px -12px;
  }
}

@keyframes ta-vt-advance {
  0%,
  55% {
    transform: translate(0) scale(0.86);
    border-radius: 48px;
    box-shadow: rgba(0, 0, 0, 0.6) 0 24px 70px -20px;
    filter: brightness(0.62);
  }
  100% {
    transform: translate(0) scale(1);
    border-radius: 0;
    box-shadow: rgba(0, 0, 0, 0) 0 0;
    filter: brightness(1);
  }
}

/* --- The masthead does not travel. ---------------------------------------
   Promoted to its own named group, pinned above everything and given no
   animation at all, with the outgoing snapshot removed outright so there is no
   ghost of it. The nav and the CTA stay perfectly still while the whole page
   body slides underneath them, which is what sells this as one surface rather
   than two documents.

   Naming it unconditionally is safe: `view-transition-name` has no rendering
   effect outside a transition beyond forming a stacking context, and .nav is
   `position: fixed; z-index: 40`, so it forms one already. */
[data-nav] {
  view-transition-name: site-header;
}

:root::view-transition-group(site-header) {
  z-index: 100;
  animation: none;
}

:root::view-transition-old(site-header) {
  display: none;
}

:root::view-transition-new(site-header) {
  animation: none;
}

/* Both snapshots are taken from live pixels, so anything pointer-driven that
   is on screen at capture is frozen into them for the full 1.4s — two stopped
   reticles crossing the viewport in opposite directions. The controller
   sets `data-vt-active` in `pageswap` and `pagereveal`, both of which run
   before their document's snapshot is taken. */
:root[data-vt-active] .reticle,
:root[data-vt-active] .reticle-label,
:root[data-vt-active] .worm,
:root[data-vt-active] .follow {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  /* !important is load-bearing: `:root::view-transition-old(root)` above wins
     on specificity over the universal selector this rule has to use. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: 1ms !important;
    animation-delay: 0s !important;
  }
}

/* ===========================================================================
   THE FOLLOW CARD
   ---------------------------------------------------------------------------
   Built by follow-card.js, which owns position, tilt and the flip. Everything
   here is chrome and geometry.

   The wrapper is a zero-size box parked at the pointer, and it carries the
   perspective. That places the vanishing point exactly on the cursor and the
   card off to one side of it, so the rotation reads as a card being held at
   arm's length rather than a plane being spun in the middle of the screen.
   =========================================================================== */

.follow {
  position: fixed;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  z-index: 50;
  perspective: 480px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 200ms ease-out;
  will-change: transform, opacity;
}

.follow[data-shown='true'] {
  opacity: 1;
}

/* Locked to the pointer with no tilt and no offset. It sits inside the lagging
   wrapper so it lags too — it is the anchor the card is being dragged from,
   not a second cursor. */
.follow__puck {
  position: absolute;
  left: 0;
  top: 0;
  display: grid;
  place-items: center;
  width: 84px;
  height: 84px;
  margin: -42px 0 0 -42px;
  border-radius: 999px;
  border: 1px solid var(--hairline-strong);
  background: rgba(255, 255, 255, 0.06);
  backdrop-filter: blur(6px);
}

.follow__puck span {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--fg);
}

.follow__card {
  position: absolute;
  transform-style: preserve-3d;
  will-change: transform;
}

.follow--card .follow__card {
  left: 56px;
  top: 44px;
  width: 192px;
}

.follow--draft .follow__card {
  left: 28px;
  top: 24px;
  width: 224px;
}

/* A fixed height is what lets both faces stack in the same box. The card is
   two sentences at most by design; anything longer belongs on the page. */
.follow__inner {
  position: relative;
  height: 152px;
  transform-style: preserve-3d;
}

.follow--draft .follow__inner {
  height: 164px;
}

.follow__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 16px;
  overflow: hidden;
  border-radius: 16px;
  background: linear-gradient(180deg, rgba(38, 38, 44, 0.98), rgba(23, 23, 26, 0.98));
  box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.55);
  /* Without this the reverse of the card shows through mirror-imaged for the
     whole 560ms of every flip. */
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.follow--card .follow__face {
  border: 1px solid var(--hairline);
}

.follow--draft .follow__face {
  border: 1px dashed var(--fg-faint);
  background: linear-gradient(180deg, rgba(30, 30, 35, 0.96), rgba(23, 23, 26, 0.96));
}

.follow__face--back {
  transform: rotateY(180deg);
}

.follow__tag {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

.follow__title {
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--fg);
}

.follow__note {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--fg-dim);
}

/* --- Draft chrome. -------------------------------------------------------- */

.follow__status {
  display: flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.05em;
  color: var(--fg-faint);
}

.follow__pulse {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--blue-300);
  animation: ta-draft-pulse 1.5s ease-in-out infinite;
}

.follow__typing {
  display: flex;
  gap: 5px;
  margin-top: auto;
}

.follow__dot {
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: var(--fg-muted);
  animation: ta-draft-dot 1.4s ease-in-out infinite;
}

.follow__dot:nth-child(2) {
  animation-delay: 0.18s;
}

.follow__dot:nth-child(3) {
  animation-delay: 0.36s;
}

@keyframes ta-draft-pulse {
  0%,
  100% {
    opacity: 0.35;
    transform: scale(1);
  }
  50% {
    opacity: 1;
    transform: scale(1.4);
  }
}

@keyframes ta-draft-dot {
  0%,
  70%,
  100% {
    opacity: 0.2;
    transform: translateY(0);
  }
  35% {
    opacity: 1;
    transform: translateY(-2px);
  }
}

/* The card is a pointer affordance and nothing else — no touch device ever
   sees it, and follow-card.js does not mount it under reduced motion. These
   are the belt to that script's braces. */
@media (hover: none), (pointer: coarse) {
  .follow {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .follow {
    display: none;
  }
}

/* =====================================================================
   COST
   The cost pages argue with arithmetic, so they need three things the
   rest of the site never needed: a two-column comparison that survives
   a phone, a calculator, and a ledger heading that carries a number.
   All of it is plain CSS on the existing tokens — no new colours.
   ===================================================================== */

/* The comparison. A table, because it IS tabular data and a screen
   reader should say so. Below 620px the head is hidden and each cell
   carries its own label from data-label, so the rows stack as pairs
   rather than shearing off the side of the phone. */
.ledger {
  width: 100%;
  border-collapse: collapse;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  overflow: hidden;
  font-size: clamp(14px, 1.25vw, 15.5px);
}

.ledger caption {
  caption-side: bottom;
  padding-top: 12px;
  color: var(--fg-faint);
  font-size: 13px;
  line-height: 1.55;
  text-align: left;
}

.ledger th,
.ledger td {
  padding: 13px 16px;
  text-align: left;
  border-bottom: 1px solid var(--hairline);
  vertical-align: top;
}

.ledger thead th {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-muted);
  background: rgba(255, 255, 255, 0.04);
}

.ledger tbody th {
  font-weight: 400;
  color: var(--fg-dim);
}

.ledger td {
  color: var(--fg-muted);
}

.ledger td b,
.ledger__win {
  color: var(--fg);
  font-weight: 500;
}

.ledger tr:last-child th,
.ledger tr:last-child td {
  border-bottom: 0;
}

.ledger__them {
  color: var(--fg-faint);
}

@media (max-width: 620px) {
  /* Block-ifying a table leaves the caption as a table-caption inside a
     non-table, which Chrome renders as a squeezed column beside the rows.
     So the table becomes a column flexbox: the caption is a block like
     everything else, and `order` puts it back underneath the rows where
     caption-side: bottom had it. */
  .ledger {
    display: flex;
    flex-direction: column;
  }

  .ledger caption {
    display: block;
    order: 2;
    padding: 12px 16px 4px;
  }

  .ledger tbody {
    order: 1;
  }

  .ledger tbody,
  .ledger tr,
  .ledger td,
  .ledger th {
    display: block;
  }

  .ledger thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .ledger tr {
    border-bottom: 1px solid var(--hairline);
    padding: 6px 0;
  }

  .ledger tr:last-child {
    border-bottom: 0;
  }

  .ledger tbody th {
    color: var(--fg);
    padding-bottom: 4px;
  }

  .ledger td {
    border-bottom: 0;
    padding: 4px 16px;
    display: flex;
    gap: 10px;
    justify-content: space-between;
  }

  .ledger td::before {
    content: attr(data-label);
    color: var(--fg-faint);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding-top: 2px;
  }
}

/* A ledger section: a number, then the argument. The number is set at
   display size because on this page it IS the headline. */
.cost__ledger {
  display: grid;
  gap: 14px;
  padding: clamp(24px, 3vw, 34px) 0;
  border-top: 1px solid var(--hairline);
}

.cost__num {
  font-family: var(--font-display);
  font-size: clamp(2.1rem, 6vw, 3.4rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--fg);
}

.cost__num small {
  display: block;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin-top: 10px;
}

/* The calculator. Inputs are plain number fields so the phone keyboard
   is numeric and the thing still submits nothing anywhere. */
.calc {
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02));
  padding: clamp(22px, 3vw, 32px);
  display: grid;
  gap: 22px;
}

.calc__fields {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}

.calc__field {
  display: grid;
  gap: 7px;
}

.calc__field label {
  color: var(--fg-dim);
  font-size: 13.5px;
  line-height: 1.45;
}

.calc__input {
  display: flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  background: var(--ink-deep);
  padding: 0 14px;
  transition: border-color var(--dur-hover) var(--ease-std);
}

.calc__input:focus-within {
  border-color: var(--blue-400);
}

.calc__input span {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--fg-faint);
}

.calc__input input {
  flex: 1;
  min-width: 0;
  appearance: none;
  border: 0;
  background: none;
  color: var(--fg);
  font-family: var(--font-mono);
  font-size: clamp(15px, 1.5vw, 17px);
  padding: 12px 0;
}

.calc__input input:focus {
  outline: none;
}

.calc__out {
  border-top: 1px solid var(--hairline);
  padding-top: 20px;
  display: grid;
  gap: 10px;
}

.calc__headline {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 2.2vw, 1.45rem);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.35;
  color: var(--fg);
  margin: 0;
}

.calc__headline b {
  font-weight: 600;
}

.calc__note {
  margin: 0;
  color: var(--fg-faint);
  font-size: 13px;
  line-height: 1.6;
}

/* With no JS the fields are pointless, so they hide and the static
   worked example below them carries the page instead. */
.calc__fallback {
  display: grid;
  gap: 10px;
}

.calc.is-ready .calc__fallback {
  display: none;
}

.calc__live {
  display: none;
}

.calc.is-ready .calc__live {
  display: grid;
  gap: 22px;
}

/* Home: the band that says what the service replaces. */
.replaces {
  display: grid;
  gap: clamp(24px, 3vw, 38px);
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 940px) {
  .replaces {
    grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
    align-items: start;
    gap: clamp(34px, 4vw, 64px);
  }
}

/* Home: the price under each of the four services. */
.costrip {
  display: grid;
  gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  background: var(--hairline);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  overflow: hidden;
  margin-top: clamp(28px, 4vh, 44px);
}

.costrip__item {
  background: var(--ink-deep);
  padding: 20px;
  display: grid;
  gap: 6px;
  align-content: start;
}

.costrip__price {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 2vw, 1.3rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--fg);
}

.costrip__what {
  color: var(--fg-faint);
  font-size: 13px;
  line-height: 1.55;
}
