/* ===== 00-fonts.css ============================================================ */
/* =========================================================================
   Fonts — self-hosted, subset to Latin. Carried over from the previous build.

   Season Mix is a TRIAL build and is NOT licensed for public use. Drop the
   licensed webfonts into static/fonts/ under these exact filenames and
   nothing else in the codebase changes.

   font-display: swap — text paints immediately in the fallback and reflows
   once the face arrives. Never `block`: a blank hero is worse than a reflow.
   ========================================================================= */

@font-face {
  font-family: 'Geist';
  src: url('/fonts/geist-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Geist';
  src: url('/fonts/geist-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Geist';
  src: url('/fonts/geist-600.woff2') format('woff2');
  font-weight: 600; font-style: normal; font-display: swap;
}

@font-face {
  font-family: 'Geist Mono';
  src: url('/fonts/geist-mono-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Geist Mono';
  src: url('/fonts/geist-mono-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}

@font-face {
  font-family: 'Season Mix';
  src: url('/fonts/season-mix-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Season Mix';
  src: url('/fonts/season-mix-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Season Mix';
  src: url('/fonts/season-mix-600.woff2') format('woff2');
  font-weight: 600; font-style: normal; font-display: swap;
}

/* ===== 01-tokens.css ============================================================ */
/* =========================================================================
   Design tokens

   Sampled from the hero mockup (a 4x export of a 1440px design, so every
   measurement below is the mockup pixel value divided by four).
   ========================================================================= */

:root {
  /* --- colour ------------------------------------------------------------ */
  --bg:          #100F0D;   /* page, nav, release bar */
  --surface:     #191919;   /* cards, chips, the input field */
  --node:        #000000;   /* pipeline node pills sit BELOW the page ground */
  --text:        #FFFFFF;
  --text-muted:  #909090;   /* muted copy, resting nav links */
  --accent:      #E47525;
  --accent-ink:  #000000;   /* label on a filled accent surface */
  --ok:          #3CC31F;   /* CONFIRMED */
  --danger:      #FF2625;   /* the failing response line */
  --warn:        #FBBF24;   /* a result that is suspicious, not yet proven */

  /* Hairlines. The mockup draws these as sub-pixel strokes of solid #8E8E8E
     — 0.4px on cards and chips, 0.2px on the nav and release bar. A 1px
     border at the matching alpha renders identically at 1x and survives
     every DPR, which a 0.2px border does not. Flip the two values to
     `0.4px solid #8E8E8E` if you want the literal Figma stroke. */
  --rule:      rgba(142, 142, 142, 0.4);
  --rule-soft: rgba(142, 142, 142, 0.2);

  --hover:     #292524;     /* nav link pill, dropdown row */
  --panel:     #141210;     /* dropdown panel ground */

  /* The release-bar release name. Orange into green, through yellow — the
     ramp measured off the mockup, which is not a straight RGB interpolation. */
  --release-ramp: linear-gradient(90deg, #E47525 0%, #E4DC26 57%, #5CE086 100%);

  /* The same ramp mirrored, so it tiles. Used where the ramp has to travel
     forever rather than sit still: translating a one-way ramp by its own
     width snaps orange back over green once a cycle, and the snap is the
     only thing anyone sees. Mirrored, the ends match and the loop is
     invisible. Stops are the ramp's own, halved — and lifted, because this
     variant is used over the canopy photograph rather than over #100F0D, and
     the ramp's own values sit too close to the greens already in the picture
     to read as a separate thing moving. */
  --release-loop: linear-gradient(90deg,
      #FF8A2E 0%, #F2E63C 28.5%, #6BF39C 50%, #F2E63C 71.5%, #FF8A2E 100%);

  /* --- type -------------------------------------------------------------- */
  --font-serif: 'Season Mix', ui-serif, Georgia, 'Times New Roman', serif;
  --font-sans:  'Geist', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono:  'Geist Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  /* The mockup is drawn oversized. These are the sizes the same layout wants
     at a normal scale — checked against a reference build at 1462px. */
  --h1: clamp(2.25rem, 4.45vw, 4rem);   /* 64px at 1440 */
  --lh-h1: 1.08;
  --h2: clamp(1.75rem, 2.78vw, 2.5rem); /* 40px at 1440 */
  --lh-h2: 1.18;

  --body-lg: 1.125rem;     /* 18px — hero sub-copy */
  --lh-body-lg: 1.56;      /* 28px */
  --body:    0.9375rem;    /* 15px — release bar */
  --body-sm: 0.9375rem;    /* 15px — nav links */

  /* --- motion ------------------------------------------------------------ */
  --ease:      cubic-bezier(0.22, 1, 0.36, 1);
  --ease-soft: cubic-bezier(0.32, 0.72, 0, 1);
  --dur-fast: 150ms;
  --dur:      220ms;
  --dur-slow: 400ms;

  /* --- layout ------------------------------------------------------------ */
  --maxw:      1280px;   /* content column; 80px gutters at 1440 */
  --gutter:    clamp(20px, 5.56vw, 80px);
  --nav-inset: 40px;     /* the nav insets a further 40px, so links sit at 120 */
  --nav-h:     72px;
  --release-h: 60px;
  --radius:    14px;
  --radius-sm: 8px;

  /* --- pipeline board ----------------------------------------------------
     Authored at 1280 wide. Rail A carries Ask; rail B carries the other
     three. Everything else hangs off these. */
  --rail-a:    17px;    /* wire centre, upper */
  --rail-b:    79px;    /* wire centre, lower */
  --node-h:    34px;
  --board-h:   340px;
}

/* ===== 02-base.css ============================================================ */
/* =========================================================================
   Reset + base. Deliberately small.
   ========================================================================= */

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

/* A class that sets `display` outranks the UA's [hidden] rule, so a hidden
   flex or grid element stays laid out — invisible, but still swallowing
   clicks meant for the page behind it. This makes [hidden] mean it. */
[hidden] { display: none !important; }

* { margin: 0; }

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

body {
  min-height: 100svh;
  padding-block-start: var(--nav-h);   /* the nav is fixed */
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

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

/* SVG is deliberately excluded from the max-width rule above: an inline SVG
   whose viewBox is wider than its container (the pipeline's full-bleed wire)
   gets silently squashed by it, which is a very hard bug to see. */
svg { display: block; }

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

button { background: none; border: 0; cursor: pointer; }

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

h1, h2, h3 {
  font-family: var(--font-serif);
  font-weight: 500;
  line-height: var(--lh-head);
  text-wrap: balance;
}

h1 { font-size: var(--h1); line-height: var(--lh-tight); }
h2 { font-size: var(--h2); }
h3 { font-size: var(--h3); }

p { text-wrap: pretty; }

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

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

/* Visible to screen readers, not to the eye. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Honour the OS setting. Every animation on this site must survive this rule
   being applied — nothing may depend on motion to become readable. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ===== 03-nav.css ============================================================ */
/* =========================================================================
   Navigation

   Structure follows Strand: a fixed bar whose inner column is inset a
   further 40px, so links start at 120px on a 1440 viewport while the page
   content column starts at 80. The bar does not change on scroll.
   ========================================================================= */

.nav {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 50;
  height: var(--nav-h);
  background: var(--bg);
  border-bottom: 1px solid var(--rule-soft);
  padding-inline: var(--gutter);
}

.nav__inner {
  height: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--nav-inset);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

.nav__left {
  display: flex;
  align-items: center;
  gap: 32px;
  min-width: 0;
}

/* --- logo ---------------------------------------------------------------- */

.nav__logo {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: none;
}

.nav__mark { width: 40px; height: 26px; }
.nav__word { width: 70px; height: auto; }

/* --- links --------------------------------------------------------------- */

.nav__links {
  display: flex;
  align-items: center;
  gap: 24px;
}

.nav__item { position: relative; }

.nav__link {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: var(--body-sm);
  font-weight: 500;
  line-height: 20px;
  white-space: nowrap;
  transition:
    background-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

.nav__link:hover,
.nav__link[aria-expanded="true"] {
  background: var(--hover);
  color: var(--text);
}

/* The chevron does not rotate — Strand leaves it still, and a rotating
   chevron on a hover-opened menu reads as a click affordance it is not. */
.chev { flex: none; opacity: 0.9; }

/* --- dropdown panel ------------------------------------------------------ */

.nav__panel {
  position: absolute;
  inset-block-start: calc(100% + 6px);
  inset-inline-start: 0;
  z-index: 2;
  display: flex;
  gap: 0;
  padding: 4px;
  border-radius: 10px;
  background: var(--panel);
  border: 1px solid var(--rule-soft);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  opacity: 0;
  transform: translateY(-4px);
  pointer-events: none;   /* never intercept while fading out */
  transition:
    opacity var(--dur) var(--ease),
    transform var(--dur) var(--ease);
}

.nav__panel[data-open] {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

.nav__card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 224px;
  padding: 12px;
  border-radius: var(--radius-sm);
  transition: background-color var(--dur-fast) var(--ease);
}

.nav__card:hover { background: var(--hover); }

.nav__card-icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  color: var(--text-muted);
}

.nav__card-head {
  display: flex;
  align-items: center;
  gap: 8px;
}

.nav__card-title {
  font-size: 14px;
  font-weight: 500;
  line-height: 20px;
  color: var(--text);
  white-space: nowrap;
}

.nav__tag {
  display: inline-flex;
  align-items: center;
  height: 17px;
  padding-inline: 6px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
}

.nav__card-desc {
  font-size: 12px;
  line-height: 16px;
  color: var(--text-muted);
}

/* --- call to action ------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  height: 44px;
  padding-inline: 22px;
  border-radius: 10px;
  font-size: 15px;
  font-weight: 600;
  white-space: nowrap;
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

/* The mockup sets a black label on the accent fill, not white. */
.btn--accent {
  background: var(--accent);
  color: var(--accent-ink);
}

.btn--accent:hover { background: #F0862F; }

.btn--ghost {
  border: 1px solid var(--rule);
  color: var(--text);
}

.btn--ghost:hover { background: var(--surface); }

.nav__cta { flex: none; }

/* =========================================================================
   Mobile nav — burger and drawer

   Breakpoint is 900px, not 768: the desktop bar runs out of room somewhere
   around 880 (three links, two chevrons, a wordmark and a 44px CTA), so the
   switch happens before the collision rather than at a round number.
   ========================================================================= */

.nav__burger { display: none; }
.nav__drawer { display: none; }

@media (max-width: 900px) {

  /* The desktop link row and its hover panels go away entirely. Hiding
     rather than restyling: the panels are pointer-driven and there is no
     pointer here. */
  .nav__links { display: none; }

  .nav__burger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 44px is the minimum comfortable touch target. */
    width: 44px;
    height: 44px;
    margin-inline-end: -10px;
    background: none;
    border: 0;
    color: var(--text);
    cursor: pointer;
  }

  .nav__burger-box {
    position: relative;
    display: block;
    width: 20px;
    height: 12px;
  }

  .nav__burger-box span {
    position: absolute;
    inset-inline: 0;
    height: 1.5px;
    background: currentColor;
    border-radius: 2px;
    transition:
      transform 260ms var(--ease),
      opacity 160ms var(--ease);
  }

  .nav__burger-box span:first-child { inset-block-start: 0; }
  .nav__burger-box span:last-child  { inset-block-end: 0; }

  /* Open: the two rules meet in the middle and cross. */
  [data-nav-burger][aria-expanded="true"] .nav__burger-box span:first-child {
    transform: translateY(5.25px) rotate(45deg);
  }
  [data-nav-burger][aria-expanded="true"] .nav__burger-box span:last-child {
    transform: translateY(-5.25px) rotate(-45deg);
  }

  /* The CTA leaves the bar — it is the last thing in the drawer instead, at
     full width, where it is a bigger target than it ever was up here. */
  .nav__cta { display: none; }

  .nav__inner { gap: 12px; }

  /* --- the drawer ------------------------------------------------------ */

  .nav__drawer {
    display: block;
    position: fixed;
    inset-block-start: var(--nav-h);
    inset-block-end: 0;
    inset-inline: 0;
    z-index: 49;
    background: var(--bg);
    /* Opacity and a small lift rather than a slide from the edge: the bar
       stays put, so the drawer reads as the bar's own content unfolding. */
    opacity: 0;
    transform: translateY(-6px);
    transition:
      opacity 200ms var(--ease),
      transform 260ms var(--ease);
  }

  .nav__drawer[data-open] {
    opacity: 1;
    transform: none;
  }

  .nav__drawer-scroll {
    height: 100%;
    overflow-y: auto;
    /* Every child keeps its own height. This is a flex column, so the default
       flex-shrink: 1 means that as soon as the content is taller than the
       screen — which it is on any phone — the items get squashed to fit
       instead of scrolling. The CTA is last, so it took the worst of it and
       rendered 23px tall instead of 48. */
    -webkit-overflow-scrolling: touch;
    padding: 24px var(--gutter) calc(32px + env(safe-area-inset-bottom));
    display: flex;
    flex-direction: column;
  }

  .nav__drawer-scroll > * { flex: none; }

  .nav__group {
    margin-block: 20px 10px;
    font-family: var(--font-mono);
    font-size: var(--mono-sm, 11px);
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .nav__group:first-child { margin-block-start: 4px; }

  .nav__d-link {
    display: block;
    padding-block: 14px;
    border-block-end: 1px solid var(--rule-soft);
    font-size: 19px;
    font-weight: 500;
    color: var(--text);
  }

  .nav__d-link span {
    display: block;
    margin-block-start: 3px;
    font-size: 14px;
    font-weight: 400;
    color: var(--text-muted);
  }

  .nav__d-link:active { color: var(--accent); }

  /* Sized to its label and aligned to the start, like every other button on
     the site. Full width made it a slab: at 342px wide and 50px tall the
     10px radius reads as a pill, and nothing else on the page is shaped like
     that. Left-aligned so it sits on the same line as the links above it. */
  .nav__d-cta {
    flex: none;
    align-self: stretch;
    margin-block-start: 28px;
    width: 100%;
    height: 52px;
    border-radius: 10px;
    font-size: 16px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .nav__drawer,
  .nav__burger-box span { transition: none; }
}

/* ===== 04-release.css ============================================================ */
/* =========================================================================
   Release bar — the strip directly under the nav
   ========================================================================= */

.release {
  border-bottom: 1px solid var(--rule-soft);
  padding-inline: var(--gutter);
}

.release__inner {
  height: var(--release-h);
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--nav-inset);
  display: flex;
  align-items: center;
  gap: 12px;
}

.chip {
  display: inline-flex;
  align-items: center;
  height: 27px;
  padding-inline: 11px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text);
  flex: none;
}

.release__dot { color: var(--text-muted); flex: none; }

.release__text {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  font-size: var(--body);
  color: var(--text-muted);
}

/* Only the sentence truncates. The link sits outside the clipping box so the
   arrow can travel on hover without being cut off at the right edge. */
.release__sentence {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Orange into green through yellow. The gradient is clipped to the glyphs,
   so it needs a solid colour underneath for any engine that cannot clip. */
.release__name {
  color: var(--accent);
  font-weight: 500;
  background: var(--release-ramp);
  -webkit-background-clip: text;
  background-clip: text;
}

@supports (-webkit-background-clip: text) or (background-clip: text) {
  .release__name { color: transparent; }
}

.release__link {
  flex: none;
  display: inline-flex;
  align-items: baseline;
  gap: 12px;
  padding-inline-end: 6px;   /* travel room for the arrow */
  color: var(--text);
  font-weight: 500;
  white-space: nowrap;
}

.release__arrow {
  color: var(--accent);
  display: inline-block;
  transition: transform var(--dur) var(--ease);
}

.release__link:hover .release__arrow { transform: translateX(4px); }

/* =========================================================================
   Phones

   At 390px the sentence has about forty pixels to work with and truncates to
   "Hu…", which is worse than absent — it reads as a rendering fault. Below
   700px the bar keeps only the parts that survive: the chip, the release
   name, and the link.
   ========================================================================= */

@media (max-width: 699px) {
  .release__inner { padding-inline: 0; gap: 10px; }

  /* The name is short enough to always fit and is the one word that carries
     information here; the rest of the sentence goes. */
  .release__sentence { display: none; }

  .release__text::before {
    content: "Hummingbird";
    color: var(--accent);
    font-weight: 500;
    white-space: nowrap;
  }

  .release__dot { display: none; }
  .release__link { font-size: var(--body); }
}

/* ===== 05-hero.css ============================================================ */
/* =========================================================================
   Hero — the copy block and the media panel beneath the pipeline
   ========================================================================= */

.hero {
  padding-block-start: 48px;   /* release-bar rule to the top of the Ask pill */
  padding-block-end: 96px;
}

/* Gutter and column width must sit on DIFFERENT boxes, or border-box sizing
   subtracts the gutter from the column and the content insets twice. */
.band { padding-inline: var(--gutter); }

.hero__lower {
  max-width: var(--maxw);
  margin-inline: auto;
  margin-block-start: 44px;   /* pipeline board to the top of the media panel */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 40px;
}

.hero__copy { max-width: 660px; }

.hero__title {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: var(--h1);
  line-height: var(--lh-h1);
  letter-spacing: -0.005em;
  max-width: 11ch;
}

.hero__sub {
  margin-block-start: 24px;
  font-size: var(--body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-muted);
  max-width: 545px;
}

.hero__actions {
  margin-block-start: 32px;
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}

@media (max-width: 1100px) {
  .hero__lower { flex-direction: column; align-items: flex-start; gap: 48px; }
}

/* ===== 06-pipeline.css ============================================================ */
/* =========================================================================
   The hero pipeline

   Authored at the 1280px content width and scaled down to fit, so the wire,
   the pills and the cards keep the same relationship at every viewport.
   The wire deliberately overflows the board on both sides — it is
   full-bleed — and .pipe clips it at the viewport edge.
   ========================================================================= */

.pipe {
  position: relative;
  overflow-x: clip;
  padding-inline: var(--gutter);
}

.pipe__frame {
  max-width: var(--maxw);
  margin-inline: auto;
  position: relative;
}

.pipe__board {
  position: relative;
  width: 1280px;
  height: var(--board-h);
  transform-origin: top left;
}

/* --- the wire ------------------------------------------------------------ */

.pipe__wire {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: -1000px;
  width: 3280px;
  height: var(--board-h);
  overflow: visible;
  pointer-events: none;
}

.wire {
  fill: none;
  stroke-width: 2;
  stroke-linecap: butt;
}

/* The route is cold-drawn from the start: the accent only ever travels along
   something the viewer can already see, which is what makes the run read as
   a signal moving through a circuit rather than a line being invented. */
.wire--track { stroke: rgba(142, 142, 142, 0.34); }

.wire--live { stroke: var(--accent); }

/* A blurred copy of the same stroke, drawn to the same length, so the live
   run carries light rather than being a flat line on a dark ground. */
.wire--halo {
  stroke: var(--accent);
  stroke-width: 5;
  opacity: 0.4;
}

.wire__head {
  fill: #FFD9B8;
  opacity: 0;
  transition: opacity 160ms linear;
}

.wire__bloom {
  fill: var(--accent);
  opacity: 0;
  transition: opacity 160ms linear;
}

.pipe__wire[data-head] .wire__head { opacity: 1; }
.pipe__wire[data-head] .wire__bloom { opacity: 0.85; }

/* --- node pills ---------------------------------------------------------- */

.node {
  position: absolute;
  inset-inline-start: var(--x);
  inset-block-start: var(--y);
  height: var(--node-h);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  padding-inline: 18px;
  border-radius: 999px;
  /* The pills sit BELOW the page ground, not above it — black on #100F0D is
     what gives them their weight.

     The warm interior is not a light of its own: it is the wire blooming
     through the pill, which is why it is a horizontal band at the pill's
     centre, brightest where the wire enters and leaves. Modelling it that
     way is what makes a pill read as sitting ON the wire, not beside it. */
  background:
    radial-gradient(56% 48% at 1% 50%, rgba(230, 124, 48, 0.26), transparent 72%),
    radial-gradient(56% 48% at 99% 50%, rgba(230, 124, 48, 0.22), transparent 72%),
    linear-gradient(180deg,
      transparent 26%,
      rgba(230, 124, 48, 0.075) 46%,
      rgba(230, 124, 48, 0.085) 56%,
      transparent 76%),
    var(--node);
  border: 1px solid rgba(178, 178, 178, 0.42);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.005em;
  opacity: 0.45;
  transition: opacity 340ms var(--ease);
}

.node--mono { font-family: var(--font-mono); font-weight: 500; font-size: 13.5px; }

.node[data-on] { opacity: 1; }

/* The moment the wire lands: one short ring pushed out from the pill edge,
   plus a small settle. Fires once, on arrival — never on a loop. */
.node[data-ignite] { animation: ignite 620ms var(--ease); }

@keyframes ignite {
  0%   { transform: scale(1);     box-shadow: 0 0 0 0 rgba(228, 117, 37, 0.55); }
  35%  { transform: scale(1.045); box-shadow: 0 0 0 6px rgba(228, 117, 37, 0.14); }
  100% { transform: scale(1);     box-shadow: 0 0 0 12px rgba(228, 117, 37, 0); }
}

/* --- shared entrance ----------------------------------------------------- */

.ask, .plan__step, .card, .found {
  opacity: 0;
  transform: translateY(7px);
  transition:
    opacity 280ms var(--ease),
    transform 280ms var(--ease);
}

.ask[data-on], .plan__step[data-on], .card[data-on], .found[data-on] {
  opacity: 1;
  transform: none;
}

/* --- the ask box --------------------------------------------------------- */

.ask {
  position: absolute;
  inset-inline-start: var(--x);
  inset-block-start: var(--y);
  width: 240px;
  padding: 11px 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 16px;
}

.ask__text {
  flex: 1 1 auto;
  font-size: 15px;
  line-height: 21px;
  color: var(--text);
  min-width: 0;
}

.ask__caret {
  display: inline-block;
  width: 2px;
  height: 16px;
  margin-inline-start: 2px;
  vertical-align: -2px;
  background: var(--accent);
  opacity: 0;
}

.ask[data-typing] .ask__caret { animation: caret 1.06s steps(1, end) infinite; }

@keyframes caret { 0%, 55% { opacity: 1 } 56%, 100% { opacity: 0 } }

.ask__send {
  flex: none;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border-radius: 7px;
  background: var(--accent);
  color: #000;
  transition: transform 130ms var(--ease), filter 130ms var(--ease);
}

.ask__send[data-press] { transform: scale(0.84); filter: brightness(1.2); }

/* --- the plan list ------------------------------------------------------- */

.plan {
  position: absolute;
  inset-inline-start: var(--x);
  inset-block-start: var(--y);
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

.plan__step {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  height: 32px;
  padding-inline: 13px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--rule);
  font-size: 13.5px;
  color: var(--text-muted);
  white-space: nowrap;
  transition:
    opacity 280ms var(--ease),
    transform 280ms var(--ease),
    color 280ms var(--ease),
    border-color 280ms var(--ease);
}

.plan__step[data-state="done"] {
  color: var(--text);
  border-color: rgba(228, 117, 37, 0.32);
}

.plan__mark {
  position: relative;
  flex: none;
  width: 15px;
  height: 15px;
}

.plan__ring, .plan__check {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.plan__ring {
  stroke: var(--accent);
  stroke-width: 2;
  opacity: 0.34;
}

/* Loading spins a gap in the ring rather than swapping in a spinner glyph,
   so the mark never changes size or jumps position between states. */
.plan__step[data-state="loading"] .plan__ring {
  opacity: 1;
  stroke-dasharray: 13 35;
  transform-origin: 50% 50%;
  animation: spin 700ms linear infinite;
}

.plan__step[data-state="done"] .plan__ring { opacity: 0; }

@keyframes spin { to { transform: rotate(360deg) } }

.plan__check {
  stroke: var(--accent);
  stroke-width: 2.2;
  stroke-dasharray: 22;
  stroke-dashoffset: 22;
}

/* The tick is drawn, not popped — it reads as the step completing. */
.plan__step[data-state="done"] .plan__check {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 260ms var(--ease) 40ms;
}

/* --- the run card -------------------------------------------------------- */

.card {
  position: absolute;
  inset-inline-start: var(--x);
  inset-block-start: var(--y);
  width: var(--w);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

.run__row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 13px 16px;
  opacity: 0;
  /* Each observation wipes in from the left, the direction the run is
     travelling — so the card fills the way the wire moves. */
  clip-path: inset(0 100% 0 0);
}

.run__row[data-on] {
  opacity: 1;
  clip-path: inset(0 0 0 0);
  transition:
    clip-path 420ms var(--ease),
    opacity 180ms linear;
}

.run__row + .run__row { border-top: 1px dashed var(--rule); }

.run__label {
  font-size: 12.5px;
  line-height: 16px;
  color: var(--text-muted);
}

.run__value {
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 16px;
  color: var(--text);
}

.run__value--bad { color: var(--danger); }

/* --- the found card ------------------------------------------------------ */

/* The halftone photograph is the card's frame: it shows only as a band around
   an inset panel, so it is a background, not content. */
.found {
  position: absolute;
  inset-inline-start: var(--x);
  inset-block-start: var(--y);
  width: var(--w);
  padding: 9px;
  border-radius: 16px;
  background: #EFE7CF url("/img/halftone.jpg") center / cover no-repeat;
}

.found[data-on] { transform: none; }

.found__inner {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: 15px 14px 13px;
  border-radius: 12px;
  background: var(--surface);
}

.found__art {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: 56%;
  z-index: -1;
  width: 136%;
  height: auto;
  transform: translate(-50%, -50%);
  opacity: 0.92;
  pointer-events: none;
  animation: drift 26s ease-in-out infinite;
}

/* Barely-there movement. Enough that the card is alive when it is the only
   thing left on screen, not enough to read as an animation. */
@keyframes drift {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  50%      { transform: translate(-52%, -50%) scale(1.035); }
}

.found__status {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 15px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ok);
}

.found[data-on] .found__status { animation: confirm 900ms var(--ease) 120ms; }

@keyframes confirm {
  0%   { text-shadow: 0 0 0 rgba(60, 195, 31, 0); }
  30%  { text-shadow: 0 0 12px rgba(60, 195, 31, 0.75); }
  100% { text-shadow: 0 0 0 rgba(60, 195, 31, 0); }
}

.found__claim {
  margin-block-start: 8px;
  font-size: 15px;
  font-weight: 600;
  line-height: 19px;
  color: var(--text);
}

.found__where {
  margin-block-start: 48px;
  font-size: 12.5px;
  line-height: 16px;
  color: var(--text-muted);
}

.found__evidence {
  margin-block-start: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 26px;
  border: 1px solid var(--accent);
  border-radius: 7px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--accent);
}

/* One pass of light across the card as the finding lands. */
.found__sheen {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent 38%,
    rgba(255, 255, 255, 0.11) 50%,
    transparent 62%);
  transform: translateX(-120%);
}

.found[data-on] .found__sheen { animation: sheen 1100ms var(--ease) 160ms; }

@keyframes sheen {
  to { transform: translateX(120%); }
}

@media (prefers-reduced-motion: reduce) {
  .found__art, .found__sheen, .found__status, .node { animation: none !important; }
}

/* =========================================================================
   Phones

   The board is authored at 1280 and scaled to fit, so at 390px it renders at
   0.30 — the pills come out about six pixels tall and the labels are not
   readable at any distance. It is decorative-plus-explanatory, and an
   unreadable explanation is worse than none, so it comes off below 700px.
   iPad portrait (768) scales to 0.6, which still reads, and keeps it.
   ========================================================================= */

@media (max-width: 699px) {
  .pipe { display: none; }
}

/* ===== 07-aviary.css ============================================================ */
/* =========================================================================
   The aviary — the filmed hero, and the switch that compares it to the crest

   Three things can occupy the media panel to the right of the headline, and
   only one is ever mounted:

     filaments   the crest canvas (08-crest.css) — the incumbent
     graded      the supplied footage pulled into the palette, vignetted into
                 the ground, with the hummingbird flying over it
     bird        the same hummingbird on the page ground, nothing behind it

   The mode lives in `data-hero` on `.crest__frame` and is cycled by
   13-hero-mode.js.

   ---------------------------------------------------------------------------
   WHY THIS FILE IS SO SMALL. Read before adding anything to it.
   ---------------------------------------------------------------------------

   The first version of this did the compositing in the browser: a background
   video, a scrim, and a separately-keyed hummingbird with an alpha channel
   stacked over it, with the colour grade as a five-function `filter` chain
   and the vignette as a `mask-image`, both on a surface whose pixels change
   thirty times a second. It ran at roughly half frame rate and felt broken.

   Measured, parked on the hero at 1440x900:

     | mode                    | fps  |
     |-------------------------|------|
     | either video alone      | 60   |
     | both video layers       | 31.7 |

   That is a cliff, not a slope, and it is the finding worth keeping: **two
   overlapping video layers cost more than twice one.** The compositor stops
   treating them as cheap surfaces once they overlap and have to be blended
   together. It was not the filters alone and it was not the alpha alone,
   though `drop-shadow` on an alpha video was expensive on its own: it blurs
   the alpha channel every decoded frame, and removing two of them took the
   bird-only mode from 41.5fps to 60.

   So none of it happens at run time any more. ffmpeg grades the footage,
   burns in the vignette and flies the bird over it, and ships one opaque
   file per mode. What is left here is `display` and `object-fit`.

   Three things follow from that, and they are all load-bearing:

     - **Never put a `filter`, a `mask-image` or a `backdrop-filter` on these
       elements.** A filter on a still image is paid once; a filter on a video
       is paid on every frame, forever. This is the lens mistake from the
       performance section wearing a different hat.
     - **Never mount two of them at once.** 13-hero-mode.js pauses and
       unmounts every layer but the current one, and that is not tidiness.
     - **To change how the hero looks, re-encode the file.** The grade lives in
       the ffmpeg invocation recorded in HANDOFF.md, not in this stylesheet.

   The files carry no alpha channel, which is the other prize: alpha video has
   no format that both Safari and Chrome can read, and compositing on the bird
   at build time made that whole problem disappear. `bird` mode is an opaque
   rectangle of the page ground with a bird on it, sitting on the page ground,
   which is invisible for as long as those two colours agree. If `--bg` ever
   changes, re-encode; nothing will warn you.
   ========================================================================= */

.aviary {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.aviary__layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: none;
  /* The files are cut to the panel's own ratio, so `cover` crops a fraction
     of a percent. It is here for the case where the panel is resized and the
     files are not re-cut. */
  object-fit: cover;
}

/* --- mounting ------------------------------------------------------------ */

/* Default: the crest, exactly as it was.

   `display: none` on the canvas and not `opacity: 0`, because the crest stops
   its own loop through an IntersectionObserver and taking it out of the box
   tree is what tells it to stop. Hiding it any other way leaves it animating
   forever behind a video. */
.crest__frame .aviary { display: none; }
.crest__frame .crest  { display: block; }

[data-hero="graded"] .aviary,
[data-hero="bird"]   .aviary { display: block; }

[data-hero="graded"] .crest,
[data-hero="bird"]   .crest  { display: none; }

[data-hero="graded"] .aviary__layer[data-aviary="graded"] { display: block; }
[data-hero="bird"]   .aviary__layer[data-aviary="bird"]   { display: block; }

/* The mode picker that used to sit here is gone: the argument is settled and
   `graded` won. `?hero=<mode>` still pins a mode for a side-by-side look, and
   13-hero-mode.js still pauses whatever is not mounted. To retire the losing
   modes for good, delete their entries from MOUNTS there and their files from
   static/video.
   ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .aviary__layer { transition: none; }
}

/* ===== 08-crest.css ============================================================ */
/* =========================================================================
   The crest — the hummingbird as a combed filament field
   ========================================================================= */

.crest {
  display: block;
  width: 100%;
  height: 100%;
  /* The hairs are the artwork; there is no panel behind them. */
  background: transparent;
  cursor: crosshair;
  touch-action: pan-y;
}

.crest__frame {
  flex: none;
  width: 560px;
  height: 430px;
  position: relative;
}

/* Anyone who has asked for less motion gets the field drawn once and left
   alone: the bird is still legible, it simply does not respond. */
@media (prefers-reduced-motion: reduce) {
  .crest { cursor: default; }
}

@media (max-width: 1100px) {
  .crest__frame { width: 100%; height: auto; aspect-ratio: 1 / 1; }
}

/* ===== 09-gap.css ============================================================ */
/* =========================================================================
   01 — The Gap

   The two arcs enclose the argument: a lens drawn around the prose. They are
   positioned against the section rather than the text so the geometry holds
   its shape while the copy inside it reflows.
   ========================================================================= */

.gap {
  position: relative;
  padding-block: clamp(96px, 11vw, 168px) clamp(96px, 11vw, 160px);
  padding-inline: var(--gutter);
  overflow: clip;
}

.gap__inner { position: relative; z-index: 1; text-align: center; }

/* --- the geometry -------------------------------------------------------- */

.gap__lens {
  position: absolute;
  inset-block: clamp(96px, 11vw, 168px) clamp(96px, 11vw, 160px);
  inset-inline-start: 50%;
  width: min(100% - var(--gutter) * 2, var(--maxw));
  transform: translateX(-50%);
  pointer-events: none;
}

.gap__arc { position: absolute; inset-inline: 0; height: 330px; }
.gap__arc--top { inset-block-start: 26px; }
.gap__arc--bottom { inset-block-end: 296px; transform: scaleY(-1); }

.arc { width: 100%; height: 100%; overflow: visible; }

.arc__curve, .arc__tick {
  stroke: rgba(142, 142, 142, 0.30);
  /* No vector-effect here. With non-scaling-stroke the browser measures the
     dash pattern in screen space while getTotalLength() reports user space;
     on a stretched viewBox the two disagree and the tail of the curve never
     draws. Slightly uneven stroke weight is the cheaper price. */
  stroke-width: 1.15;
}

.arc__bead { fill: rgba(142, 142, 142, 0.55); }

/* Drawn state. The dash values are set by the script from the real path
   length, so the curve draws at a true constant rate. */
[data-gap] .arc__curve,
[data-gap] .arc__tick { stroke-dashoffset: var(--len, 2000); }

[data-gap] .arc__bead { opacity: 0; transform: scale(0.2); transform-origin: center; }

[data-gap][data-shown] .arc__curve {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 1700ms var(--ease-soft) 120ms;
}

[data-gap][data-shown] .arc__tick {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 700ms var(--ease) 900ms;
}

[data-gap][data-shown] .arc__bead {
  opacity: 1;
  transform: scale(1);
  transition: opacity 420ms var(--ease), transform 620ms cubic-bezier(0.2, 1.5, 0.35, 1);
}

[data-gap][data-shown] .arc__curve + .arc__tick + .arc__bead { transition-delay: 1250ms; }
[data-gap][data-shown] .arc__bead--apex { transition-delay: 1500ms; }

/* --- eyebrow ------------------------------------------------------------- */

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--mono-lg, 13px);
  letter-spacing: 0.10em;
  color: var(--text);
}

.eyebrow__text {
  display: inline-block;
  /* Opens from the centre out, which is where the plumb line has just
     arrived — the label appears to be drawn by it. */
  clip-path: inset(0 50% 0 50%);
  opacity: 0;
  filter: blur(5px);
}

.eyebrow__n { color: var(--accent); }

[data-gap][data-shown] .eyebrow__text {
  clip-path: inset(0 -4% 0 -4%);
  opacity: 1;
  filter: blur(0);
  /* Opens as the plumb line arrives, so the line appears to write it. */
  transition:
    clip-path 900ms var(--ease-soft) 980ms,
    opacity 620ms var(--ease) 980ms,
    filter 780ms var(--ease) 980ms;
}

/* --- headings ------------------------------------------------------------ */

.gap__head {
  margin-block-start: 34px;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: var(--h2);
  line-height: var(--lh-h2);
  letter-spacing: -0.004em;
  text-wrap: balance;
}

.gap__head--turn { margin-block-start: clamp(180px, 19.5vw, 276px); }

.gap__head--turn em {
  font-style: normal;
  color: var(--accent);
}

.gap__sub {
  margin-block: 20px 0;
  margin-inline: auto;
  max-width: 640px;
  font-size: var(--body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-muted);
}

.gap__sub strong { color: var(--text); font-weight: 600; }

[data-lift] {
  opacity: 0;
  transform: translateY(16px);
}

[data-gap][data-shown] [data-lift] {
  opacity: 1;
  transform: none;
  transition: opacity 780ms var(--ease), transform 900ms var(--ease-soft);
}

[data-gap][data-shown] .gap__head { transition-delay: 1320ms; }
[data-gap][data-shown] .gap__head--turn,
[data-gap][data-shown] .gap__sub { transition-delay: 0ms; }

/* --- prose --------------------------------------------------------------- */

.gap__prose {
  margin-block-start: clamp(72px, 8vw, 118px);
  margin-inline: auto;
  max-width: 940px;
  display: flex;
  flex-direction: column;
  gap: 26px;
}

.gap__prose p {
  /* A step above --body-lg: this is the section's argument, and there was
     dead space below it to spend. */
  font-size: clamp(1.0625rem, 1.46vw, 1.3125rem);   /* 21px at 1440 */
  line-height: 1.58;
  color: var(--text-muted);
  text-wrap: pretty;
}

/* A word is read or unread — nothing in between. The transition is short so
   each one lands as its own beat rather than smearing into its neighbours. */
.wd { color: var(--text-muted); transition: color 170ms var(--ease); }
.wd[data-read] { color: #FFFFFF; }

/* --- the chat ------------------------------------------------------------ */

.chat {
  margin-block-start: clamp(48px, 6vw, 78px);
  margin-inline: auto;
  max-width: 660px;
  display: grid;
  gap: 12px;
  text-align: start;
  transition: opacity 640ms var(--ease-soft);
}

.chat[data-fading] { opacity: 0; }

.chat__card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 18px 20px;
}

.chat__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.chat__q {
  font-size: var(--body);
  line-height: 1.6;
  color: var(--text);
  min-height: 1.6em;
}

.chat__pill {
  flex: none;
  padding: 4px 10px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  letter-spacing: var(--mono-track);
  color: var(--text-muted);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 260ms var(--ease-soft), color 260ms var(--ease-soft);
}

.chat__pill:not(:empty) { opacity: 1; }

/* While it is thinking the pill runs the same four hues the characters flash
   through — teal, green, gold, magenta — so the chip and the transcript read
   as one system. It stops the moment there is an answer. */
.chat__pill[data-live] {
  color: transparent;
  background-image: linear-gradient(100deg,
    var(--text) 0%,
    var(--text) 20%,
    hsl(174 82% 66%) 34%,
    hsl(142 82% 66%) 44%,
    hsl(43 82% 66%) 54%,
    hsl(318 82% 66%) 64%,
    var(--text) 80%,
    var(--text) 100%);
  background-size: 210% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  animation: pill-shine 1500ms linear infinite;
}

@keyframes pill-shine {
  from { background-position: 130% 0; }
  to   { background-position: -130% 0; }
}

/* The reply card keeps its full height from the first frame. Collapsing it
   would animate the page height and shove the copy above out of view, which
   is the one thing a demo must never do while someone is reading. */
.chat__card--reply { opacity: 0; transition: opacity 380ms var(--ease-soft); }
.chat[data-replying] .chat__card--reply { opacity: 1; }

.chat__lines {
  display: grid;
  gap: 9px;
  font-family: var(--font-mono);
  font-size: var(--mono-lg, 13px);
  line-height: 1.5;
  color: var(--text-muted);
}

.chat__lines b { font-weight: 400; color: var(--text); }
.chat__hl { color: var(--accent-text, #E89158); }
.chat__warn { color: var(--warn); }
.chat__ok { color: var(--ok); }

.chat__lines > p { opacity: 0; }
.chat__lines > p[data-on] { opacity: 1; }

/* Each character fades up while flashing through an iridescent hue, then
   settles to whatever colour its line is. The stagger is short enough that
   the tail of the wave is still colouring while the head has moved on —
   that overlap is what makes it read as a sweep and not a typewriter. */
.chat__lines .ch {
  opacity: 0;
  color: var(--flash);
  transition:
    opacity 220ms var(--ease-soft),
    color 520ms var(--ease-soft);
}

.chat__lines .ch[data-on] { opacity: 1; color: inherit; }

.chat__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  margin-block-start: 4px;
}

.chat__chip {
  padding: 7px 12px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  color: var(--text-muted);
}

@media (prefers-reduced-motion: reduce) {
  [data-lift], .eyebrow__text,
  .chat, .chat__card--reply, .chat__lines > p, .chat__lines .ch {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
  }
  [data-gap] .arc__curve, [data-gap] .arc__tick { stroke-dashoffset: 0 !important; }
  [data-gap] .arc__bead { opacity: 1 !important; transform: none !important; }
  .chat__lines .ch { color: inherit !important; }
  .chat__pill[data-live] { animation: none !important; color: var(--text) !important; }
}

@media (max-width: 720px) {
  .gap__lens { display: none; }
  .gap__head--turn { margin-block-start: clamp(80px, 16vw, 140px); }
}

/* ===== 10-seam.css ============================================================ */
/* =========================================================================
   02 — Two Halves

   One painting, cut by a black band. The band is where the two sides of the
   product meet, so the seam the strands converge on sits on the band's
   vertical centre and the headline is split across it — "Two Halves." above
   the line, "No Referee." below. Nothing else in the section is centred on
   anything else.
   ========================================================================= */

.seam {
  position: relative;
  /* The canopy runs to both edges. This section is deliberately outside the
     1280 shell — it is a full-bleed plate, not a column of content. */
  isolation: isolate;
}

/* --- the canopy ---------------------------------------------------------- */

.seam__sky,
.seam__ground {
  background-image: url("/img/canopy.jpg");
  background-size: cover;
  background-repeat: no-repeat;
}

/* Measured off the comp at 1440: a 202px strip, a 708px band. Both scale
   with the viewport so the proportion survives, which is the only thing
   that actually reads. */
.seam__sky {
  height: clamp(96px, 14.03vw, 202px);
  background-position: center top;
}

.seam__ground {
  position: relative;
  min-height: clamp(400px, 44vw, 634px);
  padding-block: clamp(64px, 8vw, 116px);
  padding-inline: var(--gutter);
  /* Framed so the wordmark lands on the teal-to-green passage rather than
     the bright yellow one, which is the only part of this painting that a
     yellow-to-green ramp cannot be read against. */
  background-position: center 52%;
  display: grid;
  place-items: center;
}

/* The canopy is a painting, and a yellow-green ramp cannot be read against
   the yellow-green in it. The first attempt put an ellipse of page colour
   under the type, which reads as a black smudge pasted onto a photograph.
   This instead darkens the whole width evenly — edge to edge, so there is no
   shape to notice — and lets the wordmark carry its own light. */
.seam__ground::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    rgba(16, 15, 13, 0) 0%,
    rgba(16, 15, 13, 0.30) 26%,
    rgba(16, 15, 13, 0.42) 50%,
    rgba(16, 15, 13, 0.30) 74%,
    rgba(16, 15, 13, 0) 100%);
  pointer-events: none;
}

.intro { position: relative; z-index: 1; }

/* --- the band ------------------------------------------------------------ */

.seam__band {
  position: relative;
  height: clamp(460px, 49.17vw, 708px);
  background: var(--bg);
  overflow: clip;
}

.seam__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* --- the copy ------------------------------------------------------------ */

/* Both blocks are anchored to the band's centre — the seam — rather than to
   each other, so the gap between them lands on the line of light at every
   width. Nothing sits across the seam: the core is a white blowout roughly
   eighty pixels tall and any glyph inside that is simply lost. */
.seam__head,
.seam__sub {
  position: absolute;
  inset-inline: 0;
  top: 50%;
  margin-inline: auto;
  padding-inline: var(--gutter);
  text-align: center;
}

/* One sentence per line — see the note in the partial. */
.seam__sub span { display: block; }

.seam__head {
  /* Bottom-anchored ABOVE the seam, not centred on it.

     It used to straddle the line of light, with the light passing through the
     gap between the two lines. That worked while the middle was only light —
     it stopped working the moment the middle became an object. The node graph
     is about 150px tall and the gap between two lines of 74px type is about
     twelve, so every node landed on a glyph.

     Head above, sub below, graph in the corridor between them. Same
     arrangement the phone already uses. */
  inset-block-start: auto;
  inset-block-end: 50%;
  margin-block-end: clamp(76px, 7vw, 104px);
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: calc(var(--h1) * 1.15);
  /* Loose enough that the bundle has somewhere to pass. */
  line-height: 1.16;
  letter-spacing: -0.014em;
  color: var(--text);
  /* Anchored by its bottom edge, so growing the size pushes it up and away
     from the core rather than into it. */
  /* Tight only. A wide soft shadow at this size stops reading as a halo and
     starts reading as a dark panel behind the words — the headline sits on
     near-black already and does not need the reach. */
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
}

.seam__line { display: block; }

.seam__sub {
  /* Hangs below the seam by the same margin the headline sits above it, so
     the corridor the graph occupies is symmetrical. */
  margin-block-start: clamp(88px, 8.33vw, 126px);
  /* Narrow enough that neither line reaches the strands. The measure is set
     by the longer sentence: it has to stay on one line, so the box is sized
     to it rather than to a round number. */
  max-width: calc(596px + var(--gutter) * 2);
  font-size: calc(var(--body-lg) * 1.15);
  line-height: var(--lh-body-lg, 1.56);
  /* Full white with its own halo. At 86% the first and last words of each
     line were washing out, because that is exactly where the strands are
     still wide and bright — the middle of the line sits over black, the ends
     do not. */
  color: var(--text);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.66), 0 0 18px rgba(0, 0, 0, 0.6);
}

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

/* The headline arrives from the seam outward: the top line lifts, the bottom
   line drops, both from the line of light that is already there. */
[data-seam-line] {
  opacity: 0;
  filter: blur(9px);
}

.seam__line:first-child { transform: translateY(26px); }
.seam__line:last-child  { transform: translateY(-26px); }

[data-seam-sub] {
  opacity: 0;
  transform: translateY(14px);
}

[data-seam][data-shown] .seam__line,
[data-seam][data-shown] .seam__sub {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition:
    opacity 900ms var(--ease),
    transform 1100ms var(--ease-soft),
    filter 900ms var(--ease);
}

[data-seam][data-shown] .seam__line:last-child { transition-delay: 120ms; }
[data-seam][data-shown] .seam__sub { transition-delay: 420ms; }

/* --- introducing --------------------------------------------------------- */

.intro { text-align: center; }

.intro__eyebrow {
  font-family: var(--font-mono);
  font-size: var(--body);
  font-weight: 500;
  letter-spacing: 0.26em;
  text-indent: 0.26em;          /* the tracking is trailing; this re-centres it */
  text-transform: uppercase;
  color: var(--text);

  clip-path: inset(0 50% 0 50%);
  opacity: 0;
  filter: blur(4px);
}

[data-intro][data-shown] .intro__eyebrow {
  clip-path: inset(0 -6% 0 -6%);
  opacity: 1;
  filter: blur(0);
  transition:
    clip-path 820ms var(--ease-soft),
    opacity 560ms var(--ease),
    filter 700ms var(--ease);
}

.intro__word {
  margin-block-start: clamp(14px, 1.8vw, 26px);
  font-family: var(--font-serif);
  font-weight: 400;
  /* A step above --h1: this is the release name, and it is the largest thing
     on the page after the hero. Derived from the token rather than typed as a
     number so it still moves with the scale. */
  font-size: calc(var(--h1) * 1.3);
  /* 1.04 cropped the descender of the g: the mask below covers the element
     box, and anything the line box does not contain is outside it. */
  line-height: 1.18;
  letter-spacing: -0.008em;

  /* The release ramp, tiled. The tile is a palindrome of the ramp — orange
     to green and back — so translating by exactly one tile width returns to
     an identical pattern and the loop has no seam of its own. A one-way ramp
     would jump every cycle. */
  /* The word measures very close to 6em wide in this face, so a tile of
     twice that puts exactly one clean orange-to-green sweep across it at
     rest — the comp's colouring — with the palindrome's return half always
     off to the side. Being in em, it holds at every size, which is why there
     is no breakpoint override for it. */
  --ramp-tile: 12em;
  background-image: var(--release-loop);
  background-size: var(--ramp-tile) 100%;
  background-repeat: repeat-x;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* Separates the letterforms from the canopy without tinting the ramp, which
     is what a text-shadow on clipped text would do. */
  /* Layered shadows rather than a panel behind the type: they darken only
     what is immediately around a stroke, so the letterforms separate from the
     canopy without a rectangle appearing anywhere. */
  filter:
    drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55))
    drop-shadow(0 0 22px rgba(0, 0, 0, 0.45));
  position: relative;

  /* Revealed by sliding a soft-edged mask off the word, left to right — the
     same direction the strands travel into the seam. A mask rather than a
     block of --bg sliding over it, because the canopy is behind.

     The mask is three times the width of the word, and the two end states are
     the only ones that matter: at 100% the window sits over the tail of the
     gradient and the word is entirely hidden; at 0% it sits over the head and
     the word is entirely lit. Get those percentages wrong by a little and the
     animation stops mid-wipe with the first few letters still missing, which
     looks like a font that failed to load rather than a reveal. */
  -webkit-mask-image: linear-gradient(90deg, #000 0 54%, transparent 66% 100%);
          mask-image: linear-gradient(90deg, #000 0 54%, transparent 66% 100%);
  /* Three times as wide for the wipe, twice as tall so the box's overflow —
     descenders, and the blurred halo below — stays inside the mask. */
  -webkit-mask-size: 300% 200%;
          mask-size: 300% 200%;
  -webkit-mask-position: 100% 50%;
          mask-position: 100% 50%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* The word's own halo: the same text, the same ramp, blurred and sat behind
   it. It lights the canopy in the release colours instead of darkening it,
   and because it is the same gradient on the same tile it drifts in lockstep
   with the face — one object, not a glow bolted on. */
.intro__word::before {
  content: attr(data-word);
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--release-loop);
  background-size: var(--ramp-tile) 100%;
  background-repeat: repeat-x;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  filter: blur(22px) saturate(1.5);
  opacity: 0.95;
}

[data-intro][data-shown] .intro__word::before {
  animation: ramp-drift 5s linear infinite 1700ms;
}

[data-intro][data-shown] .intro__word {
  -webkit-mask-position: 0 50%;
          mask-position: 0 50%;
  transition: -webkit-mask-position 1500ms var(--ease-soft) 380ms,
                      mask-position 1500ms var(--ease-soft) 380ms;
  animation: ramp-drift 5s linear infinite 1700ms;
}

/* Exactly one tile of travel. Any other distance lands mid-tile and jumps.
   The distance is read from the same custom property that sizes the tile, so
   the two can never drift apart. */
@keyframes ramp-drift {
  from { background-position-x: 0; }
  to   { background-position-x: calc(-1 * var(--ramp-tile)); }
}

/* --- reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .seam__line, .seam__sub, .intro__eyebrow, .intro__word, .intro__word::before {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
    animation: none !important;
  }
  .intro__word {
    -webkit-mask-image: none !important;
            mask-image: none !important;
  }
}

/* --- narrow -------------------------------------------------------------- */

@media (max-width: 720px) {
  .seam__sub { line-height: 1.75; }
}

/* ===== 11-rail.css ============================================================ */
/* =========================================================================
   02 — The Rail

   One horizontal rail carrying three cells. Scroll opens them in turn: the
   open cell takes the full content width, the other two hold a fixed spine.
   Sizes come from the type scale in 01-tokens.css — nothing here invents a
   number that the rest of the site does not already use.
   ========================================================================= */

.rail {
  position: relative;
  --spine: clamp(118px, 11vw, 182px);
  --bw: calc(min(var(--maxw), 100vw) - 2 * var(--gutter) - 2 * var(--spine));
}

/* --- the grid ------------------------------------------------------------
   Six columns across the viewport and a fixed row height, drawn as a mask
   over a vertical ramp so the lines themselves carry the colour: #8E8E8E at
   the top of the section down to the page's own #191919, then flat. Painting
   the lines a flat colour and fading them with opacity looks similar in a
   screenshot and wrong in motion — the ramp has to be in the ink.

   The geometry is deliberately independent of this section's height, because
   the next section hangs off the same lines. */

/* The plate is everything stacked under the rail. It carries no background of
   its own: the grid that used to run behind these sections has been removed,
   and they sit directly on the page ground. It stays a positioned ancestor
   because every section inside it establishes a stacking context off it. */
.plate { position: relative; }


/* --- section frame ------------------------------------------------------- */

.rail__eyebrow {
  margin-block-end: clamp(22px, 3vw, 40px);
  text-align: center;
}

/* The eyebrow's opening animation lives in 09-gap.css keyed to that section.
   Re-arm it here rather than duplicating the rule, so the two sections open
   their labels identically by construction. */
[data-rail][data-shown] .eyebrow__text {
  clip-path: inset(0 -4% 0 -4%);
  opacity: 1;
  filter: blur(0);
  transition:
    clip-path 900ms var(--ease-soft),
    opacity 620ms var(--ease),
    filter 780ms var(--ease);
}

.stage {
  position: sticky;
  top: var(--nav-h);
  height: calc(100svh - var(--nav-h));
  display: grid;
  align-content: center;
  z-index: 1;
}

/* A grid item's min-width is auto, so the three cells' min-content width was
   pushing this track two pixels past the viewport at 1024 and putting a
   horizontal scrollbar on the whole page. The cells already clip and scale
   themselves; the track must be allowed to be narrower than they want. */
.stage > .shell,
.stage .cells { min-width: 0; }

.track { height: 300vh; }

/* --- the rail -------------------------------------------------------------
   Three cells in a row. The open one takes the content width; the other two
   hold a spine and let their bodies bleed through it. */

.cells {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface);
}

.cell {
  position: relative;
  flex: 0 0 var(--spine);
  min-width: 0;
  overflow: hidden;
  border-left: 1px solid var(--rule);
  transition: flex-basis 700ms var(--ease-soft);
}

.cell:first-child { border-left: 0; }
.cell.is-on { flex-basis: var(--bw); }

/* --- the spine ----------------------------------------------------------- */

.cell__sp {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 17px 20px;
  min-height: 56px;
  background: #252525;
  border-bottom: 1px solid var(--rule);
  white-space: nowrap;
  position: relative;
}

/* The accent underline writes itself across the open cell's spine — the only
   accent-coloured rule in the section, so it reads as "you are here". */
.cell__sp::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 1px;
  width: 0;
  background: var(--accent);
  transition: width 700ms var(--ease-soft);
}

.cell.is-on .cell__sp::after { width: 100%; }

.cell__n {
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  color: var(--text-muted);
  transition: color 400ms var(--ease);
}

.cell__nm {
  font-size: var(--body-sm);
  color: var(--text-muted);
  transition: color 400ms var(--ease);
}

.cell__tg {
  font-size: var(--body-sm);
  color: var(--text-muted);
  opacity: 0;
  transition: opacity 500ms var(--ease-soft);
}

.cell.is-on .cell__n  { color: var(--accent); }
.cell.is-on .cell__nm { color: var(--text); font-weight: 500; }
.cell.is-on .cell__tg { opacity: 0.68; transition-delay: 200ms; }

/* --- the body ------------------------------------------------------------ */

.cell__b {
  width: var(--bw);
  padding: clamp(26px, 3.4vh, 42px) 22px clamp(26px, 3.4vh, 42px);
  display: grid;
  grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.14fr);
  gap: clamp(24px, 3.4vw, 54px);
  /* Top, not centre. Centred, each cell's grid balances its own mockup, and
     since the three mockups are different heights the panel appears to jump
     vertically every time the step changes. */
  align-items: start;
  opacity: 0.16;
  transition: opacity 700ms var(--ease-soft);
}

.cell.is-on .cell__b { opacity: 1; }

/* A closed cell's body is masked, not cropped: the whole point of the spine is
   that the thing behind it is visibly continuing. */
.cell:not(.is-on) .cell__b {
  -webkit-mask-image: linear-gradient(to right, #000 calc(var(--spine) - 46px), transparent var(--spine));
          mask-image: linear-gradient(to right, #000 calc(var(--spine) - 46px), transparent var(--spine));
}

.cell__tx h2 {
  font-family: var(--font-serif);
  font-weight: 400;
  /* A step under --h2: the text column is 0.86fr of the open cell, and the
     full step breaks all three of these headlines into four lines apiece.
     Derived from the token so it still moves with the scale. */
  font-size: calc(var(--h2) * 0.85);
  line-height: var(--lh-h2);
  letter-spacing: -0.02em;
}

.cell__tx p {
  margin-block-start: 18px;
  max-width: 42ch;
  font-size: var(--body);
  line-height: 1.55;
  color: var(--text-muted);
}

.cell__tx ul {
  margin-block-start: 20px;
  display: grid;
  gap: 9px;
  list-style: none;
  padding: 0;
}

.cell__tx li {
  position: relative;
  padding-inline-start: 18px;
  font-size: var(--body-sm);
  line-height: 1.45;
  color: var(--text-muted);
}

.cell__tx li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  width: 6px;
  height: 1px;
  background: var(--rule);
}

.lnk {
  display: inline-block;
  margin-block-start: 22px;
  font-size: var(--body-sm);
  color: var(--accent);
}

.lnk span {
  display: inline-block;
  transition: transform 220ms var(--ease);
}

.lnk:hover span { transform: translateX(4px); }

/* =========================================================================
   The mockups

   House rule, and it is the whole difference between these reading as
   software and reading as an illustration of software: no letter-spaced
   words, no uppercase labels, and no coloured dot that does not mean
   something. Every dot in here is either a browser control or a state.
   ========================================================================= */

.mk {
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 9px;
  overflow: hidden;
  box-shadow: 0 26px 60px rgba(0, 0, 0, 0.45);
}

.mk__bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--rule);
  background: rgba(255, 255, 255, 0.018);
}

.mk__t {
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text);
}

.mk__t--dim, .mk__sep { color: var(--text-muted); }
.mk__sep { font-family: var(--font-mono); font-size: var(--mono-sm, 11px); }

.mk__s {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
}

.mk__s--ok { color: var(--ok); }

.mk__s--live b {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--danger);
  display: block;
  animation: rec 1.4s ease-in-out infinite;
}

@keyframes rec { 50% { opacity: 0.25; } }

.mk__clock { color: var(--text); font-variant-numeric: tabular-nums; }

.mk__b { padding: 16px 16px 18px; position: relative; }

/* A single pass of light across the panel when its cell opens — the same
   idea as the seam's sparks, at a tenth of the volume. */
.mk::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(105deg,
    transparent 38%, rgba(255, 255, 255, 0.05) 50%, transparent 62%);
  background-size: 260% 100%;
  background-position: 130% 0;
  opacity: 0;
}

.cell.play .mk::after { animation: mk-sheen 1400ms var(--ease-soft) 200ms; }

@keyframes mk-sheen {
  0%   { opacity: 1; background-position: 130% 0; }
  100% { opacity: 1; background-position: -130% 0; }
}

.mk { position: relative; }

/* --- 1 · the graph -------------------------------------------------------- */

.graph { width: 100%; height: auto; display: block; overflow: visible; }

.gn rect { fill: var(--surface); stroke: var(--rule); stroke-width: 1; }
.gn__l { font-family: var(--font-sans); font-size: 12.5px; fill: var(--text); }
.gn__k { font-family: var(--font-mono); font-size: 8.5px; fill: var(--text-muted); }

.gn--src rect { stroke: var(--accent); fill: rgba(228, 117, 37, 0.09); }
.gn--src .gn__k { fill: var(--accent-text, #E89158); }

.gn { opacity: 0; transform: translateX(-8px); }
.cell.play .gn { animation: gin 500ms var(--ease-soft) forwards; animation-delay: calc(var(--i) * 160ms); }
@keyframes gin { to { opacity: 1; transform: none; } }

/* The source keeps breathing after it lands: it is the node the whole panel
   is about, and a static diagram gives the eye nowhere to start. */
.cell.play .gn--src rect { animation: src-pulse 2.8s var(--ease-soft) 1.2s infinite; }
@keyframes src-pulse {
  0%, 100% { stroke: var(--accent); filter: none; }
  50%      { stroke: #FF9A55; filter: drop-shadow(0 0 6px rgba(228, 117, 37, 0.5)); }
}

/* Each destination lights once as its packet arrives. */
.cell.play .gn--dst rect { animation: dst-hit 2.4s linear infinite; animation-delay: calc(var(--i) * 140ms + 1000ms); }
@keyframes dst-hit {
  0%, 74%, 100% { stroke: var(--rule); }
  80%           { stroke: rgba(232, 145, 88, 0.75); }
}

.ge {
  fill: none;
  stroke: var(--rule);
  stroke-width: 1.2;
  stroke-dasharray: 260;
  stroke-dashoffset: 260;
}

.cell.play .ge { animation: gdraw 700ms var(--ease-soft) forwards; animation-delay: calc(var(--i) * 140ms + 420ms); }
@keyframes gdraw { to { stroke-dashoffset: 0; } }

.gp { fill: var(--accent); opacity: 0; }
.cell.play .gp { animation: gfade 400ms linear forwards; animation-delay: calc(var(--i) * 140ms + 1000ms); }
@keyframes gfade { to { opacity: 0.9; } }

.mk__f {
  margin-block-start: 12px;
  text-align: center;
  font-size: var(--body-sm);
  color: var(--text-muted);
  opacity: 0;
}

.cell.play .mk__f { animation: gfade 500ms linear forwards 1.5s; }

/* --- 2 · browser and backend --------------------------------------------- */

.br {
  border: 1px solid var(--rule);
  border-radius: 7px;
  overflow: hidden;
  background: var(--surface);
}

.br__chrome {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 9px 11px;
  border-bottom: 1px solid var(--rule);
  background: rgba(255, 255, 255, 0.02);
}

/* Window controls. These are the only free-standing dots in the section and
   they are here because a browser has them. */
.br__chrome i { width: 7px; height: 7px; border-radius: 50%; background: var(--rule); }

.br__url {
  margin-left: 8px;
  flex: 1;
  padding: 4px 10px;
  border-radius: 4px;
  background: var(--bg);
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
}

.br__page { padding: 13px 14px 15px; position: relative; }

.br__h { font-size: var(--body-sm); font-weight: 500; margin-block-end: 10px; }

.br__h em {
  font-style: normal;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
  margin-inline-start: 8px;
}

.br__rows { display: grid; gap: 5px; list-style: none; padding: 0; }

.br__r {
  display: grid;
  grid-template-columns: 62px 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 8px 11px;
  border-radius: 5px;
  background: var(--bg);
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
  opacity: 0;
  transform: translateY(4px);
}

.br__r > :last-child { color: var(--text); font-variant-numeric: tabular-nums; }

.cell.play .br__r { animation: rin 400ms var(--ease-soft) forwards; animation-delay: calc(var(--i) * 130ms + 700ms); }
@keyframes rin { to { opacity: 1; transform: none; } }

.br__r--other { color: var(--text); }

.cell.play .br__r--other {
  animation:
    rin 400ms var(--ease-soft) forwards,
    flag 900ms var(--ease-soft) forwards calc(var(--i) * 130ms + 1500ms);
}

@keyframes flag {
  to {
    background: rgba(251, 191, 36, 0.11);
    box-shadow: inset 0 0 0 1px rgba(251, 191, 36, 0.35);
  }
}

.br__cur {
  position: absolute;
  left: 24px;
  top: 34px;
  width: 13px;
  height: 17px;
  opacity: 0;
  background: var(--text);
  clip-path: polygon(0 0, 0 100%, 27% 76%, 45% 100%, 62% 90%, 44% 66%, 100% 62%);
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.6));
}

.cell.play .br__cur { animation: cur 2.4s var(--ease-soft) forwards 200ms; }

@keyframes cur {
  0%   { opacity: 0; transform: translate(120px, 90px); }
  12%  { opacity: 1; }
  45%  { transform: translate(38px, 26px); }
  60%  { transform: translate(38px, 26px) scale(0.86); }
  70%  { transform: translate(38px, 26px); }
  100% { opacity: 1; transform: translate(38px, 26px); }
}

.sync {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-block: 13px 11px;
  opacity: 0;
}

.cell.play .sync { animation: gfade 400ms linear forwards 1.5s; }

.sync__l {
  flex: 1;
  height: 1px;
  background: repeating-linear-gradient(90deg, var(--rule) 0 4px, transparent 4px 8px);
}

.sync__t {
  font-size: var(--body-sm);
  color: var(--text-muted);
}

.bk { display: grid; gap: 7px; list-style: none; padding: 0; }

.bk li {
  display: grid;
  grid-template-columns: 58px 1fr;
  gap: 10px;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
  opacity: 0;
  transform: translateY(3px);
}

.bk__k { color: var(--text-muted); opacity: 0.7; }
.bk li b { color: var(--warn); font-weight: 400; }

.cell.play .bk li { animation: rin 400ms var(--ease-soft) forwards; animation-delay: calc(var(--i) * 220ms + 1700ms); }

/* --- 3 · the finding ------------------------------------------------------ */

.fd__t {
  font-size: 19px;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.015em;
  opacity: 0;
  transform: translateY(6px);
}

.fd__r {
  margin-block-start: 10px;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
  opacity: 0;
}

.cell.play .fd__t { animation: rin 500ms var(--ease-soft) forwards 300ms; }
.cell.play .fd__r { animation: gfade 400ms linear forwards 600ms; }

.fd__ev { display: flex; gap: 7px; margin-block-start: 15px; flex-wrap: wrap; }

.fd__e {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 9px;
  border: 1px solid rgba(232, 145, 88, 0.38);
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--accent-text, #E89158);
  opacity: 0;
  transform: translateY(5px) scale(0.96);
}

.cell.play .fd__e { animation: rin 380ms var(--ease-soft) forwards; animation-delay: calc(var(--i) * 110ms + 950ms); }

.rep {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-block-start: 18px;
  padding-block-start: 15px;
  border-top: 1px solid var(--rule);
}

.rep__l, .rep__n {
  font-size: var(--body-sm);
  color: var(--text-muted);
}

.rep__n { margin-left: auto; color: var(--ok); opacity: 0; font-variant-numeric: tabular-nums; }
.cell.play .rep__n { animation: gfade 400ms linear forwards 2.2s; }

/* Two runs, two ticks. They fill in sequence rather than together, because
   the claim is that it happened twice and not that it happened. */
.rep__d {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1.5px solid var(--rule);
  position: relative;
}

.rep__d::after {
  content: "";
  position: absolute;
  left: 3.5px;
  top: 4.5px;
  width: 5px;
  height: 2.5px;
  border-left: 1.5px solid var(--bg);
  border-bottom: 1.5px solid var(--bg);
  transform: rotate(-45deg);
  opacity: 0;
}

.cell.play .rep__d { animation: dot 350ms var(--ease-soft) forwards; animation-delay: calc(var(--i) * 420ms + 1500ms); }
.cell.play .rep__d::after { animation: gfade 200ms linear forwards; animation-delay: calc(var(--i) * 420ms + 1700ms); }

@keyframes dot { to { background: var(--ok); border-color: var(--ok); } }

.fd__ho {
  margin-block-start: 14px;
  font-size: var(--body-sm);
  color: var(--text-muted);
  opacity: 0;
}

.fd__ho b { color: var(--text); font-weight: 500; }
.cell.play .fd__ho { animation: gfade 400ms linear forwards 2.5s; }

/* --- narrow -------------------------------------------------------------- */

@media (max-width: 1023px) {
  .stage { position: static; height: auto; padding-block: 28px 32px; }
  .track { display: none; }

  .cells { display: block; border: 0; background: none; overflow: visible; }

  .cell {
    flex: none;
    overflow: visible;
    border-left: 0;
    border-top: 1px solid var(--rule);
  }

  .cell__sp {
    padding-inline: 0;
    white-space: normal;
    border-bottom: 0;
    background: none;
  }

  .cell__sp::after { display: none; }
  .cell__n { color: var(--accent); }
  .cell__nm { color: var(--text); font-weight: 500; }
  .cell__tg { opacity: 0.68; }

  .cell__b {
    width: auto;
    opacity: 1;
    grid-template-columns: 1fr;
    gap: 26px;
    padding: 0 0 44px;
    -webkit-mask-image: none !important;
            mask-image: none !important;
  }
}

/* --- reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .rail *, .rail *::before, .rail *::after { animation: none !important; }
  .gn, .ge, .gp, .br__r, .bk li, .fd__t, .fd__r, .fd__e, .mk__f, .sync, .rep__n, .fd__ho {
    opacity: 1 !important;
    transform: none !important;
  }
  .ge { stroke-dashoffset: 0 !important; }
  .br__cur { opacity: 1 !important; transform: translate(38px, 26px) !important; }
  .rep__d { background: var(--ok); border-color: var(--ok); }
  .rep__d::after { opacity: 1 !important; }
}

/* =========================================================================
   The rail as a card stack — below 1024

   The sticky rail is a wide-viewport idea: three cells side by side, opened
   in turn by scroll. Stacked, the arrangement it was built to show is gone,
   and what was left was three long text blocks each trailing a mockup.

   The reference (CodeRabbit on a phone) does the same job with a card: a
   numbered header, an accent rule under it, the mockup inside, and the copy
   trimmed to what the header does not already say. That is what this is.

   Copy is cut two ways. The h2 is dropped, because it restates the header's
   own tagline almost word for word — "The Map / Knows a column from a
   function" then "It knows a column from a function." And the bullet list
   goes: three cells of three bullets is nine lines of secondary detail
   sitting between the reader and the next step.
   ========================================================================= */

@media (max-width: 1023px) {

  /* A horizontal scroller with snap points, driven by the arrows below —
     the reference's arrangement, and the one that keeps the three steps
     feeling like one rail rather than three stacked articles. Bleeding to
     both page edges is what makes the next card's edge visible. */
  .cells {
    display: flex;
    flex-direction: row;
    align-items: start;
    gap: 14px;
    margin-inline: calc(var(--gutter) * -1);
    padding-inline: var(--gutter);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline-start: var(--gutter);
    scrollbar-width: none;
    border: 0;
    background: none;
  }

  .cells::-webkit-scrollbar { display: none; }

  /* `.cell.is-on` carries a flex shorthand for the horizontal rail, and every
     cell is open once stacked. In a column container that shorthand's basis
     is a HEIGHT, so all three cards collapsed to the 106px spine width and
     clipped their own mockups. Both selectors are reset here, and the second
     matches the specificity of the rule it has to beat. */
  .cell,
  .cell.is-on {
    /* One card per screen, with the next one's edge showing. */
    flex: 0 0 88%;
    scroll-snap-align: start;
    align-self: stretch;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--rule-soft);
    border-radius: var(--radius);
    background: rgba(255, 255, 255, 0.012);
    overflow: hidden;
  }

  /* `display: contents` lifts the two halves of the body out of their
     wrapper so the mockup can be ordered above the copy without touching
     the markup, which the desktop grid still needs as one box. */
  .cell__b { display: contents; }

  .cell__sp {
    order: 0;
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 16px 18px 14px;
    border-block-end: 1px solid var(--rule-soft);
  }

  /* The accent rule under the header, as in the reference: a short bar at
     the start of the border rather than a full-width stripe. */
  .cell__sp { position: relative; }

  .cell__sp::before {
    content: "";
    position: absolute;
    inset-block-end: -1px;
    inset-inline-start: 0;
    width: 38%;
    height: 1px;
    background: var(--accent);
  }

  .cell__tg { white-space: normal; opacity: 1; color: var(--text-muted); }

  .cell__mk { order: 1; padding: 18px; }

  .cell__tx { order: 2; padding: 0 18px 20px; }

  /* The card has to fit one screen — otherwise reaching the arrow means
     scrolling past the thing the arrow controls, then scrolling back to read
     the next one. So the card carries only what the reference's does: the
     header, the mockup, and the way out. The h2 restates the header's own
     tagline, and the paragraph and bullets are detail the feature page
     already carries in full. */
  .cell__tx h2,
  .cell__tx ul,
  .cell__tx p { display: none; }

  .cell__tx { padding-block-start: 0; }

  /* Whatever the mockup's natural height, the card stays inside the screen:
     nav, the section label, the card and the controls all together. The
     mockups are anchored to their tops, so what a cap trims is trailing
     space rather than the first row. */
  .cell__mk {
    max-height: calc(100svh - var(--nav-h) - 250px);
    min-height: 200px;
    overflow: hidden;
  }

  .cell__b { padding: 0; }
}

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

.rail__ctl { display: none; }

@media (max-width: 1023px) {
  .rail__ctl {
    display: flex;
    gap: 10px;
    margin-block-start: 18px;
  }

  .rail__btn {
    width: 46px;
    height: 46px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--rule-soft);
    border-radius: 10px;
    background: none;
    color: var(--text-muted);
    font-size: 17px;
    cursor: pointer;
    transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
  }

  .rail__btn:active { color: var(--text); border-color: var(--rule); }
  .rail__btn[disabled] { opacity: 0.35; }

  /* Pushed to the far end, as in the reference. */
  .rail__btn--end { margin-inline-start: auto; }

  /* Two bars for pause; one triangle for play. Two boxes rather than one
     clip-path: a polygon is a single closed shape and cannot describe two
     separate bars, which is why the first attempt drew a corner. */
  .rail__ic {
    position: relative;
    display: block;
    width: 12px;
    height: 13px;
  }

  .rail__ic::before,
  .rail__ic::after {
    content: "";
    position: absolute;
    inset-block: 0;
    width: 3.5px;
    background: currentColor;
    border-radius: 1px;
  }

  .rail__ic::before { inset-inline-start: 1px; }
  .rail__ic::after  { inset-inline-end: 1px; }

  /* Paused: the two bars become one triangle. */
  [data-rail-play][aria-pressed="true"] .rail__ic::after { display: none; }

  [data-rail-play][aria-pressed="true"] .rail__ic::before {
    inset-inline-start: 1px;
    width: 0;
    background: none;
    border-block: 6.5px solid transparent;
    border-inline-start: 11px solid currentColor;
    border-inline-end: 0;
  }
}

/* --- the map, portrait --------------------------------------------------- */

.graph--tall { display: none; }

@media (max-width: 1023px) {
  .graph--wide { display: none; }
  .graph--tall { display: block; }
}

/* ===== 12-notices.css ============================================================ */
/* =========================================================================
   03 — It Notices

   A normal section sitting on the plate's pinned grid: it scrolls while the
   lines behind it hold still.

   The mockup runs one loop on a single `--cycle`. Every beat in it — the line
   typing, the gutter mark, the tab's modified dot, the status bar changing
   its wording, the notice arriving, the button landing — is a percentage of
   that one duration rather than its own delay, which is the only way a
   sequence this long stays in step over hours of looping.
   ========================================================================= */

.notices {
  position: relative;
  z-index: 1;
  padding-block: clamp(58px, 7.3vw, 100px) clamp(74px, 9.3vw, 126px);
}

.notices__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.88fr) minmax(0, 1.12fr);
  gap: clamp(34px, 5vw, 86px);
  align-items: center;
}

/* --- the copy ------------------------------------------------------------ */

.notices__eyebrow { text-align: start; }

/* The label's opening animation lives in 09-gap.css keyed to that section.
   Re-armed here rather than duplicated, so every section on the page opens
   its label identically by construction. */
[data-notices][data-shown] .eyebrow__text {
  clip-path: inset(0 -4% 0 -4%);
  opacity: 1;
  filter: blur(0);
  transition:
    clip-path 900ms var(--ease-soft),
    opacity 620ms var(--ease),
    filter 780ms var(--ease);
}

.notices__head {
  margin-block-start: 26px;
  font-family: var(--font-serif);
  font-weight: 400;
  /* A shade under --h1. This is a single statement with a whole column to
     itself, so it carries more weight than a section heading — but not the
     hero's. Derived from the token so it still moves with the scale. */
  font-size: calc(var(--h1) * 0.85);
  line-height: 1.04;
  letter-spacing: -0.02em;
}

.notices__head span { display: block; }

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

[data-notices][data-shown] [data-lift] {
  opacity: 1;
  transform: none;
  transition:
    opacity 820ms var(--ease),
    transform 1000ms var(--ease-soft);
}

.notices__head span:nth-child(1) { transition-delay: 120ms; }
.notices__head span:nth-child(2) { transition-delay: 220ms; }
.notices__tx .lead { transition-delay: 320ms; }
.notices__tx .pts > li:nth-child(1) { transition-delay: 400ms; }
.notices__tx .pts > li:nth-child(2) { transition-delay: 470ms; }
.notices__tx .pts > li:nth-child(3) { transition-delay: 540ms; }

.notices__mk { transition-delay: 260ms; }

/* The editor arrives with a little more than a fade: it lifts, settles out of
   a slight scale, and takes one pass of light across it. */
[data-notices] .notices__mk { transform: translateY(26px) scale(0.985); }

[data-notices][data-shown] .notices__mk {
  transform: none;
  transition:
    opacity 900ms var(--ease) 260ms,
    transform 1100ms var(--ease-soft) 260ms;
}

/* =========================================================================
   The editor
   ========================================================================= */

.ed {
  --cycle: 11s;
  position: relative;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 11px;
  box-shadow: 0 32px 70px rgba(0, 0, 0, 0.55);
}

/* One pass of light when the section opens, matched to the rail's mockups. */
.ed::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(105deg, transparent 38%, rgba(255, 255, 255, 0.05) 50%, transparent 62%);
  background-size: 260% 100%;
  background-position: 130% 0;
  opacity: 0;
}

[data-notices][data-shown] .ed::after { animation: ed-sheen 1500ms var(--ease-soft) 620ms; }

@keyframes ed-sheen {
  0%   { opacity: 1; background-position: 130% 0; }
  100% { opacity: 1; background-position: -130% 0; }
}

/* --- tab bar ------------------------------------------------------------- */

.ed__bar {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 11px 13px 0;
  border-bottom: 1px solid var(--rule);
  border-radius: 11px 11px 0 0;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent);
  /* Three filenames do not fit on a phone. A real tab strip scrolls rather
     than wrapping or shrinking its labels, so this one does too. */
  overflow-x: auto;
  scrollbar-width: none;
}

.ed__bar::-webkit-scrollbar { display: none; }

/* Window controls. The only dots here that are not a state. */
.ed__bar > i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--rule);
  flex: none;
}

.ed__bar > i:first-child { margin-inline-start: 2px; }

.ed__tab {
  margin-block-end: -1px;
  padding: 9px 14px;
  border-radius: 6px 6px 0 0;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  gap: 7px;
  white-space: nowrap;
}

.ed__tab:first-of-type { margin-inline-start: 12px; }

.ed__tab.is-on {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--rule);
  border-bottom-color: var(--bg);
}

.ed__mod {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--rule);
  display: block;
  animation: ed-mod var(--cycle) linear infinite;
}

@keyframes ed-mod {
  0%, 26%   { background: var(--rule); }
  30%, 100% { background: var(--warn); }
}

/* --- code ---------------------------------------------------------------- */

/* The code lines do not wrap — wrapped source in a mockup reads as broken
   source. On a narrow screen they are wider than the panel, so the panel
   scrolls them rather than letting them push the page sideways. */
.ed__body {
  background: var(--bg);
  padding: 15px 0 17px;
  overflow-x: auto;
  scrollbar-width: none;
}

.ed__body::-webkit-scrollbar { display: none; }

.ed__code {
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  line-height: 2;
  list-style: none;
  padding: 0;
  margin: 0;
}

.ed__l {
  display: grid;
  grid-template-columns: 46px 1fr;
  opacity: 0;
  transform: translateY(3px);
}

[data-notices][data-shown] .ed__l {
  animation: ed-line 400ms var(--ease-soft) forwards;
  animation-delay: calc(var(--i) * 90ms + 700ms);
}

@keyframes ed-line { to { opacity: 1; transform: none; } }

.ed__n {
  color: #4A423D;
  text-align: end;
  padding-inline-end: 16px;
  user-select: none;
}

.ed__c { position: relative; white-space: nowrap; }

/* An editor theme, not a palette of accents. These are the colours a real
   syntax theme uses, and swapping them for the brand's would make the panel
   look like a diagram of code rather than code. */
.ed-k  { color: #C98AE8; font-weight: 400; }
.ed-f  { color: #7FB2F0; }
.ed-s  { color: #8FD19E; }
.ed-nu { color: #E8A87C; }
.ed-v  { color: var(--text); }
.ed-pu { color: #8A7F78; }
.ed-c  { color: #5C534D; font-style: italic; }

.ed__l--new { opacity: 0; }

[data-notices][data-shown] .ed__l--new { animation: ed-line 400ms var(--ease-soft) forwards 1150ms; }

[data-notices][data-shown] .ed__l--new .ed__c { animation: ed-hl var(--cycle) linear infinite; }

@keyframes ed-hl {
  0%, 24%   { background: rgba(228, 117, 37, 0); }
  30%, 72%  { background: rgba(228, 117, 37, 0.07); }
  100%      { background: rgba(228, 117, 37, 0); }
}

.ed__gut {
  position: absolute;
  left: -16px;
  top: 2px;
  bottom: 2px;
  width: 2px;
  border-radius: 2px;
  background: var(--accent);
  opacity: 0;
  animation: ed-gut var(--cycle) linear infinite;
}

@keyframes ed-gut {
  0%, 25%  { opacity: 0; }
  30%, 74% { opacity: 0.9; }
  100%     { opacity: 0; }
}

/* The line types itself. `steps` matches the character count exactly — one
   short and the last bracket never lands, one long and it holds a blank
   column at the end. */
.ed__type {
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
  vertical-align: bottom;
  width: 0;
  color: var(--text);
  animation: ed-type var(--cycle) steps(29, end) infinite;
}

@keyframes ed-type {
  0%, 8%    { width: 0; }
  26%, 74%  { width: 29ch; }
  100%      { width: 29ch; }
}

.ed__caret {
  display: inline-block;
  width: 1.5px;
  height: 15px;
  background: var(--accent);
  vertical-align: -2px;
  margin-inline-start: 1px;
  animation: ed-blink 1s steps(2) infinite;
}

@keyframes ed-blink { 50% { opacity: 0; } }

/* --- status bar ---------------------------------------------------------- */

.ed__st {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 9px 15px;
  border-top: 1px solid var(--rule);
  border-radius: 0 0 11px 11px;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
  background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.02));
}

.ed__ch { animation: ed-changed var(--cycle) linear infinite; }

@keyframes ed-changed {
  0%, 27%   { color: var(--text-muted); }
  32%, 100% { color: var(--warn); }
}

/* Two labels stacked in one grid cell, so the swap cannot change the width of
   the status bar and shuffle everything to its left. */
.ed__w {
  margin-inline-start: auto;
  display: grid;
  justify-items: end;
  color: var(--accent-text, #E89158);
}

.ed__w > span { grid-area: 1 / 1; white-space: nowrap; }

.ed__w1 { animation: ed-w1 var(--cycle) linear infinite; }
.ed__w2 { opacity: 0; animation: ed-w2 var(--cycle) linear infinite; }

@keyframes ed-w1 {
  0%, 27%   { opacity: 1; }
  33%, 92%  { opacity: 0; }
  100%      { opacity: 1; }
}

@keyframes ed-w2 {
  0%, 27%   { opacity: 0; }
  33%, 90%  { opacity: 1; }
  96%, 100% { opacity: 0; }
}

/* --- the notice ---------------------------------------------------------- */

.nt {
  position: absolute;
  right: -20px;
  bottom: -34px;
  width: min(80%, 382px);
  padding: 16px 17px 15px;
  /* Opaque, and no backdrop-filter. The fill was already 94% opaque, so the
     blur behind it was invisible — and a backdrop-filter is the most expensive
     thing a small element can ask for, re-read every frame while the card
     animates in and out of a looping cycle. */
  background: #191919;
  border: 1px solid var(--rule);
  border-radius: 11px;
  box-shadow: 0 26px 54px rgba(0, 0, 0, 0.6), 0 0 40px rgba(228, 117, 37, 0.07);
  opacity: 0;
  transform: translateY(16px) scale(0.985);
  animation: nt-in var(--cycle) var(--ease-soft) infinite;
}

@keyframes nt-in {
  0%, 40%   { opacity: 0; transform: translateY(16px) scale(0.985); }
  46%, 86%  { opacity: 1; transform: translateY(0) scale(1); }
  94%, 100% { opacity: 0; transform: translateY(10px) scale(0.99); }
}

/* The app's own mark, the way any real notification carries the icon of
   whatever sent it — and one fewer coloured dot standing in for a brand. */
.nt__from {
  display: flex;
  align-items: center;
  gap: 9px;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
}

.nt__mark { width: 18px; height: auto; opacity: 0.85; }

.nt__msg { margin-block-start: 11px; font-size: var(--body-sm); line-height: 1.5; }

.nt__msg b {
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  font-weight: 400;
  color: var(--accent-text, #E89158);
}

.nt__act { display: flex; gap: 9px; margin-block-start: 15px; flex-wrap: wrap; }

.nt__b {
  padding: 9px 14px;
  border-radius: 7px;
  border: 1px solid var(--rule);
  font-size: var(--body-sm);
  color: var(--text-muted);
  white-space: nowrap;
}

.nt__b--y {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 500;
  opacity: 0;
  animation: nt-btn var(--cycle) var(--ease-soft) infinite;
}

@keyframes nt-btn {
  0%, 46%   { opacity: 0; transform: translateY(4px); }
  52%, 86%  { opacity: 1; transform: none; }
  94%, 100% { opacity: 0; }
}

/* --- the loop only runs while it is on screen ---------------------------- */

[data-notices] .ed, [data-notices] .ed *, [data-notices] .nt, [data-notices] .nt * {
  animation-play-state: paused;
}

[data-notices][data-live] .ed, [data-notices][data-live] .ed *,
[data-notices][data-live] .nt, [data-notices][data-live] .nt * {
  animation-play-state: running;
}

/* --- narrow -------------------------------------------------------------- */

@media (max-width: 1023px) {
  /* minmax(0, 1fr), not 1fr. A bare 1fr track takes its automatic minimum
     from the content's min-content width, and the editor's tab strip is
     wider than a phone — so the column grows past the viewport and the whole
     page scrolls sideways, even though the strip itself scrolls. */
  .notices__grid { grid-template-columns: minmax(0, 1fr); gap: 56px; }

  /* The status bar cannot scroll — its last item is the one that changes, and
     an item you have to scroll to is not a status. The branch labels go
     instead; what is left is the change count and what Descry is doing. */
  .ed__st > span:nth-child(1),
  .ed__st > span:nth-child(2) { display: none; }

  .notices__head { font-size: calc(var(--h2) * 1.1); }

  /* Stacked, the notice has room of its own and no longer needs to hang off
     the corner of a panel that is now the full width of the screen. */
  .nt {
    position: static;
    width: auto;
    margin-block-start: 16px;
    opacity: 1 !important;
    transform: none !important;
    animation: none;
  }

  .nt__b--y { opacity: 1; animation: none; }
}

/* --- reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .notices *, .notices *::before, .notices *::after { animation: none !important; }

  .notices [data-lift], .ed__l, .ed__l--new {
    opacity: 1 !important;
    transform: none !important;
  }

  .ed__type { width: 29ch; }
  .ed__caret { display: none; }
  .ed__mod { background: var(--warn); }
  .ed__ch { color: var(--warn); }
  .ed__w1 { opacity: 0; }
  .ed__w2 { opacity: 1; }
  .nt { opacity: 1 !important; transform: none !important; }
  .nt__b--y { opacity: 1 !important; }
}

/* ===== 13-think.css ============================================================ */
/* =========================================================================
   04 — How it thinks

   Centred label and headline, then the instrument on the left and the
   argument on the right.

   Every state in the panel is a glyph, not a coloured dot: a ring that fills
   with a rotating arc while a theory is working, a tick when it is confirmed,
   a dash when it is contradicted. A row of coloured dots would carry the same
   information and read as a legend rather than as software.
   ========================================================================= */

.think {
  position: relative;
  z-index: 1;
  padding-block: clamp(64px, 8vw, 112px) clamp(72px, 9.3vw, 126px);

  /* The one state colour this section needs that the palette does not have.
     Confirmed is --ok and contradicted is muted, but "working" has to be
     legible as neither, and every warm hue on the page already means
     something else. */
  --th-run: #61A6F5;
}

.think__eyebrow { text-align: start; }

[data-think][data-shown] .eyebrow__text {
  clip-path: inset(0 -4% 0 -4%);
  opacity: 1;
  filter: blur(0);
  transition:
    clip-path 900ms var(--ease-soft),
    opacity 620ms var(--ease),
    filter 780ms var(--ease);
}

.think__head {
  margin-block-start: clamp(20px, 2.6vw, 34px);
  text-align: start;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: calc(var(--h1) * 0.85);
  line-height: 1.06;
  letter-spacing: -0.022em;
}

.think__head span { display: block; }

.think__cols {
  margin-block-start: clamp(34px, 4.4vw, 60px);
  display: grid;
  /* The panel gets the wider track: the failure line under the title bar is
     a single unwrapped request, and at an even split it breaks after "null
     on", which reads as a truncated log rather than as one event. */
  grid-template-columns: minmax(0, 1.14fr) minmax(0, 1fr);
  gap: clamp(36px, 5vw, 84px);
  /* Top-aligned, not centred: the panel is two and a half times the height of
     the argument beside it, and centring the short column against the tall one
     opens a hole above the copy that reads as a mistake. */
  align-items: start;
}

/* --- the copy ------------------------------------------------------------ */

.think__copy { text-align: start; }

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

[data-think][data-shown] [data-lift] {
  opacity: 1;
  transform: none;
  transition:
    opacity 820ms var(--ease),
    transform 1000ms var(--ease-soft);
}

.think__head span:nth-child(1) { transition-delay: 110ms; }
.think__head span:nth-child(2) { transition-delay: 200ms; }
.think__bed { transition-delay: 300ms; }
.think__copy .lead { transition-delay: 360ms; }
.think__copy .pts > li:nth-child(1) { transition-delay: 430ms; }
.think__copy .pts > li:nth-child(2) { transition-delay: 500ms; }
.think__copy .pts > li:nth-child(3) { transition-delay: 570ms; }

/* =========================================================================
   The panel
   ========================================================================= */

.th {
  position: relative;
  border: 1px solid var(--rule);
  border-radius: 16px;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 8% 0%, rgba(228, 117, 37, 0.055), transparent 58%),
    var(--surface);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 30px 70px -30px rgba(0, 0, 0, 0.9);
}

/* A faint field of points, drifting. It is the difference between a panel
   that is switched on and one that is printed.

   It drifts by TRANSFORM, not by background-position. Animating
   background-position repaints the whole panel on every frame for as long as
   the loop runs, and this panel is nine hundred pixels tall: it was measured
   at a 50ms median frame, on its own. A transform is handed to the compositor
   and costs nothing per frame. The element is oversized by one tile so the
   translate never drags an edge into view, and `.th` clips it. */
.th::before {
  content: "";
  position: absolute;
  inset: -24px;
  pointer-events: none;
  opacity: 0.5;
  background-image: radial-gradient(circle at 1px 1px, rgba(245, 242, 237, 0.05) 1px, transparent 0);
  background-size: 22px 22px;
  animation: th-drift 26s linear infinite;
}

@keyframes th-drift { to { transform: translate3d(22px, 22px, 0); } }

/* One pass of light on arrival, matched to the other mockups. */
.th::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(105deg, transparent 38%, rgba(255, 255, 255, 0.05) 50%, transparent 62%);
  background-size: 260% 100%;
  background-position: 130% 0;
  opacity: 0;
}

[data-think][data-shown] .th::after { animation: th-sheen 1500ms var(--ease-soft) 700ms; }

@keyframes th-sheen {
  0%   { opacity: 1; background-position: 130% 0; }
  100% { opacity: 1; background-position: -130% 0; }
}

/* --- title bar ----------------------------------------------------------- */

.th__bar {
  position: relative;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 16px clamp(18px, 2.2vw, 28px);
  border-bottom: 1px solid var(--rule);
}

/* The rotation is on this element, not on the arc inside the SVG. A transform
   on an SVG child is not composited: it re-renders the whole SVG every frame,
   forever. On the wrapper it is handed to the compositor. The track underneath
   is a full circle, so rotating the pair looks identical. */
.th__spin {
  width: 14px;
  height: 14px;
  flex: none;
  animation: th-spin 1.1s linear infinite;
}
.th__spin svg { width: 100%; height: 100%; fill: none; stroke-width: 2; }
.th__spin-t { stroke: var(--rule); }

.th__spin-a {
  stroke: var(--th-run);
  stroke-linecap: round;
  stroke-dasharray: 9 27;
}

@keyframes th-spin { to { transform: rotate(360deg); } }

.th__bar-t {
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
}

.th__bar-t em { font-style: normal; color: var(--text); }

.th__clock {
  margin-inline-start: auto;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
  opacity: 0.7;
  font-variant-numeric: tabular-nums;
}

/* --- the failure --------------------------------------------------------- */

.th__subj {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
  padding: 14px clamp(18px, 2.2vw, 28px);
  border-bottom: 1px solid var(--rule);
  background: rgba(0, 0, 0, 0.16);
}

.th__subj-k {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
  opacity: 0.7;
}

.th__subj-v {
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  line-height: 1.5;
  color: var(--text-muted);
}

.th__subj-v b { color: var(--text); font-weight: 400; }

/* --- the theories -------------------------------------------------------- */

.th__list { position: relative; padding: 6px clamp(18px, 2.2vw, 28px) 4px; }

.th__rail {
  position: absolute;
  top: 22px;
  bottom: 22px;
  left: calc(clamp(18px, 2.2vw, 28px) + 7px);
  width: 1px;
  background: linear-gradient(to bottom, transparent, var(--rule) 8%, var(--rule) 92%, transparent);
}

/* The finding in transit. A short comet rather than a dot — the same language
   the pipeline and the seam use for something moving along a track. */
.th__packet {
  position: absolute;
  left: 50%;
  top: 0;
  width: 1px;
  height: 22px;
  margin-inline-start: -0.5px;
  margin-block-start: -11px;
  border-radius: 1px;
  background: linear-gradient(to bottom, transparent, var(--accent), transparent);
  box-shadow: 0 0 10px 1px rgba(228, 117, 37, 0.45);
  opacity: 0;
}

.th__row {
  position: relative;
  display: flex;
  gap: 18px;
  padding: 19px 0;
  border-bottom: 1px solid var(--rule);
}

.th__row:last-child { border-bottom: 0; }

/* --- the state glyph ----------------------------------------------------- */

.th__mark { flex: none; width: 15px; padding-block-start: 4px; }
.th__mark svg { width: 15px; height: 15px; fill: none; stroke-width: 1.6; }

.th__ring { stroke: var(--rule); }

.th__arc {
  stroke: var(--th-run);
  stroke-linecap: round;
  stroke-dasharray: 8 27;
  transform-origin: 50% 50%;
  opacity: 0;
}

.th__tick, .th__dash {
  stroke: var(--ok);
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
  transition: opacity 400ms var(--ease);
}

.th__dash { stroke: var(--text-muted); opacity: 0; }

/* Only the rows that are actually working spin. Four invisible arcs animating
   at opacity 0 still re-render four SVGs on every frame. */
.th__row[data-state="run"] .th__arc {
  opacity: 1;
  animation: th-spin 1.1s linear infinite;
}
.th__row[data-state="ok"]  .th__tick { opacity: 1; }
.th__row[data-state="ok"]  .th__ring { stroke: rgba(60, 195, 31, 0.45); }
.th__row[data-state="out"] .th__dash { opacity: 1; }
.th__row[data-state="out"] .th__ring { stroke: rgba(142, 142, 142, 0.22); }

/* --- the row ------------------------------------------------------------- */

.th__main { position: relative; flex: 1; min-width: 0; }
.th__top { display: flex; align-items: center; gap: 16px; }

/* flex: 0 1 auto, not flex: 1 — so the strike spans the words and not the
   empty run out to the tag. */
.th__name {
  position: relative;
  flex: 0 1 auto;
  min-width: 0;
  font-size: var(--body-lg);
  line-height: 1.35;
  color: var(--text);

  /* A real line-through, so it strikes every line of a wrapped name. It used
     to be an absolutely positioned bar at 52% of the block, which is the
     middle of the text only while the text is one line; on two it drew
     neatly through the gap between them. There is no way to place one box
     through two line boxes, so this is the browser's job. It fades in rather
     than drawing across, which is the cost of getting it right. */
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  text-decoration-color: transparent;
  transition:
    color 500ms var(--ease),
    text-decoration-color 550ms var(--ease);
}

.th__row[data-state="out"] .th__name { text-decoration-color: currentColor; }

/* Kept in the markup, no longer drawn. */
.th__strike { display: none; }

.th__row[data-state="out"] .th__name { color: var(--text-muted); opacity: 0.65; }

.th__tag {
  flex: none;
  margin-inline-start: auto;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--text-muted);
  opacity: 0.75;
  transition: color 450ms var(--ease), opacity 450ms var(--ease);
}

.th__row[data-state="run"] .th__tag { color: var(--th-run); opacity: 1; }
.th__row[data-state="ok"]  .th__tag { color: var(--ok); opacity: 1; }

.th__meta {
  margin-block-start: 9px;
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  color: var(--text-muted);
  transition: opacity 280ms var(--ease);
}

.th__meta.is-swapping { opacity: 0; }
.th__row[data-state="out"] .th__meta { opacity: 0.6; }

/* A thin travelling highlight while a theory is working. */
.th__scan {
  position: absolute;
  left: -33px;
  right: 0;
  bottom: -20px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  transition: opacity 350ms var(--ease);
}

.th__row[data-state="run"] .th__scan { opacity: 1; }

.th__scan::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 34%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(97, 166, 245, 0.75), transparent);
  animation: th-scan 1.9s var(--ease-soft) infinite;
}

@keyframes th-scan {
  0%   { transform: translateX(-110%); }
  100% { transform: translateX(400%); }
}

/* The +1 that lands when a finding arrives from another theory. */
.th__gain {
  /* Bottom-anchored, not 26px from the top. The tag sits vertically centred
     in .th__top, so as soon as a name wraps to two lines that row grows and a
     fixed offset puts "+1 finding" straight through "Investigating". Pinned to
     the bottom it shares the meta line instead, right-aligned against it, and
     the two can never meet however the name wraps. */
  position: absolute;
  right: 0;
  bottom: 1px;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  color: var(--accent);
  opacity: 0;
  pointer-events: none;
  transform: translateY(4px);
}

.th__gain.is-on { animation: th-gain 1150ms var(--ease-soft); }

@keyframes th-gain {
  0%   { opacity: 0; transform: translateY(6px); }
  18%  { opacity: 1; transform: none; }
  70%  { opacity: 1; transform: none; }
  100% { opacity: 0; transform: translateY(-5px); }
}

/* --- footer -------------------------------------------------------------- */

.th__foot {
  position: relative;
  padding: 16px clamp(18px, 2.2vw, 28px) 20px;
  border-top: 1px solid var(--rule);
}

.th__foot p {
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  color: var(--accent-text, #E89158);
  font-variant-numeric: tabular-nums;
}

/* --- narrow -------------------------------------------------------------- */

@media (max-width: 1023px) {
  /* minmax(0, 1fr), not 1fr: a bare track takes its automatic minimum from
     the content's min-content width, and the failure line does not wrap. */
  .think__cols { grid-template-columns: minmax(0, 1fr); gap: 48px; }
  .think__head { font-size: calc(var(--h2) * 1.1); }
}

@media (max-width: 560px) {
  .th__top { flex-wrap: wrap; gap: 6px; }
  .th__tag { order: -1; margin-inline-start: 0; }
  .th__gain { position: static; display: block; margin-block-start: 6px; }
  .th__name { font-size: var(--body); }
}

/* --- reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .think *, .think *::before, .think *::after { animation: none !important; }
  .think [data-lift] { opacity: 1 !important; transform: none !important; }
  .th__name { transition: none; }
  .th__spin-a { opacity: 1; }
}

/* ===== 14-hand.css ============================================================ */
/* =========================================================================
   05 — Hand it over

   Artwork left, argument right. No mockup: the reveal is the section.
   ========================================================================= */

.hand {
  position: relative;
  z-index: 1;
  padding-block: clamp(64px, 8vw, 112px) clamp(80px, 10vw, 140px);

  /* The angle the plates are drawn at, measured off the artwork: the top edge
     of each rhombus rises 1 unit for every 2 it travels. Every straight line
     in this section — the wipe, the light bar — is set from these two, so
     nothing in the reveal cuts across the geometry of the thing it reveals. */
  --iso-rise: 100%;    /* the wipe edge climbs this over 200% of the width */
  --iso-deg: -26.6deg; /* the same slope, as an angle, for the light bar */
}

.hand__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(36px, 5vw, 80px);
  align-items: center;
}

/* --- the copy ------------------------------------------------------------ */

.hand__tx { text-align: start; }
.hand__eyebrow { text-align: start; }

[data-hand][data-shown] .eyebrow__text {
  clip-path: inset(0 -4% 0 -4%);
  opacity: 1;
  filter: blur(0);
  transition:
    clip-path 900ms var(--ease-soft),
    opacity 620ms var(--ease),
    filter 780ms var(--ease);
}

.hand__head {
  margin-block-start: clamp(20px, 2.6vw, 34px);
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: calc(var(--h1) * 0.85);
  line-height: 1.06;
  letter-spacing: -0.022em;
}

.hand__head span { display: block; }

/* =========================================================================
   The artwork
   ========================================================================= */

.hand__art {
  position: relative;
  perspective: 1400px;

  /* Pointer position, 0–100% of the box. 11-hand.js writes these; at rest
     they sit off the top-left so the highlight has somewhere to be parked. */
  --mx: 50%;
  --my: 30%;
  --tilt-x: 0deg;
  --tilt-y: 0deg;
}

.hand__tilt {
  transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  transform-style: preserve-3d;
}

/* The arrival swing. On its own element because the pointer tilt above it is
   driven from script and the idle float below it never stops — three
   transforms that have to compose without ever writing over each other.

   The stack comes up out of the page: turned away, pushed back, and a little
   small, so what lands is an object that was somewhere else rather than an
   image that faded up. */
.hand__in {
  transform: rotateX(19deg) rotateY(-11deg) translateZ(-190px) scale(0.93);
  transform-origin: 50% 62%;
  transform-style: preserve-3d;
}

[data-hand][data-shown] .hand__in {
  transform: none;
  transition: transform 1800ms var(--ease-soft) 90ms;
}

.hand__float { position: relative; }

.hand__img {
  display: block;
  width: 100%;
  height: auto;
  position: relative;
  z-index: 1;
}

/* --- the wipe ------------------------------------------------------------
   A straight edge climbing at the plates' own angle, from below the
   bottom-left corner to above the top-right one. Both states are four-point
   polygons — clip-path interpolates between two polygons only when the point
   counts match — and both are a half-plane: the slanted edge, then the two
   bottom corners, held well outside the box so no corner of the shape is
   ever visible.

   The stack is also blurred and dark under the closed state, so it reads as
   arriving rather than as being uncovered by a shape. */

.hand__stack {
  position: relative;
  clip-path: polygon(-50% 175%, 150% 75%, 150% 330%, -50% 330%);
}

[data-hand][data-shown] .hand__stack {
  clip-path: polygon(-50% -5%, 150% -105%, 150% 330%, -50% 330%);
  transition: clip-path 1700ms var(--ease-soft) 90ms;
}

/* The clip edge has to stay a hard line — it is the light drawing the object,
   and a soft one reads as a fade. So the blur that the stack resolves out of
   sits on a child of the clip rather than on the clip itself, and it is gone
   well before the edge is. */
.hand__res {
  position: relative;
  filter: blur(7px) brightness(0.55) saturate(0.75);
  opacity: 0;
}

[data-hand][data-shown] .hand__res {
  filter: blur(0) brightness(1) saturate(1);
  opacity: 1;
  transition:
    filter 950ms var(--ease) 180ms,
    opacity 420ms linear 90ms;
}

/* --- out of register -----------------------------------------------------
   Two silhouettes of the artwork pulled apart either side of it, in two of
   the four hues the character wave uses. As the light passes they slide back
   into register and go out.

   This is the last of the chromatic separation left on the site. There was a
   lens band at the bottom of the viewport that did the same thing to the whole
   page; it was removed for performance, and this survives because it is two
   masked layers on one object rather than a filter over everything. */

.hand__fringe {
  position: absolute;
  inset: 0;
  pointer-events: none;
  -webkit-mask-image: url("/img/handoff.png");
          mask-image: url("/img/handoff.png");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  filter: blur(10px);
  opacity: 0;
}

.hand__fringe--a {
  background: hsl(174 72% 52%);
  transform: translate(-22px, 14px);
}

.hand__fringe--b {
  background: hsl(318 72% 58%);
  transform: translate(22px, -14px);
}

[data-hand][data-shown] .hand__fringe {
  transform: none;
  opacity: 0;
  animation: hand-register 1750ms var(--ease-soft) 160ms forwards;
}

@keyframes hand-register {
  0%   { opacity: 0;    }
  18%  { opacity: 0.62; }
  70%  { opacity: 0.24; }
  100% { opacity: 0;    }
}

[data-hand][data-shown] .hand__fringe--a,
[data-hand][data-shown] .hand__fringe--b { transition: transform 1650ms var(--ease-soft) 160ms; }

/* --- the wavefront -------------------------------------------------------
   A bar of light travelling the same path as the wipe edge, one beat ahead
   of it. The mask is on this element and the rotation is on the child, so
   the mask stays square with the artwork while the bar turns inside it. */

.hand__front {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  overflow: hidden;
  opacity: 0;
  -webkit-mask-image: url("/img/handoff.png");
          mask-image: url("/img/handoff.png");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

.hand__front i {
  position: absolute;
  left: -60%;
  top: 50%;
  width: 220%;
  height: 118px;
  margin-block-start: -59px;
  background: linear-gradient(180deg,
    transparent 0%,
    rgba(228, 117, 37, 0.55) 30%,
    rgba(255, 240, 225, 1) 48%,
    rgba(255, 255, 255, 1) 53%,
    rgba(228, 117, 37, 0.45) 68%,
    transparent 100%);
  filter: blur(4px);
  transform: rotate(var(--iso-deg)) translateY(560px);
}

[data-hand][data-shown] .hand__front { animation: hand-front-in 1780ms linear 90ms forwards; }
[data-hand][data-shown] .hand__front i { animation: hand-front 1780ms var(--ease-soft) 90ms forwards; }

@keyframes hand-front-in {
  0%   { opacity: 0; }
  8%   { opacity: 1; }
  82%  { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes hand-front {
  0%   { transform: rotate(var(--iso-deg)) translateY(560px); }
  100% { transform: rotate(var(--iso-deg)) translateY(-660px); }
}

/* --- the pointer highlight ----------------------------------------------- */

.hand__spec {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(38% 30% at var(--mx) var(--my),
    rgba(255, 255, 255, 0.30), rgba(255, 255, 255, 0.06) 45%, transparent 72%);
  -webkit-mask-image: url("/img/handoff.png");
          mask-image: url("/img/handoff.png");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  transition: opacity 420ms var(--ease);
}

[data-hand-art][data-hover] .hand__spec { opacity: 1; }

/* --- the glow ------------------------------------------------------------
   A rhombus of accent light behind the stack, brightest as the wipe passes
   the middle of the artwork and then settling to a low glow it keeps. */

.hand__glow {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 76%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%) rotate(45deg) scale(0.2);
  border-radius: 18%;
  /* Drawn soft rather than blurred soft. `filter: blur(46px)` on a box this
     size is a real per-frame cost for something a multi-stop gradient renders
     identically and for free. */
  background: radial-gradient(closest-side,
    rgba(228, 117, 37, 0.34) 0%,
    rgba(228, 117, 37, 0.16) 42%,
    rgba(228, 117, 37, 0.05) 68%,
    transparent 84%);
  opacity: 0;
  pointer-events: none;
}

[data-hand][data-shown] .hand__glow { animation: hand-bloom 2800ms var(--ease-soft) 140ms forwards; }

@keyframes hand-bloom {
  0%   { opacity: 0;    transform: translate(-50%, -50%) rotate(45deg) scale(0.2); }
  42%  { opacity: 0.85; transform: translate(-50%, -50%) rotate(45deg) scale(1.04); }
  100% { opacity: 0.3;  transform: translate(-50%, -50%) rotate(45deg) scale(0.92); }
}

/* Once it has landed it keeps breathing, very slightly. A stack this heavy
   sitting perfectly still reads as a screenshot pasted onto the page. */
[data-hand][data-shown] .hand__float { animation: hand-float 9s ease-in-out 2600ms infinite alternate; }

@keyframes hand-float {
  from { transform: translateY(-7px); }
  to   { transform: translateY(7px); }
}

/* --- the copy's own arrival ---------------------------------------------- */

[data-hand][data-shown] [data-lift] {
  opacity: 1;
  transform: none;
  transition:
    opacity 820ms var(--ease),
    transform 1000ms var(--ease-soft);
}

.hand__head span:nth-child(1) { transition-delay: 240ms; }
.hand__head span:nth-child(2) { transition-delay: 330ms; }
.hand__tx .lead { transition-delay: 440ms; }
.hand__tx .pts > li:nth-child(1) { transition-delay: 520ms; }
.hand__tx .pts > li:nth-child(2) { transition-delay: 590ms; }

/* --- narrow -------------------------------------------------------------- */

@media (max-width: 1023px) {
  .hand__grid { grid-template-columns: minmax(0, 1fr); gap: 52px; }
  .hand__head { font-size: calc(var(--h2) * 1.1); }
  .hand__art { max-width: 520px; margin-inline: auto; }
  .hand__front i { height: 130px; margin-block-start: -65px; }
}

/* --- reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .hand *, .hand *::before, .hand *::after { animation: none !important; }
  .hand [data-lift] { opacity: 1 !important; transform: none !important; }
  .hand__tilt { transform: none !important; }
  .hand__stack { clip-path: none !important; }
  .hand__in { transform: none !important; }
  .hand__res {
    filter: none !important;
    opacity: 1 !important;
  }
  .hand__fringe, .hand__front, .hand__spec { display: none; }
  .hand__glow { opacity: 0.28; transform: translate(-50%, -50%) rotate(45deg) scale(0.92); }
}

/* =========================================================================
   Settling

   Everything that makes the reveal work is dead weight once it has finished,
   and it is not free dead weight: two silhouettes of the artwork carrying a
   1100px mask and a 10px blur, a third masked layer for the light bar, a clip
   path, and a `filter` that still runs a filter pass over the whole stack
   even at blur(0). Left in place they cost a measured 28fps against 42.

   11-hand.js sets `data-settled` when the last of it has run. From that
   moment the section is an image, a glow and a pointer tilt.
   ========================================================================= */

[data-hand][data-settled] .hand__fringe,
[data-hand][data-settled] .hand__front { display: none; }

[data-hand][data-settled] .hand__res {
  filter: none;
  transition: none;
}

[data-hand][data-settled] .hand__stack { clip-path: none; }

/* ===== 15-bed.css ============================================================ */
/* =========================================================================
   The bed

   A neutral, warm-light panel that sits under a dark mockup. Only two
   things in the page get one: the editor in 03 — It Notices and the
   instrument in 04 — How it thinks. Everything else stays on the ground.

   It exists because both of those are dark cards on a dark page, and a dark
   card on a dark page has to be found. Set on sand, the mockup is the only
   dark object inside a light rectangle and the eye goes to it first — which
   is what the sections are for. The panel is deliberately the one warm-light
   surface on the site, so it cannot be mistaken for a component.
   ========================================================================= */

:root {
  --bed:      #EAE4D8;   /* the panel ground */
  --bed-lift: #F6F3EC;   /* its lit corner */
  --bed-edge: rgba(16, 15, 13, 0.10);
}

.bed {
  position: relative;
  isolation: isolate;
  border-radius: 22px;
  padding: clamp(18px, 2.8vw, 44px);
  background:
    radial-gradient(120% 100% at 12% 0%, var(--bed-lift), transparent 62%),
    var(--bed);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    0 1px 0 var(--bed-edge),
    0 50px 90px -50px rgba(0, 0, 0, 0.95);
}

/* The same dot field the artwork's plates are printed with, at the strength
   of paper grain. It keeps the panel from reading as a flat colour swatch. */
.bed::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: -1;
  background-image: radial-gradient(rgba(16, 15, 13, 0.14) 1px, transparent 1px);
  background-size: 7px 7px;
  opacity: 0.38;
  -webkit-mask-image: radial-gradient(130% 110% at 10% 0%, #000, transparent 78%);
          mask-image: radial-gradient(130% 110% at 10% 0%, #000, transparent 78%);
}

/* The mockup keeps its own border and shadow; on sand the shadow finally has
   something to fall on, so it is deepened rather than removed. */
.bed > * { position: relative; z-index: 1; }

.bed .ed,
.bed .th {
  box-shadow:
    0 2px 4px rgba(16, 15, 13, 0.18),
    0 26px 44px -18px rgba(16, 15, 13, 0.55);
}

/* --- narrow -------------------------------------------------------------- */

@media (max-width: 640px) {
  .bed { border-radius: 16px; padding: 12px; }
}

/* ===== 16-points.css ============================================================ */
/* =========================================================================
   Points

   The one shape the argument takes in sections 03, 04 and 05: a short white
   title over a grey line or two, stacked.

   It exists because the alternative — running prose with individual phrases
   flipped from grey to white for emphasis — gives every section a different
   silhouette and no reading order. A reader scanning the page sees a headline
   and then a paragraph they have to actually read to find out whether they
   care. Titled points can be read at a glance and in any order, and the
   colour finally means one thing: white is a claim, grey is the detail
   underneath it.

   Three rules hold across all three sections:
     · everything starts on the same left edge, including copy in a right-hand
       column — one vertical line down the whole page
     · white is the title, grey is the body, and nothing switches mid-sentence
     · the lead line under a headline is one grey sentence, never two
   ========================================================================= */

.lead {
  margin-block-start: clamp(18px, 2.2vw, 28px);
  max-width: 46ch;
  font-size: var(--body-lg);
  line-height: 1.6;
  color: var(--text-muted);
  text-align: start;
}

.pts {
  list-style: none;
  padding: 0;
  margin-block-start: clamp(28px, 3.4vw, 44px);
  max-width: 46ch;
  text-align: start;
}

.pts > li + li { margin-block-start: clamp(22px, 2.4vw, 32px); }

.pts__t {
  font-family: var(--font-sans);
  font-size: 1.0625rem;   /* 17px */
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.005em;
  color: var(--text);
}

.pts__d {
  margin-block-start: 7px;
  font-size: 1rem;        /* 16px */
  line-height: 1.6;
  color: var(--text-muted);
}

/* A quotation of the interface, not emphasis: set in the plain face at the
   body colour, with the mono face doing the work of saying it is a machine
   talking. Nothing here goes white. */
.pts__d cite {
  font-style: normal;
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  color: var(--text-muted);
}

/* ===== 17-before.css ============================================================ */
/* =========================================================================
   Before anyone sees it

   The interlude. Centred, no eyebrow, no number: it is the one sentence the
   page is arguing for, and it is set apart from the numbered sections on
   purpose.
   ========================================================================= */

.before {
  position: relative;
  z-index: 1;
  padding-block: clamp(56px, 7vw, 96px) clamp(64px, 8vw, 110px);
}

.before__in { text-align: center; }

.before__head {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: calc(var(--h1) * 0.85);
  line-height: 1.06;
  letter-spacing: -0.022em;
}

/* No frame. The eye is a drawing on the page ground, not a screenshot in a
   box — a panel around it would make it look like a placeholder for something
   else, and it cost the section a whole viewport of height. Sized so the
   headline, the eye and the sentence all land on screen together. */
.before__frame {
  margin: clamp(24px, 2.8vw, 38px) auto 0;
  width: min(340px, 58vw);
}

/* The one paragraph the section exists for, so it is white: on this page grey
   is the detail under a claim, and there is no claim above this one. */
.before__p {
  margin: clamp(24px, 2.8vw, 38px) auto 0;
  max-width: 56ch;
  font-size: var(--body-lg);
  line-height: 1.62;
  color: var(--text);
}

/* --- the eye -------------------------------------------------------------
   Drawn, not a GIF. A GIF carries one-bit transparency at best, so it always
   arrives as a rectangle of its own background sitting on ours, and there is
   no edge blending that hides it convincingly.

   If a GIF has to be used, drop it in as <img class="eye eye--gif"> and add:

     .eye--gif { mix-blend-mode: screen; }          // for art on BLACK
     .eye--gif { mix-blend-mode: multiply; }        // for art on WHITE

   Screen drops a black background out against a dark ground; multiply drops
   a white one. Either way, mask the edges with
   `mask-image: radial-gradient(closest-side, #000 72%, transparent)` so the
   corners of the frame feather instead of ending on a hard line. */

.eye {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.eye__ball { animation: eye-look 9s var(--ease-soft) infinite; }
.eye__lid  { animation: eye-blink 6.4s linear infinite; }

@keyframes eye-look {
  0%, 14%   { transform: translate(0, 0); }
  22%, 34%  { transform: translate(26px, -5px); }
  42%, 54%  { transform: translate(-24px, 4px); }
  62%, 70%  { transform: translate(9px, 9px); }
  80%, 100% { transform: translate(0, 0); }
}

@keyframes eye-blink {
  0%, 90%    { transform: translateY(0); }
  93%, 95%   { transform: translateY(134px); }
  98%, 100%  { transform: translateY(0); }
}

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

[data-before][data-shown] [data-lift] {
  opacity: 1;
  transform: none;
  transition:
    opacity 820ms var(--ease),
    transform 1000ms var(--ease-soft);
}

.before__frame { transition-delay: 140ms; }
.before__p { transition-delay: 260ms; }

/* --- narrow -------------------------------------------------------------- */

@media (max-width: 1023px) {
  .before__head { font-size: calc(var(--h2) * 1.1); }
}

/* --- reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .eye__ball, .eye__lid { animation: none !important; }
  .before [data-lift] { opacity: 1 !important; transform: none !important; }
}

/* ===== 18-matrix.css ============================================================ */
/* =========================================================================
   06 — What's supported

   Four rows and a link out. Same left edge, same headline scale and the same
   grey lead as every section above it; the only new thing here is the badge.
   ========================================================================= */

.mtx {
  position: relative;
  z-index: 1;
  padding-block: clamp(64px, 8vw, 112px) clamp(72px, 9.3vw, 126px);
}

.mtx__eyebrow { text-align: start; }

[data-mtx][data-shown] .eyebrow__text {
  clip-path: inset(0 -4% 0 -4%);
  opacity: 1;
  filter: blur(0);
  transition:
    clip-path 900ms var(--ease-soft),
    opacity 620ms var(--ease),
    filter 780ms var(--ease);
}

.mtx__head {
  margin-block-start: clamp(20px, 2.6vw, 34px);
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: calc(var(--h1) * 0.85);
  line-height: 1.06;
  letter-spacing: -0.022em;
}

.mtx__head span { display: block; }

/* Wider than the standard lead: this one runs above a full-width table and a
   46ch column above a 1280px rule reads as a stray paragraph. */
.mtx__lead { max-width: 82ch; }

/* --- the rows ------------------------------------------------------------ */

.mtx__rows {
  list-style: none;
  padding: 0;
  margin-block-start: clamp(34px, 4vw, 54px);
  border-block-start: 1px solid var(--rule-soft);
}

.mtx__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 120px minmax(0, 1.7fr) 74px;
  align-items: center;
  gap: clamp(16px, 2.4vw, 34px);
  padding-block: clamp(18px, 2.1vw, 26px);
  border-block-end: 1px solid var(--rule-soft);
}

.mtx__name { color: var(--text); }
.mtx__kind, .mtx__fw { color: var(--text-muted); }
.mtx__fw { text-align: end; }

/* The one number on the row, so it is the one thing with a fill behind it. */
.mtx__badge {
  justify-self: end;
  min-width: 74px;
  padding: 7px 0;
  text-align: center;
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--mono, 13px);
  font-variant-numeric: tabular-nums;
  color: #6FE04F;
  background: rgba(60, 195, 31, 0.15);
  border: 1px solid rgba(60, 195, 31, 0.28);
}

/* Not a score. An adapter that reads a published contract has no accuracy to
   quote, so it does not get the green treatment and borrow one. */
.mtx__badge--direct {
  color: #61A6F5;
  background: rgba(97, 166, 245, 0.14);
  border-color: rgba(97, 166, 245, 0.3);
}

/* --- the link out -------------------------------------------------------- */

.mtx__more { margin-block-start: clamp(28px, 3.2vw, 42px); }

.mtx__link {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--text);
  text-decoration: none;
}

.mtx__arrow {
  color: var(--accent);
  transition: transform var(--dur) var(--ease);
}

.mtx__link:hover .mtx__arrow { transform: translateX(5px); }
.mtx__link:hover { color: var(--accent); }

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

[data-mtx][data-shown] [data-lift] {
  opacity: 1;
  transform: none;
  transition:
    opacity 820ms var(--ease),
    transform 1000ms var(--ease-soft);
}

.mtx__head span { transition-delay: 120ms; }
.mtx__lead { transition-delay: 200ms; }
.mtx__rows > li:nth-child(1) { transition-delay: 280ms; }
.mtx__rows > li:nth-child(2) { transition-delay: 340ms; }
.mtx__rows > li:nth-child(3) { transition-delay: 400ms; }
.mtx__rows > li:nth-child(4) { transition-delay: 460ms; }
.mtx__more { transition-delay: 540ms; }

/* --- narrow --------------------------------------------------------------
   Stacked, the framework list is the row's second line and the badge sits
   beside the name, because the name and its number are the pair worth
   keeping together. */

@media (max-width: 860px) {
  .mtx__head { font-size: calc(var(--h2) * 1.1); }

  .mtx__row {
    grid-template-columns: minmax(0, 1fr) 74px;
    row-gap: 8px;
  }

  .mtx__name { order: 1; }
  .mtx__badge { order: 2; }
  .mtx__kind { order: 3; font-family: var(--font-mono); font-size: var(--mono, 12px); }
  .mtx__fw   { order: 4; grid-column: 1 / -1; text-align: start; }
}

/* --- reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .mtx [data-lift] { opacity: 1 !important; transform: none !important; }
  .mtx__arrow { transition: none; }
}

/* ===== 19-faq.css ============================================================ */
/* =========================================================================
   Frequently asked questions

   Headline left, questions right. The rows are hairlines rather than cards:
   six bordered boxes stacked would be the heaviest object on the page, and
   this is the last thing before the footer.
   ========================================================================= */

.faq {
  position: relative;
  z-index: 1;
  padding-block: clamp(64px, 8vw, 112px) clamp(80px, 10vw, 140px);
}

.faq__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr);
  gap: clamp(36px, 5vw, 84px);
  align-items: start;
}

.faq__head {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: calc(var(--h1) * 0.85);
  line-height: 1.06;
  letter-spacing: -0.022em;
}

/* --- the list ------------------------------------------------------------ */

.faq__list {
  list-style: none;
  padding: 0;
  border-block-start: 1px solid var(--rule-soft);
}

.faq__item { border-block-end: 1px solid var(--rule-soft); }

.faq__q {
  display: flex;
  align-items: center;
  gap: 24px;
  width: 100%;
  padding-block: clamp(20px, 2.3vw, 28px);
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--body-lg);
  line-height: 1.4;
  text-align: start;
  cursor: pointer;
}

.faq__qt { flex: 1; }

.faq__q:hover { color: var(--accent); }
.faq__q:focus-visible { outline: 1px solid var(--accent); outline-offset: 4px; }

/* A plus that turns into a cross. Two bars, one of which rotates out of the
   way — cheaper than swapping glyphs and it can be animated. */
.faq__mk {
  position: relative;
  flex: none;
  width: 15px;
  height: 15px;
}

.faq__mk::before,
.faq__mk::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1.5px;
  margin-block-start: -0.75px;
  background: var(--accent);
  transition: transform 320ms var(--ease-soft);
}

.faq__mk::after { transform: rotate(90deg); }

.faq__item.is-open .faq__mk::before { transform: rotate(45deg); }
.faq__item.is-open .faq__mk::after  { transform: rotate(135deg); }

/* --- the answer ----------------------------------------------------------
   0fr to 1fr, because the answer's height is not a number anyone has, and a
   max-height guess is either a clipped answer or a lazy open. */

.faq__a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 420ms var(--ease-soft);
}

.faq__ain {
  overflow: hidden;
  opacity: 0;
  transition: opacity 260ms var(--ease);
}

.faq__item.is-open .faq__a { grid-template-rows: 1fr; }
.faq__item.is-open .faq__ain { opacity: 1; transition-delay: 120ms; }

.faq__ain p {
  max-width: 62ch;
  padding-block-end: clamp(22px, 2.4vw, 30px);
  padding-inline-end: 39px;
  font-size: 1rem;
  line-height: 1.65;
  color: var(--text-muted);
}

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

[data-faq][data-shown] [data-lift] {
  opacity: 1;
  transform: none;
  transition:
    opacity 820ms var(--ease),
    transform 1000ms var(--ease-soft);
}

.faq__tx .lead { transition-delay: 120ms; }
.faq__list > li:nth-child(1) { transition-delay: 160ms; }
.faq__list > li:nth-child(2) { transition-delay: 220ms; }
.faq__list > li:nth-child(3) { transition-delay: 280ms; }
.faq__list > li:nth-child(4) { transition-delay: 340ms; }
.faq__list > li:nth-child(5) { transition-delay: 400ms; }
.faq__list > li:nth-child(6) { transition-delay: 460ms; }

/* --- narrow -------------------------------------------------------------- */

@media (max-width: 1023px) {
  .faq__grid { grid-template-columns: minmax(0, 1fr); gap: 44px; }
  .faq__head { font-size: calc(var(--h2) * 1.1); }
}

/* --- reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .faq [data-lift] { opacity: 1 !important; transform: none !important; }
  .faq__a, .faq__ain, .faq__mk::before, .faq__mk::after { transition: none !important; }
}

/* ===== 20-cta.css ============================================================ */
/* =========================================================================
   The close

   Centred, like the interlude, and for the same reason: it is the ask, not
   another feature.
   ========================================================================= */

.cta {
  position: relative;
  z-index: 1;
  padding-block: clamp(90px, 11vw, 160px) clamp(96px, 12vw, 170px);

  background: var(--bg);
}

.cta__in {
  position: relative;
  text-align: center;
}

/* --- the sphere ----------------------------------------------------------
   The GIF carries its own transparency: index 0 of its palette is pure black
   and is declared the transparent colour, so the ground of every frame is
   simply not painted.

   It used to be composited with `mix-blend-mode: screen` instead. That worked
   until it met the nav, which is a `backdrop-filter` element: a blended layer
   under a backdrop-filtered one makes Chromium and Safari fall back to an
   opaque black rectangle over the whole blending group, which is exactly the
   black bar that appeared at the top of this section. A blend mode that has
   to negotiate with a backdrop filter is a bug waiting on a scroll position;
   transparency in the file needs no negotiation at all, and costs the
   compositor nothing. */

.cta__sphere {
  position: absolute;
  left: 50%;
  top: 46%;
  width: min(620px, 92vw);
  height: auto;
  transform: translate(-50%, -50%);
  opacity: 0.95;
  pointer-events: none;
  z-index: 0;
  animation: cta-drift 26s ease-in-out infinite alternate;
}

@keyframes cta-drift {
  from { transform: translate(-50%, -50%) scale(1); }
  to   { transform: translate(-50%, -52%) scale(1.05); }
}

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

.cta__head,
.cta__p,
.cta__act { position: relative; z-index: 1; }

.cta__head {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: calc(var(--h1) * 1.05);
  line-height: 1.06;
  letter-spacing: -0.024em;
}

.cta__head span { display: block; }

.cta__p {
  margin: clamp(22px, 2.6vw, 34px) auto 0;
  max-width: 52ch;
  font-size: var(--body-lg);
  line-height: 1.6;
  color: var(--text);
}

/* The release name carries the release bar's ramp, because it is the same
   name and it should look like the same thing. */
.cta__rel {
  background: var(--release-ramp);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}

.cta__act { margin-block-start: clamp(26px, 3vw, 40px); }

.cta__btn { font-size: var(--body-lg); padding: 14px 26px; }

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

[data-cta][data-shown] [data-lift] {
  opacity: 1;
  transform: none;
  transition:
    opacity 820ms var(--ease),
    transform 1000ms var(--ease-soft);
}

.cta__head span:nth-child(1) { transition-delay: 100ms; }
.cta__head span:nth-child(2) { transition-delay: 190ms; }
.cta__p { transition-delay: 300ms; }
.cta__act { transition-delay: 400ms; }

/* --- narrow -------------------------------------------------------------- */

@media (max-width: 1023px) {
  .cta__head { font-size: calc(var(--h2) * 1.25); }
}

/* --- reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .cta__sphere { animation: none; }
  .cta [data-lift] { opacity: 1 !important; transform: none !important; }
}

/* ===== 21-footer.css ============================================================ */
/* =========================================================================
   The footer

   Links, then the brand. The wordmark runs the full width and sits behind the
   meadow, so the hills cut across its lower half: the word rises out of the
   field instead of being parked under it.
   ========================================================================= */

.ft {
  position: relative;
  z-index: 1;
  padding-block-start: clamp(60px, 7vw, 96px);
  background: var(--bg);
}

/* --- the columns --------------------------------------------------------- */

.ft__cols {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(28px, 4vw, 64px);
}

.ft__ct {
  font-family: var(--font-sans);
  font-size: var(--body-sm);
  font-weight: 600;
  color: var(--text-muted);
}

.ft__col ul {
  list-style: none;
  padding: 0;
  margin-block-start: clamp(18px, 2vw, 26px);
}

.ft__col li + li { margin-block-start: 14px; }

.ft__col a {
  color: var(--text);
  text-decoration: none;
  font-size: var(--body-sm);
  transition: color var(--dur) var(--ease);
}

.ft__col a:hover { color: var(--accent); }

/* Not a link, and it should not look like one waiting to be clicked. */
.ft__soon {
  color: var(--text-muted);
  font-size: var(--body-sm);
}

.ft__mail { text-decoration: underline !important; text-underline-offset: 3px; }

.ft__legal {
  margin-block-start: clamp(40px, 5vw, 68px);
  padding-inline: var(--gutter);
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  color: var(--text-muted);
}

/* --- the brand stage -----------------------------------------------------
   Everything here is a percentage of the stage, and the stage's height comes
   from its own width, so the wordmark meets the horizon at the same point at
   every viewport size. The meadow is 1351 of the stage's 1500 units tall; the
   rest is sky. */

.ft__stage {
  position: relative;
  margin-block-start: clamp(18px, 2.4vw, 34px);
  /* 1351 units of meadow and 149 of sky. The sky has to clear the sticky nav:
     at the very bottom of the page this stage fills the viewport, and anything
     parked higher than the lockup ends up behind the nav bar. */
  aspect-ratio: 2400 / 1500;
  overflow: hidden;
}

.ft__meadow {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  width: 100%;
  height: auto;
  display: block;
  z-index: 2;
}

/* One lockup, absolutely placed so it straddles the horizon: the top half
   stands in the sky, the bottom half is in the field. Both halves are masks
   rather than <img>, so they can be filled with a gradient — bright where the
   letters stand up, dimmer where they go into the grass. */
.ft__lock {
  position: absolute;
  left: 50%;
  top: 18%;
  width: min(1200px, 88%);
  transform: translateX(-50%);
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 4%;
}

.ft__mark,
.ft__word {
  display: block;
  background: linear-gradient(180deg,
    #FFFFFF 0%, rgba(255, 255, 255, 0.94) 44%, rgba(255, 255, 255, 0.6) 100%);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

.ft__mark {
  /* Sized off the wordmark, not off the lockup: the mark's drawn height is
     about 62% of the wordmark's, which at these two aspect ratios puts it at
     roughly 30% of the wordmark's width. Anything much under that and a
     289px-tall word sits next to a bird the size of a bullet point. */
  flex: none;
  width: 22%;
  aspect-ratio: 385 / 246;
  -webkit-mask-image: url("/logo/mark.svg");
          mask-image: url("/logo/mark.svg");
}

.ft__word {
  flex: 1;
  aspect-ratio: 576 / 184;
  -webkit-mask-image: url("/logo/wordmark.svg");
          mask-image: url("/logo/wordmark.svg");
}

/* A low warmth behind the word, so the sky is not a flat black rectangle
   above the photograph. */
.ft__glow {
  position: absolute;
  left: 50%;
  top: 26%;
  width: 70%;
  aspect-ratio: 2.4;
  transform: translate(-50%, -50%);
  z-index: 0;
  background: radial-gradient(closest-side, rgba(228, 117, 37, 0.16), transparent 74%);
  filter: blur(50px);
  pointer-events: none;
}

/* The photograph ends on a straight edge at the bottom of the page. This
   carries it back into the ground instead. */
.ft__fade {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 22%;
  z-index: 3;
  background: linear-gradient(180deg, transparent, var(--bg) 82%);
  pointer-events: none;
}

/* --- narrow --------------------------------------------------------------
   Two columns, and the stage grows taller relative to its width so the
   wordmark does not shrink to a hairline. */

@media (max-width: 760px) {
  .ft__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 40px; }
  .ft__stage { aspect-ratio: 1400 / 1120; }
  .ft__meadow { width: 160%; left: -30%; inset-inline: auto; }
  .ft__lock { top: 26%; width: 94%; gap: 4%; }
}

/* --- reduced motion / print ---------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .ft__glow { filter: none; opacity: 0.5; }
}

/* ===== 22-perf.css ============================================================ */
/* =========================================================================
   Rendering budget

   Everything in this file is about what the browser is allowed to skip.

   The page is roughly fifteen viewports tall and several of its sections are
   permanent instruments: canvases, looping mockups, blurred glows, a masked
   photograph. Without help the browser lays out, styles and paints all of it
   on every frame, whether or not any of it is on screen.

   `content-visibility: auto` tells it not to. A section that is nowhere near
   the viewport is skipped entirely — no style, no layout, no paint — and is
   rendered normally the moment it comes near. `contain-intrinsic-size: auto
   <height>` gives the scrollbar a height to work with in the meantime, and
   the `auto` keyword makes the browser remember each section's real height
   once it has rendered it, so the estimate only ever matters once.

   Measured on a straight 5000px scroll: 34fps before, 52fps after, worst
   frame 67ms down to 50ms.

   Two exclusions, and they matter:

   · Sections with sticky or scroll-driven internals — the rail, the seam,
     the gap — are left alone. Skipping the render of a subtree whose layout
     is what the scroll position is reading produces jumps.
   · The hero is always on screen at load, so there is nothing to save.
   ========================================================================= */

.notices,
.think,
.hand,
.before,
.mtx,
.faq,
.cta {
  content-visibility: auto;
  contain-intrinsic-size: auto 900px;
}

.think { contain-intrinsic-size: auto 1500px; }
.before { contain-intrinsic-size: auto 640px; }
.ft { contain-intrinsic-size: auto 1400px; }

/* The footer is the last thing on the page and its meadow is a 2400px image
   under a masked lockup. Skipping it until it is approached is most of the
   cost of the footer. */
.ft { content-visibility: auto; }

/* Anyone who has asked for less motion has already had the canvases turned
   off; leaving content-visibility on as well is free. */

/* ===== 23-feature.css ============================================================ */
/* =========================================================================
   Feature pages — Knowledge, Runtime, Governance

   The LAYOUT is transcribed from three reference pages, section by section:

     Knowledge  <- strand.framer.website/features/ask
     Runtime    <- strand.framer.website/features/cortex
     Governance <- strand.framer.website/features/pulse

   Only the skeleton is borrowed: section order, column counts, which side
   each mockup sits on, where the rules and dividers fall. Every colour,
   typeface, hairline, radius and spacing value below is this site's own, out
   of 01-tokens.css. Nothing of theirs is reproduced.

   The three pages deliberately do NOT share one template, because the three
   references do not. Each has its own hero and its own band order:

     Knowledge   centred hero  -> 3-col band -> full-width media -> two-tone
                 (left) -> 6 items in 3x2 -> statement + 4 figures -> close
     Runtime     left hero, rule, sub-left/CTA-right, rule -> 2-col band ->
                 text-left/media-right -> statement + accordion / media -> close
     Governance  split hero (text left, art right) -> 2-col band -> centred
                 two-tone -> media-left / accordion-right -> close

   Shared primitives live at the top, page-specific blocks below.
   ========================================================================= */

/* --- the frame -----------------------------------------------------------

   The references run a hairline down both sides of the content column for the
   whole page, which is what makes their bands read as one ruled sheet rather
   than as stacked sections. Ours sits just outside the content column.
   ------------------------------------------------------------------------- */

.fx {
  max-width: calc(var(--maxw) + 120px);
  margin-inline: auto;
  border-inline: 1px solid var(--rule-soft);
}

.fx__in {
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: clamp(20px, 3vw, 44px);
}

/* A full-bleed hairline across the frame. The references use these to cut the
   hero away from the sub-copy and from the first band. */
.fx__rule { border-block-start: 1px solid var(--rule-soft); }

/* --- breadcrumb ---------------------------------------------------------- */

.fx__crumb {
  display: flex;
  align-items: center;
  gap: 10px;
  padding-block: 26px 0;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.fx__crumb-sep { color: var(--rule); }
.fx__crumb-now { color: var(--text); }

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

/* The references set their hero H1 far larger than their section heads, and
   break the lines by hand. `text-wrap: initial` overrides the balance rule in
   02-base.css, which would otherwise re-break the authored lines. */
.fx__h1 {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(2.5rem, 5.4vw, 4.75rem);
  line-height: 1.04;
  letter-spacing: -0.012em;
  text-wrap: initial;
}

.fx__sub {
  font-size: var(--body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-muted);
  max-width: 52ch;
}

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

/* --- hero A: centred (Knowledge) ----------------------------------------- */

.fx__hero--center { text-align: center; padding-block: clamp(28px, 3.5vw, 44px) clamp(56px, 7vw, 96px); }
.fx__hero--center .fx__sub { margin-inline: auto; }
.fx__hero--center .fx__actions { justify-content: center; }
.fx__hero--center .fx__h1 { margin-block-start: clamp(18px, 2.2vw, 26px); }
.fx__hero--center .fx__sub { margin-block-start: clamp(18px, 2.2vw, 26px); }
.fx__hero--center .fx__actions { margin-block-start: clamp(24px, 3vw, 34px); }

/* The decorative art above the headline. Its own slot, centred, and the one
   place on these pages where a visual is purely atmospheric. */
.fx__crown { display: flex; justify-content: center; padding-block-start: clamp(24px, 3vw, 40px); }
.fx__crown .ph { width: min(460px, 100%); }

/* --- hero B: left, with rules (Runtime) ---------------------------------- */

/* The art is absolutely positioned into this band's top-right corner, so the
   band has to be tall enough to hold it — otherwise a 360px image hangs out
   of the bottom of a 220px hero and lands on the buttons in the band below.
   The floor matches the art's own clamp ceiling plus its inset. */
.fx__hero--left {
  position: relative;
  min-height: clamp(260px, 32vw, 400px);
  padding-block: clamp(28px, 3.5vw, 48px) clamp(40px, 5vw, 72px);
}
.fx__hero--left .fx__h1 { margin-block-start: clamp(18px, 2.2vw, 26px); max-width: 18ch; }

/* Art parked at the top right of the hero, behind the headline's air. Below
   1024 there is no corner to park in, so it returns to normal flow above the
   chip — see the override in the narrow block. */
.fx__hero-art {
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: clamp(20px, 3vw, 44px);
  width: min(420px, 34vw);
  pointer-events: none;
}

/* Sub-copy on the left, actions on the right, on one row between two rules.
   This is the reference's signature move and the reason the hero needs its
   own band rather than a shared one. */
.fx__hero-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  padding-block: clamp(26px, 3.2vw, 40px);
}

.fx__hero-foot .fx__sub { margin: 0; max-width: 44ch; }

/* --- hero C: split (Governance) ------------------------------------------ */

.fx__hero--split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(32px, 5vw, 80px);
  align-items: center;
  padding-block: clamp(36px, 4.5vw, 64px) clamp(48px, 6vw, 84px);
}

.fx__hero--split .fx__h1 { margin-block-start: clamp(18px, 2.2vw, 26px); max-width: 13ch; }
.fx__hero--split .fx__sub { margin-block-start: clamp(18px, 2.2vw, 26px); }
.fx__hero--split .fx__actions { margin-block-start: clamp(24px, 3vw, 34px); }

/* --- column bands --------------------------------------------------------

   Two or three columns, each holding a mockup and then its caption, with a
   hairline running down between them. The divider is a border on the cell
   rather than a separate element, so it cannot fall out of step with the
   column it divides.
   ------------------------------------------------------------------------- */

.fx__cols {
  display: grid;
  gap: 0;
  padding-block: clamp(40px, 5vw, 72px) clamp(48px, 6vw, 84px);
}

.fx__cols--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.fx__cols--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.fx__cell { padding-inline: clamp(18px, 2.4vw, 34px); }
.fx__cell + .fx__cell { border-inline-start: 1px solid var(--rule-soft); }

.fx__cell-h {
  margin-block-start: clamp(26px, 3.2vw, 40px);
  font-family: var(--font-sans);
  font-size: clamp(1.05rem, 1.35vw, 1.25rem);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.005em;
}

.fx__cell-b {
  margin-block-start: 10px;
  font-size: var(--body);
  line-height: 1.6;
  color: var(--text-muted);
}

/* The dot field that used to sit behind the mockups is gone: a decorative
   field of coloured points under a panel that already carries its own faint
   monochrome texture read as noise, and it fought the panels it was meant to
   support. Mockups sit on the page ground now.
   ------------------------------------------------------------------------- */

/* --- full-width media ---------------------------------------------------- */

.fx__wide { padding-block: clamp(40px, 5vw, 76px); }

/* --- two-tone statement --------------------------------------------------

   One line dim, one bright. The references use it left-aligned on one page
   and centred on another, hence the modifier.
   ------------------------------------------------------------------------- */

.fx__twotone {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1.75rem, 3.2vw, 2.85rem);
  line-height: 1.16;
  letter-spacing: -0.008em;
  text-wrap: initial;
}

.fx__twotone span { display: block; }
.fx__twotone .is-dim { color: var(--text-muted); }
.fx__twotone--center { text-align: center; margin-inline: auto; max-width: 24ch; }

/* A short accent rule under a centred statement, borrowed in position only;
   the ramp is this site's own release gradient. */
.fx__spark {
  margin: clamp(26px, 3.2vw, 40px) auto 0;
  width: 220px;
  height: 2px;
  border-radius: 2px;
  background: var(--release-ramp);
  opacity: 0.75;
}

/* --- six items, 3 x 2 ---------------------------------------------------- */

.fx__six {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(28px, 3.4vw, 46px) clamp(28px, 3.8vw, 56px);
  padding-block: clamp(36px, 4.4vw, 60px) clamp(48px, 6vw, 84px);
}

.fx__item-h {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: var(--mono-lg, 13px);
  font-weight: 500;
}

/* Each of the six carries a line icon drawn to the same 16px grid and 1.3
   stroke, so the row reads as one set rather than six borrowed glyphs. */
.fx__ico {
  flex: none;
  width: 16px;
  height: 16px;
  color: var(--accent);
}

.fx__ico path,
.fx__ico rect,
.fx__ico circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.fx__item-b {
  margin-block-start: 12px;
  font-size: var(--body);
  line-height: 1.6;
  color: var(--text-muted);
}

/* --- figure row ---------------------------------------------------------- */

.fx__figs {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(20px, 2.6vw, 36px);
  padding-block: clamp(36px, 4.4vw, 64px) clamp(52px, 6.5vw, 92px);
}

/* --- the figure marks ----------------------------------------------------

   Four small line drawings, each animating the thing its number counts. No
   ring around them: a circle turns four different drawings into four
   identical buttons, and the reference lets them sit in the air.

   Everything is stroked `currentColor` at 1.2px and loops slowly. They start
   only when the section arrives, so nothing animates off-screen, and reduced
   motion freezes each on a legible frame rather than an empty one.
   ------------------------------------------------------------------------- */

.fx__fig-mark { width: 52px; height: 52px; color: var(--text-muted); }

.fig { width: 100%; height: 100%; overflow: visible; }
.fig line, .fig path, .fig circle { fill: none; stroke: currentColor; stroke-width: 1.2; stroke-linecap: round; }
/* Outlined by default. Only the fifteen nodes are solid: they are dots, and
   a 4px outlined square at this size is a smudge. */
.fig rect { fill: none; stroke: currentColor; stroke-width: 1.2; }
.fig-grid rect { fill: currentColor; stroke: none; }

/* Fifteen — the nodes light in a wave, one type at a time. */
.fig-grid rect { opacity: 0.28; }
[data-shown] .fig-grid rect {
  animation: fig-wave 3.4s var(--ease-soft) infinite;
  animation-delay: calc(var(--n) * 110ms);
}
@keyframes fig-wave {
  0%, 62%, 100% { opacity: 0.28; }
  18% { opacity: 1; color: var(--accent); }
}

/* Ten — ten strokes settling onto one shared line. */
.fig-bars line { transform-origin: 50% 26px; transform: scaleY(0.45); opacity: 0.45; stroke-width: 1.4; }
[data-shown] .fig-bars line {
  animation: fig-settle 3.2s var(--ease-soft) infinite;
  animation-delay: calc(var(--n) * 70ms);
}
@keyframes fig-settle {
  0%, 100% { transform: scaleY(0.4); opacity: 0.4; stroke: currentColor; }
  40%      { transform: scaleY(1);   opacity: 0.95; stroke: var(--accent); }
}

/* The one shared line the ten settle onto. Full strength: at 0.75 over a
   1.2px stroke it read as another grey hairline. */
.fig-rule { stroke: var(--accent); opacity: 1; stroke-width: 1.4; }

/* Two — two contracts sliding together and apart. */
.fig-a, .fig-b { opacity: 0.8; }

/* `.fig rect` sets the stroke and outranks a bare `.fig-b`, so the accent has
   to be qualified the same way or it silently loses. */
.fig rect.fig-b { stroke: var(--accent); opacity: 0.95; }
[data-shown] .fig-a { animation: fig-la 3.6s var(--ease-soft) infinite; }
[data-shown] .fig-b { animation: fig-lb 3.6s var(--ease-soft) infinite; }
@keyframes fig-la { 0%, 100% { transform: translateX(-3px); } 45% { transform: translateX(3px); } }
@keyframes fig-lb { 0%, 100% { transform: translateX(3px); } 45% { transform: translateX(-3px); } }

/* One — two halves closing into a single ring. */
.fig-l, .fig-r { stroke-dasharray: 44; stroke-dashoffset: 44; }
[data-shown] .fig-l, [data-shown] .fig-r { animation: fig-draw 3.8s var(--ease-soft) infinite; }
[data-shown] .fig-r { animation-delay: 120ms; }
@keyframes fig-draw {
  0%       { stroke-dashoffset: 44; }
  45%, 78% { stroke-dashoffset: 0; }
  100%     { stroke-dashoffset: 44; }
}
.fig-seam { fill: var(--accent); stroke: none; opacity: 0; r: 2.6; }
[data-shown] .fig-seam { animation: fig-seam 3.8s var(--ease-soft) infinite; }
@keyframes fig-seam { 0%, 38% { opacity: 0; } 52%, 80% { opacity: 1; } 100% { opacity: 0; } }

/* The ring warms as the two halves meet, and cools as they part. */
@keyframes fig-warm {
  0%, 30%  { stroke: currentColor; }
  52%, 78% { stroke: var(--accent); }
  100%     { stroke: currentColor; }
}
[data-shown] .fig-l { animation: fig-draw 3.8s var(--ease-soft) infinite, fig-warm 3.8s var(--ease-soft) infinite; }
[data-shown] .fig-r { animation: fig-draw 3.8s var(--ease-soft) infinite 120ms, fig-warm 3.8s var(--ease-soft) infinite 120ms; }

@media (prefers-reduced-motion: reduce) {
  [data-shown] .fig-grid rect,
  [data-shown] .fig-bars line,
  [data-shown] .fig-a, [data-shown] .fig-b,
  [data-shown] .fig-l, [data-shown] .fig-r,
  [data-shown] .fig-seam { animation: none; }
  /* frozen on a frame that still reads */
  .fig-grid rect { opacity: 0.65; }
  .fig-bars line { transform: scaleY(0.8); opacity: 0.8; }
  .fig-l, .fig-r { stroke-dashoffset: 0; stroke: var(--accent); }
  .fig-seam { opacity: 1; }
}

.fx__fig-n {
  margin-block-start: clamp(22px, 2.8vw, 34px);
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(2rem, 3.2vw, 2.75rem);
  line-height: 1;
  letter-spacing: -0.01em;
}

.fx__fig-l {
  margin-block-start: 10px;
  font-size: var(--body);
  line-height: 1.5;
  color: var(--text-muted);
}

/* --- media beside a list -------------------------------------------------

   Used twice, mirrored: Runtime puts the text left and the media right,
   Governance puts the media left and the list right.
   ------------------------------------------------------------------------- */

.fx__beside {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(32px, 5vw, 76px);
  align-items: center;
  padding-block: clamp(40px, 5vw, 76px);
}

.fx__beside--media-first .fx__beside-text { order: 2; }
.fx__beside--media-first .fx__beside-media { order: 1; }

/* Text sitting at the foot of its column, which is how the reference aligns a
   short caption against a tall mockup. */
.fx__beside--foot { align-items: end; }

.fx__lede {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1.4rem, 2.1vw, 1.85rem);
  line-height: 1.24;
  letter-spacing: -0.006em;
  max-width: 20ch;
}

/* --- accordion -----------------------------------------------------------

   Rows separated by hairlines, one open at a time, a glyph at the right that
   turns. Same one-open-at-a-time rule as the FAQ, for the same reason: three
   open answers is a wall of text.
   ------------------------------------------------------------------------- */

.fx__acc { border-block-start: 1px solid var(--rule-soft); }

.fx__acc-item {
  position: relative;
  border-block-end: 1px solid var(--rule-soft);
}

/* The progress bar. A pseudo-element sitting on the item's own bottom rule, so
   it needs no markup and cannot fall out of step with the row it belongs to.
   `transform: scaleX` rather than an animated width: width relayouts the row
   every frame, transform is handed to the compositor and costs nothing.

   14-feature.js sets --fx-p and the transition duration; CSS only says what
   the bar looks like. */
.fx__acc-item::after {
  content: "";
  position: absolute;
  inset-block-end: -1px;
  inset-inline: 0;
  height: 2px;
  background: var(--accent);
  transform: scaleX(var(--fx-p, 0));
  transform-origin: left;
  pointer-events: none;
  /* The transition lives HERE, not in an inline style on the item: an inline
     style cannot reach a pseudo-element, so setting it on the row did nothing
     and the bar snapped to full instead of running. Custom properties DO
     inherit into ::after, so the script sets --fx-d and --fx-p on the row and
     both arrive. */
  transition: transform var(--fx-d, 0ms) linear;
}

.fx__acc-q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding-block: clamp(18px, 2.2vw, 26px);
  text-align: start;
  font-size: clamp(1.05rem, 1.35vw, 1.25rem);
  font-weight: 600;
  line-height: 1.3;
  color: var(--text-muted);
  transition: color var(--dur) var(--ease);
}

.fx__acc-q:hover { color: var(--text); }
.fx__acc-item.is-open .fx__acc-q { color: var(--text); }

.fx__acc-mark {
  flex: none;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--text-muted);
  transition: transform var(--dur) var(--ease), color var(--dur) var(--ease);
}

.fx__acc-item.is-open .fx__acc-mark { transform: rotate(45deg); color: var(--accent); }

/* While the sequence is running the rows are not buttons anyone needs to
   press, but they still are: clicking one takes it over and stops the
   advance. The cursor says so. */
[data-acc][data-auto] .fx__acc-q { cursor: pointer; }

/* Height is animated from a measured pixel value set by 14-feature.js, not
   from `auto`, which does not transition. */
.fx__acc-a {
  overflow: hidden;
  height: 0;
  transition: height var(--dur-slow) var(--ease);
}

.fx__acc-a p {
  padding-block-end: clamp(18px, 2.2vw, 26px);
  max-width: 46ch;
  font-size: var(--body);
  line-height: 1.62;
  color: var(--text-muted);
}

/* --- closing CTA --------------------------------------------------------- */

.fx__close {
  position: relative;
  text-align: center;
  padding-block: clamp(90px, 11vw, 170px);
  border-block-start: 1px solid var(--rule-soft);
}

.fx__close-head {
  margin-inline: auto;
  max-width: 16ch;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(2rem, 3.6vw, 3.25rem);
  line-height: 1.12;
  letter-spacing: -0.01em;
}

.fx__close-sub {
  margin: clamp(18px, 2.2vw, 26px) auto 0;
  max-width: 44ch;
  font-size: var(--body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-muted);
}

.fx__close .btn { margin-block-start: clamp(24px, 3vw, 36px); }


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

   Decorative only: the three hero images carry no claim and name nothing, so
   they are aria-hidden and the page still reads correctly without them.
   ------------------------------------------------------------------------- */

.fx__art {
  margin: 0;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--rule-soft);
}

.fx__art img {
  display: block;
  width: 100%;
  height: var(--art-h, 300px);
  object-fit: cover;
  object-position: center;
}

/* --- placeholders --------------------------------------------------------

   Every visual on these pages is a placeholder, by decision. A placeholder
   states its ID and what the slot has to communicate, and reserves the real
   height so the page can be judged as a layout rather than as a list of gaps.

   Not aria-hidden: the finished mockups that replace them must be, but while
   a slot is a written description of pending work, hiding it from a screen
   reader hides the only thing in it. Replace one by deleting the .ph, putting
   the real panel in, and giving it aria-hidden="true".
   ------------------------------------------------------------------------- */

.ph { margin: 0; }

.ph__frame {
  min-height: var(--ph-h, 280px);
  padding: clamp(18px, 2.2vw, 28px);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 10px;
  border: 1px dashed var(--rule);
  border-radius: var(--radius);
}

.ph--plain .ph__frame { border-radius: var(--radius); }

.ph__tag {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: var(--mono-sm, 11px);
}

.ph__id { color: var(--accent); }
.ph__kind { color: var(--text-muted); }

.ph__note {
  max-width: 54ch;
  font-size: var(--body);
  line-height: 1.55;
  color: var(--text-muted);
}

.ph__cap {
  margin-block-start: 12px;
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  line-height: 1.5;
  color: var(--text-muted);
}

/* --- entrances ----------------------------------------------------------- */

[data-fp] [data-lift] { opacity: 0; transform: translateY(14px); }

[data-fp][data-shown] [data-lift] {
  opacity: 1;
  transform: none;
  transition:
    opacity 520ms var(--ease) calc(var(--i, 0) * 70ms),
    transform 620ms var(--ease) calc(var(--i, 0) * 70ms);
}

/* --- responsive ---------------------------------------------------------- */

@media (max-width: 1000px) {
  .fx__cols--2, .fx__cols--3 { grid-template-columns: minmax(0, 1fr); gap: clamp(40px, 6vw, 64px); }
  /* Stacked, a left border is a stray line down the page, not a divider. */
  .fx__cell + .fx__cell { border-inline-start: 0; border-block-start: 1px solid var(--rule-soft); padding-block-start: clamp(32px, 5vw, 48px); }
  .fx__cell { padding-inline: 0; }

  .fx__hero--split { grid-template-columns: minmax(0, 1fr); }
  /* The art is absolutely positioned into the hero's top-right corner,
     which is a wide-viewport composition — stacked there is no corner to sit
     in. It was hidden outright, which lost the image entirely on every phone
     and tablet. It returns to normal flow above the chip instead, matching
     what Knowledge does with its own crown. */
  .fx__hero-art {
    position: static;
    width: auto;
    margin-block-end: 24px;
  }
  .fx__hero-foot { flex-direction: column; align-items: flex-start; gap: 24px; }

  .fx__beside,
  .fx__beside--media-first { grid-template-columns: minmax(0, 1fr); }
  /* Mirroring is a desktop idea: stacked, every band reads text then media. */
  .fx__beside--media-first .fx__beside-text { order: 0; }
  .fx__beside--media-first .fx__beside-media { order: 0; }

  .fx__six { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .fx__figs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .fx { border-inline: 0; }
}

@media (max-width: 640px) {
  .fx__six, .fx__figs { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  [data-fp] [data-lift] { opacity: 1 !important; transform: none !important; }
  .fx__acc-a { transition: none; }
}

/* ===== 24-waitlist.css ============================================================ */
/* =========================================================================
   /waitlist

   Layout transcribed from strand.framer.website/pricing, which is the page
   this one replaces: a left headline on its own ruled row, then three columns
   divided by hairlines inside a ruled frame. Their cards are not boxes, they
   are columns between rules, which is why this reuses .fx__cols.

   Two deliberate departures, both from the build guide §5.5:
     - the Monthly/Yearly toggle is removed, and nothing replaces it
     - the three per-card buttons become ONE shared form, above the columns
   ========================================================================= */

.wl__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  padding-block: clamp(30px, 3.6vw, 48px);
}

.wl__h1 {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(2.25rem, 4.2vw, 3.5rem);
  line-height: 1.06;
  letter-spacing: -0.012em;
}

.wl__sub {
  max-width: 46ch;
  font-size: var(--body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-muted);
}

/* --- the form ------------------------------------------------------------ */

.wl__form-wrap { padding-block: clamp(30px, 3.8vw, 52px); }

.wl__form {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  max-width: 640px;
}

/* The supplied banner. Fixed aspect from the intrinsic width/height on the
   element itself, so it reserves its own space and cannot shift the layout as
   it decodes. */
.wl__art {
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--rule);
}

.wl__field {
  flex: 1 1 200px;
  height: 44px;
  padding-inline: 16px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 10px;
  font-size: 15px;
  color: var(--text);
  transition: border-color var(--dur) var(--ease);
}

.wl__field::placeholder { color: var(--text-muted); }
.wl__field:focus { outline: none; border-color: var(--accent); }
.wl__field:disabled { opacity: 0.6; }

.wl__submit { flex: none; min-width: 150px; position: relative; }
.wl__submit[disabled] { opacity: 0.75; cursor: default; }

/* The honeypot. Off-screen rather than display:none, because some bots skip
   fields they can tell are hidden. Not focusable, not announced. */
.wl__pot {
  position: absolute;
  inset-inline-start: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.wl__micro {
  margin-block-start: 14px;
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  line-height: 1.5;
  color: var(--text-muted);
}

/* One region for both outcomes, so a screen reader hears exactly one message
   and the layout does not jump between two boxes. */
.wl__msg {
  margin-block-start: 16px;
  max-width: 520px;
  font-size: var(--body);
  line-height: 1.6;
}

.wl__msg:empty { margin-block-start: 0; }
.wl__msg[data-state="ok"] { color: var(--ok); }
.wl__msg[data-state="error"] { color: var(--warn); }

/* Success replaces the form, per the guide. */
.wl__form-wrap[data-done] .wl__form,
.wl__form-wrap[data-done] .wl__micro { display: none; }

.wl__form-wrap[data-done] .wl__msg {
  font-family: var(--font-serif);
  font-size: clamp(1.25rem, 1.9vw, 1.6rem);
  line-height: 1.3;
  color: var(--text);
}

.wl__spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  margin-inline-end: 8px;
  border: 2px solid currentColor;
  border-block-start-color: transparent;
  border-radius: 50%;
  animation: wl-spin 620ms linear infinite;
  vertical-align: -2px;
}

@keyframes wl-spin { to { transform: rotate(360deg); } }

/* --- the three columns --------------------------------------------------- */

.wl__card-t {
  font-family: var(--font-sans);
  font-size: 1.05rem;
  font-weight: 600;
  line-height: 1.3;
}

.wl__card-b {
  margin-block-start: 10px;
  font-size: var(--body);
  line-height: 1.6;
  color: var(--text-muted);
}

@media (max-width: 1000px) {
  .wl__head { flex-direction: column; align-items: flex-start; gap: 16px; }
}

@media (prefers-reduced-motion: reduce) {
  .wl__spinner { animation-duration: 2s; }
}


/* --- the card illustrations ----------------------------------------------

   One line drawing above each benefit, sitting in the column's air rather
   than in a frame: the reference hangs them over the dividing rules, and a
   box round them would turn three drawings into three more cards.

   A fixed height with `object-fit: contain` so all three sit on the same
   baseline whatever their own aspect ratio, and so the row cannot shift as
   they decode.
   ------------------------------------------------------------------------- */

.wl__ill {
  margin: 0 0 clamp(26px, 3.2vw, 40px);
  display: flex;
  justify-content: center;
}

.wl__ill img {
  width: 100%;
  max-width: 300px;
  height: clamp(120px, 13vw, 168px);
  object-fit: contain;
  object-position: center bottom;
}

@media (max-width: 1000px) {
  .wl__ill img { height: 128px; }
}

/* ===== 25-mock.css ============================================================ */
/* =========================================================================
   Feature-page mockups

   Built to the reference's construction, which is the thing the first two
   attempts got wrong:

   THERE IS NO PANEL. The reference never wraps a mockup in a bordered box
   with a title bar. It floats two or three cards on a faint dotted field, at
   different sizes, overlapping, with air around them. A framed component with
   a header row is what a dashboard template looks like; loose floating pieces
   are what a fragment of real software looks like. Everything below follows
   from that.

     fmk-stage      transparent, no border, one faint monochrome dot grid,
                a fixed height so mockups sharing a row fmk-line up
     card       18px radius, 1px --rule, near-black fill, real shadow
     overlap    at least one element sits across another's edge, offset from
                the grid. Perfect alignment reads as a diagram
     type       labels in sans at body size, values in mono 13px. Nothing at
                10px, nothing letter-spaced, sentence case throughout
     status     a glyph and a word. Never colour alone

   The fmk-stage height is fixed per band so three mockups in a row end at the
   same fmk-line and their captions align. The card inside is free to be any size.
   ========================================================================= */

:root { --fm-run: #61A6F5; }

.fmk-stage {
  /* The @container rules below query THIS, so the offsets and floating pills
     switch on the panel's own width. Three mockups share a row on Knowledge
     at ~330px each, while Runtime and Governance give theirs ~530px. */
  container-type: inline-size;
  position: relative;
  overflow: visible;
  display: grid;
  place-items: center;
  min-height: var(--stage-h, 340px);
  align-content: center;
  padding: 24px;
  border-radius: var(--radius);
  /* The reference's field: monochrome, one pixel, barely there. */
  background-image: radial-gradient(rgba(245, 242, 237, 0.055) 1px, transparent 1px);
  background-size: 20px 20px;
}

.fmk-stage-in { position: relative; width: 100%; max-width: 400px; }

/* --- the card ------------------------------------------------------------ */

.fmk-c {
  position: relative;
  border: 1px solid var(--rule);
  border-radius: 18px;
  background: #121110;
  box-shadow: 0 24px 50px -24px rgba(0, 0, 0, 0.95);
  padding: 18px 20px;
}

.fmk-c--tight { padding: 14px 16px; }

/* The offsets. Whole-pixel nudges off the grid, which is what stops a
   composition reading as a table. */
/* Offsets and floating pills are a WIDE-ONLY idea, and this is the lesson
   that cost two rebuilds: a composition that overlaps needs room to overlap
   into. Three of these mockups share a row on Knowledge, which leaves each
   about 330px, and there a negative margin simply puts one card's text
   through another's and an absolutely-placed fmk-pill lands on a heading.

   So: stacked and inline by default, floating only once the container proves
   it has the width. Narrow is the common case, not the fallback. */
.fmk-c--up, .fmk-c--right, .fmk-c--left { margin-inline: 0; }

@container (min-width: 460px) {
  .fmk-c--up { margin-block-start: -12px; margin-inline-start: 24px; z-index: 2; }
  .fmk-c--right { margin-inline-start: 22px; }
  .fmk-c--left { margin-inline-end: 22px; }
}

.fmk-c + .fmk-c { margin-block-start: 12px; }

/* --- pieces -------------------------------------------------------------- */

.fmk-p-lab { font-size: var(--body); color: var(--text-muted); }
.fmk-p-val { font-family: var(--font-mono); font-size: var(--mono-lg, 13px); color: var(--text); }
.fmk-p-name { font-size: var(--body); color: var(--text); }
.fmk-p-mono { font-family: var(--font-mono); font-size: var(--mono-lg, 13px); color: var(--text-muted); }

.fmk-p-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.fmk-p-row + .fmk-p-row { margin-block-start: 13px; }

.fmk-p-head { font-size: clamp(1.1rem, 1.5vw, 1.3rem); color: var(--text); letter-spacing: -0.005em; }
.fmk-p-sub { margin-block-start: 5px; font-size: var(--body); color: var(--text-muted); line-height: 1.45; }

/* A floating fmk-pill, the reference's most-used accent. It is a child of the
   card it annotates, not of the fmk-stage: absolute offsets measured from the
   fmk-stage do not survive the column getting narrower, and the fmk-pill ends up
   across somebody's text. Anchored to its own card, it is always right. */
.fmk-pill {
  position: static;
  z-index: 3;
  margin-block-end: 12px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: #191817;
  box-shadow: 0 14px 30px -12px rgba(0, 0, 0, 0.95);
  font-size: var(--body);
  color: var(--text);
  white-space: nowrap;
}

.fmk-pill--mono { font-family: var(--font-mono); font-size: var(--mono-lg, 13px); }
.fmk-pill i { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.fmk-pill span { color: var(--text); }
.fmk-pill--ok { color: var(--ok); }
.fmk-pill--run { color: var(--fm-run); }
.fmk-pill--accent { border-color: rgba(228, 117, 37, 0.55); color: var(--accent); }

/* Floating, once there is room to float into. */
@container (min-width: 460px) {
  /* `width: max-content` is not decoration: an absolutely positioned box with
     both insets on one axis stretches to fill it, and a stray inline
     `inset-inline-start` next to the `inset-inline-end` here turned a pill
     into a full-width blob. This makes that impossible. */
  .fmk-pill {
    position: absolute;
    margin: 0;
    width: max-content;
    height: max-content;
    inset-block-start: -15px;
    inset-inline-end: 14px;
  }
  .fmk-pill--bl { inset-block-start: auto; inset-inline-end: auto; inset-block-end: -15px; inset-inline-start: 16px; }
}

/* a small square glyph fmk-tile, like the reference's source icons */
.fmk-tile {
  width: 34px; height: 34px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--rule);
  border-radius: 9px;
  background: rgba(255, 255, 255, 0.03);
}

.fmk-tile svg { width: 15px; height: 15px; fill: none; stroke: var(--text-muted); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }

/* the dashed ochre connector */
.fmk-wire { position: relative; height: 34px; margin-inline-start: 30px; border-inline-start: 1px dashed var(--accent); }
.fmk-wire::after {
  content: ""; position: absolute; inset-block-end: 0; inset-inline-start: -3.5px;
  border-inline: 4px solid transparent; border-block-start: 6px solid var(--accent);
}

/* a spinner arc, never a bare coloured dot */
.fmk-spin { width: 13px; height: 13px; flex: none; animation: fm-spin 1.1s linear infinite; }
.fmk-spin svg { width: 100%; height: 100%; fill: none; stroke-width: 2; }
.fmk-spin-t { stroke: var(--rule); }
.fmk-spin-a { stroke: var(--fm-run); stroke-linecap: round; stroke-dasharray: 9 27; }
@keyframes fm-spin { to { transform: rotate(360deg); } }

/* --- specific bits ------------------------------------------------------- */

.fmk-node {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 14px;
  border: 1px solid var(--rule);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.02);
  font-size: var(--body);
  color: var(--text-muted);
}

.fmk-node + .fmk-node { margin-block-start: 8px; }
.fmk-node--typed { color: var(--text); border-color: rgba(228, 117, 37, 0.45); }
.fmk-node-k { flex: none; min-width: 60px; font-family: var(--font-mono); font-size: var(--mono, 12px); color: var(--text-muted); }
.fmk-node--typed .fmk-node-k { color: var(--accent); }

.fmk-chipset { display: flex; flex-wrap: wrap; gap: 7px; }
.fmk-chipset span {
  padding: 7px 12px; border: 1px solid var(--rule); border-radius: 8px;
  background: rgba(255, 255, 255, 0.02);
  font-size: var(--body); color: var(--text-muted);
}
.fmk-chipset span.fmk-is-x { border-color: rgba(228, 117, 37, 0.5); color: var(--text); }

.fmk-line { display: flex; align-items: baseline; gap: 14px; opacity: 0; }
.fmk-line + .fmk-line { margin-block-start: 12px; }
[data-shown] .fmk-line { animation: fm-in 360ms var(--ease) forwards; animation-delay: var(--d, 0ms); }
@keyframes fm-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }
.fmk-line-k { flex: none; min-width: 66px; font-family: var(--font-mono); font-size: var(--mono, 12px); color: var(--text-muted); }
.fmk-line-m { font-size: var(--body); line-height: 1.4; color: var(--text-muted); }
.fmk-line-m b { font-weight: 400; color: var(--text); }
.fmk-line-m i { font-style: normal; color: var(--warn); }
.fmk-line-m u { text-decoration: none; color: var(--ok); }

.fmk-thr { display: flex; align-items: center; gap: 13px; padding-block: 12px; }
.fmk-thr + .fmk-thr { border-block-start: 1px solid var(--rule); }
.fmk-thr-g { flex: none; width: 16px; }
.fmk-thr-g svg { width: 16px; height: 16px; fill: none; stroke-width: 1.6; }
.fmk-thr-ring { stroke: var(--rule); }
.fmk-thr-arc { stroke: var(--fm-run); stroke-linecap: round; stroke-dasharray: 9 30; opacity: 0; transform-origin: 50% 50%; }
.fmk-thr-tick { stroke: var(--ok); opacity: 0; stroke-linecap: round; stroke-linejoin: round; }
.fmk-thr-flat { stroke: var(--text-muted); opacity: 0; stroke-linecap: round; }
[data-s="ok"] .fmk-thr-tick { opacity: 1; }
[data-s="ok"] .fmk-thr-ring { stroke: rgba(60, 195, 31, 0.45); }
[data-s="run"] .fmk-thr-arc { opacity: 1; animation: fm-spin 1.1s linear infinite; }
[data-s="out"] .fmk-thr-flat { opacity: 1; }
[data-s="out"] .fmk-thr-ring { stroke: rgba(142, 142, 142, 0.22); }
.fmk-thr-n { flex: 1; min-width: 0; font-size: var(--body); color: var(--text); }
[data-s="out"] .fmk-thr-n { color: var(--text-muted); opacity: 0.55; text-decoration: line-through; }
.fmk-thr-t { flex: none; font-family: var(--font-mono); font-size: var(--mono, 12px); color: var(--text-muted); }
[data-s="ok"] .fmk-thr-t { color: var(--ok); }
[data-s="run"] .fmk-thr-t { color: var(--fm-run); }

.fmk-el-btn {
  padding: 9px 16px; border: 1px solid var(--rule); border-radius: 9px;
  font-size: var(--body); color: var(--text);
  outline: 2px solid var(--accent); outline-offset: 3px;
  transition: border-radius 500ms var(--ease), padding 500ms var(--ease), background-color 500ms var(--ease);
}
[data-alt] .fmk-el-btn { border-radius: 999px; padding: 11px 22px; background: var(--hover); }
[data-alt] .fmk-c--ui { border-radius: 4px; }
.fmk-c--ui { transition: border-radius 500ms var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .fmk-spin, .fmk-thr-arc { animation: none; }
  .fmk-line { opacity: 1 !important; animation: none !important; }
}


/* =========================================================================
   Motion

   Two mockups a page, and only two. Motion is an accent: on every panel it
   stops meaning anything and the page starts to twitch. Each plays ONCE on
   entry, driven by `data-shown` from 16-mock.js, and each shows something the
   still frame cannot — a chain resolving, a theory being ruled out, a
   category being decided. Nothing loops except the two spinners.

   Knowledge   K-01 the chain resolves downward · K-04 the inputs settle
   Runtime     R-01 the run narrates itself · R-03 the fragment restyles
   Governance  G-02 two theories get dropped · G-03 the badge lands last

   The markup ships in its FINISHED state. These rules rewind it, so a browser
   that never runs the script, or one honouring reduced motion, sees the
   settled panel rather than an empty box.
   ========================================================================= */

/* --- K-01: the chain resolves top to bottom ------------------------------ */

[data-motion="k01"] .fmk-wire { transform: scaleY(0); transform-origin: top; }
[data-motion="k01"] .fmk-c--right,
[data-motion="k01"] .fmk-pill { opacity: 0; transform: translateY(6px); }

[data-motion="k01"][data-shown] .fmk-wire {
  transform: scaleY(1);
  transition: transform 420ms var(--ease) 260ms;
}

[data-motion="k01"][data-shown] .fmk-c--right {
  opacity: 1; transform: none;
  transition: opacity 420ms var(--ease) 700ms, transform 480ms var(--ease) 700ms;
}

[data-motion="k01"][data-shown] .fmk-pill {
  opacity: 1; transform: none;
  transition: opacity 380ms var(--ease) 1000ms, transform 420ms var(--ease) 1000ms;
}

/* --- K-04: the inputs settle, then the vocabulary resolves --------------- */

[data-motion="k04"] .fmk-chipset span { opacity: 0; transform: translateY(5px); }
[data-motion="k04"] .fmk-c--right { opacity: 0; transform: translateY(6px); }

[data-motion="k04"][data-shown] .fmk-chipset span {
  opacity: 1; transform: none;
  transition: opacity 320ms var(--ease) var(--d, 0ms), transform 360ms var(--ease) var(--d, 0ms);
}

[data-motion="k04"][data-shown] .fmk-c--right {
  opacity: 1; transform: none;
  transition: opacity 420ms var(--ease) 760ms, transform 460ms var(--ease) 760ms;
}

/* --- G-02: two theories are ruled out ------------------------------------
   The row ships as `data-s="out"`; the script sets it to "run" first and
   hands it back after its own delay, so you watch it being dropped. */

[data-motion="g02"] .fmk-thr-n,
[data-motion="g02"] .fmk-thr-t {
  transition: color 500ms var(--ease), opacity 500ms var(--ease),
              text-decoration-color 550ms var(--ease);
}

[data-motion="g02"] [data-s="out"] .fmk-thr-n {
  text-decoration-color: currentColor;
}

/* --- G-03: the badge lands after the observations, never before ---------- */

[data-motion="g03"] .fmk-pill--ok { opacity: 0; transform: scale(0.94); }
[data-motion="g03"] .fmk-c--up { opacity: 0; transform: translateY(7px); }

[data-motion="g03"][data-shown] .fmk-pill--ok {
  opacity: 1; transform: none;
  transition: opacity 340ms var(--ease) 620ms, transform 420ms var(--ease-soft) 620ms;
}

[data-motion="g03"][data-shown] .fmk-c--up {
  opacity: 1; transform: none;
  transition: opacity 420ms var(--ease) 980ms, transform 460ms var(--ease) 980ms;
}

@media (prefers-reduced-motion: reduce) {
  [data-motion] .fmk-wire { transform: none !important; }
  [data-motion] .fmk-c--right,
  [data-motion] .fmk-c--up,
  [data-motion] .fmk-pill,
  [data-motion] .fmk-chipset span {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* =========================================================================
   Phones

   --stage-h is set per band so three mockups in a row end level. On one
   column that alignment job disappears, and a stage sized for a 530px panel
   leaves a tall band of empty dot field above a small card. Below 700px the
   stage hugs its content instead.
   ========================================================================= */

@media (max-width: 699px) {
  .fmk-stage {
    min-height: 0;
    padding: 18px 0;
    /* Nothing is off to the side any more, so the field only draws where
       there is something to sit on. */
    background-image: none;
  }

  .fmk-stage-in { max-width: none; }
}

/* ===== 26-support.css ============================================================ */
/* =========================================================================
   /support-matrix

   Layout transcribed from strand.framer.website/integrations: a left two-tone
   headline, a search field, a row of filter pills, then a card grid divided
   by hairlines, three across. Only the skeleton; every token is ours.

   The grid is divided by borders on the cells rather than by gaps, which is
   the same construction the pricing page uses for its columns and the reason
   the page reads as one ruled sheet.
   ========================================================================= */

.sup__head { padding-block: clamp(34px, 4.4vw, 62px) clamp(26px, 3.2vw, 40px); }

.sup__h1 {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(2.25rem, 4.6vw, 3.75rem);
  line-height: 1.06;
  letter-spacing: -0.012em;
  text-wrap: initial;
}

.sup__h1 span { display: block; }
.sup__h1 .is-dim { color: var(--text-muted); }

.sup__sub {
  margin-block-start: clamp(18px, 2.2vw, 26px);
  max-width: 62ch;
  font-size: var(--body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-muted);
}

/* --- search --------------------------------------------------------------- */

.sup__find {
  position: relative;
  margin-block-start: clamp(26px, 3.2vw, 40px);
  max-width: 340px;
}

.sup__find svg {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 14px;
  transform: translateY(-50%);
  width: 15px; height: 15px;
  fill: none; stroke: var(--text-muted); stroke-width: 1.6;
  pointer-events: none;
}

.sup__input {
  width: 100%;
  height: 44px;
  padding-inline: 40px 16px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 10px;
  font-size: 15px;
  color: var(--text);
  transition: border-color var(--dur) var(--ease);
}

.sup__input::placeholder { color: var(--text-muted); }
.sup__input:focus { outline: none; border-color: var(--accent); }

/* --- filters -------------------------------------------------------------- */

.sup__filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding-block: clamp(24px, 3vw, 38px);
}

.sup__f {
  padding: 9px 16px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-size: var(--body);
  color: var(--text-muted);
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.sup__f:hover { color: var(--text); border-color: var(--rule); }

.sup__f[aria-pressed="true"] {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}

/* --- the grid ------------------------------------------------------------- */

.sup__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border-block-start: 1px solid var(--rule-soft);
}

.sup__card {
  position: relative;
  padding: clamp(22px, 2.8vw, 34px) clamp(18px, 2.2vw, 28px);
  border-block-end: 1px solid var(--rule-soft);
  min-width: 0;
}

/* --- hover ---------------------------------------------------------------

   The cell fills with a faint point field and grows four corner brackets.
   Two pseudo-elements do all of it, so the hover adds no nodes and nothing
   moves: no lift, no scale, no shadow, which is the rule for cards here.

   The brackets are one element, drawn with a conic gradient masked to the
   four corners: four separate absolutely positioned L shapes would be four
   more boxes per card, times twelve cards.
   ------------------------------------------------------------------------- */

.sup__card::before,
.sup__card::after {
  content: "";
  position: absolute;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

/* the point field */
.sup__card::before {
  inset: 0;
  background-image: radial-gradient(rgba(245, 242, 237, 0.075) 1px, transparent 1px);
  background-size: 14px 14px;
}

/* the corner brackets */
.sup__card::after {
  inset: 10px;
  border: 1px solid var(--accent);
  -webkit-mask-image:
    linear-gradient(#000 0 0), linear-gradient(#000 0 0),
    linear-gradient(#000 0 0), linear-gradient(#000 0 0);
  -webkit-mask-size: 12px 12px;
  -webkit-mask-position: 0 0, 100% 0, 0 100%, 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-image:
    linear-gradient(#000 0 0), linear-gradient(#000 0 0),
    linear-gradient(#000 0 0), linear-gradient(#000 0 0);
  mask-size: 12px 12px;
  mask-position: 0 0, 100% 0, 0 100%, 100% 100%;
  mask-repeat: no-repeat;
}

.sup__card:hover::before { opacity: 1; }
.sup__card:hover::after { opacity: 0.75; }
.sup__card:hover .sup__mark { border-color: var(--accent); }

.sup__card > * { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  .sup__card::before, .sup__card::after { transition: none; }
}

/* Dividers on the cell, not gaps between cells: the row reads as one ruled
   sheet rather than as three floating boxes. `:nth-child` rather than a
   sibling selector, so a hidden card cannot leave a divider with nothing on
   either side of it. */
.sup__card:not(:nth-child(3n + 1)) { border-inline-start: 1px solid var(--rule-soft); }

/* Once a filter has run, :nth-child is counting hidden cards and the dividers
   land on the wrong edges. 17-support.js re-indexes the visible ones into
   data-col and these take over. */
.sup__card[data-col] { border-inline-start: 0; }
.sup__card[data-col]:not([data-col="0"]) { border-inline-start: 1px solid var(--rule-soft); }

.sup__mark {
  width: 58px; height: 58px;
  display: grid; place-items: center;
  border: 1px solid var(--rule);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.025);
  transition: border-color var(--dur) var(--ease);
}

/* The logos are the vendors' own marks, at their own colours. They carry no
   meaning the text does not also carry, so they are decorative and unlabelled. */
.sup__mark img { width: 30px; height: 30px; object-fit: contain; }

.sup__mark--x { border-color: rgba(228, 117, 37, 0.45); }

.sup__name {
  margin-block-start: clamp(18px, 2.2vw, 26px);
  font-size: 1.15rem;
  font-weight: 600;
  line-height: 1.25;
  color: var(--text);
}

.sup__meta {
  margin-block-start: 7px;
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  color: var(--text-muted);
}

.sup__what {
  margin-block-start: 12px;
  font-size: var(--body);
  line-height: 1.55;
  color: var(--text-muted);
}

.sup__nodes { margin-block-start: 14px; display: flex; flex-wrap: wrap; gap: 6px; }

.sup__node {
  padding: 4px 9px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.24);
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  color: var(--text-muted);
}

/* --- empty state + footer line -------------------------------------------- */

.sup__none {
  padding-block: clamp(50px, 6vw, 84px);
  text-align: center;
  font-size: var(--body-lg);
  color: var(--text-muted);
}

.sup__note {
  padding-block: clamp(28px, 3.4vw, 44px);
  font-family: var(--font-mono);
  font-size: var(--mono, 12px);
  line-height: 1.7;
  color: var(--text-muted);
}

.sup__note b { font-weight: 400; color: var(--text); }

/* The accuracy row. It states that no figure ships until its measurement is
   written down, which is the whole reason there is no percentage on any card. */
.sup__acc {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  padding: clamp(16px, 2vw, 22px) clamp(18px, 2.2vw, 26px);
  border: 1px solid var(--rule);
  border-inline-start: 2px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--surface);
  margin-block-start: clamp(26px, 3.2vw, 40px);
}

.sup__acc-k { flex: none; font-size: var(--body); color: var(--text); }
.sup__acc-v { font-family: var(--font-mono); font-size: var(--mono, 12px); line-height: 1.6; color: var(--text-muted); }

@media (max-width: 1000px) {
  .sup__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sup__card:not(:nth-child(3n + 1)) { border-inline-start: 0; }
  .sup__card:not(:nth-child(2n + 1)) { border-inline-start: 1px solid var(--rule-soft); }
  .sup__card[data-col] { border-inline-start: 0; }
}

@media (max-width: 640px) {
  .sup__grid { grid-template-columns: minmax(0, 1fr); }
  .sup__card:not(:nth-child(2n + 1)) { border-inline-start: 0; }
  .sup__card[data-col] { border-inline-start: 0; }
}

/* ===== 27-mobile.css ============================================================ */
/* =========================================================================
   Mobile — the phone design system
   =========================================================================

   Everything above this file is authored for a 1280px column and scaled down
   with vw-based clamps. That produces a phone layout that is technically
   correct and reads as an afterthought: 15px body copy, 36px headlines,
   centred blocks, and mockups squeezed to a third of their design size.

   The references (CodeRabbit and Strand, both viewed on a phone) do four
   things this file adopts:

     1. Type does not shrink much. The headline is 40px on a 390 screen, not
        36; body copy is 17px, not 15. A phone is held closer than a laptop
        but the type gets *relatively* larger, not smaller.
     2. Everything left-aligns. Centred blocks are a wide-viewport device;
        at 390 they read as uncertain.
     3. Vertical rhythm opens up. Sections are separated by real space
        rather than by the same clamped padding the desktop uses.
     4. Mockups keep their real size and bleed off the right edge rather
        than being scaled down to fit. A readable panel with its edge cut off
        beats a whole panel nobody can read.

   One breakpoint: 700px. Below it, phone. Above it, the existing responsive
   rules already tested fine at 768 and 1024.
   ========================================================================= */

@media (max-width: 699px) {

  :root {
    /* 24px, matching both references. The old clamp bottomed out at 20. */
    --gutter: 24px;
    --nav-inset: 0px;

    /* --- type ----------------------------------------------------------
       Fixed values, not clamps: below 700px there is no range left to
       interpolate over, and a clamp here only makes the sizes harder to
       reason about. */
    --h1: 40px;
    --lh-h1: 1.06;
    --h2: 30px;
    --lh-h2: 1.15;

    --body: 17px;
    --body-sm: 15px;
    --body-lg: 18px;
    --lh-body-lg: 1.6;

    --radius: 12px;

    /* The vertical unit every section below is spaced by. */
    --sec-y: 72px;
  }

  body { font-size: var(--body); }

  /* --- alignment -------------------------------------------------------
     Every centred block on the site, un-centred. Listed explicitly rather
     than done with a wildcard so that a future centred thing has to opt in
     here deliberately. */
  .gap__inner,
  .intro,
  .rail__eyebrow,
  .before__in,
  .matrix__head,
  .cta__in,
  .fx__hero--center,
  .fx__twotone--center,
  .sup__head {
    text-align: start;
  }

  .fx__twotone--center { margin-inline: 0; max-width: none; }

  /* Actions were a centred row; they become a left-aligned column so both
     buttons are the same width and the primary one is unmissable. */
  .fx__actions,
  .hero__actions {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }

  .btn { height: 48px; font-size: 16px; }

  /* --- rhythm ----------------------------------------------------------
     One rule for the plate sections rather than a per-section override:
     they all share the same shell, so they can share the same rhythm. */
  .fx__in { padding-block: 0; }

  .fx__hero--center,
  .fx__hero--left,
  .fx__hero--split { padding-block: 28px var(--sec-y); }

  .fx__cols,
  .fx__six,
  .fx__figs { gap: 40px; }

  .fx__cols--2,
  .fx__cols--3 { grid-template-columns: minmax(0, 1fr); }

  /* --- headings --------------------------------------------------------- */

  .fx__h1, .hero__h1 { font-size: var(--h1); line-height: var(--lh-h1); }

  .fx__sub, .hero__sub {
    font-size: var(--body-lg);
    line-height: var(--lh-body-lg);
  }

  /* --- mockups ---------------------------------------------------------
     The reference sites let a wide mockup keep its size and bleed off the
     right edge. That works there because what falls off the edge is
     decorative continuation. Ours are key-and-value rows: cutting the right
     edge cuts the value, which is the half that carries the meaning
     ("Browser — Clicked Delete" becomes "Browser — Click"). So these take
     the full column instead, and keep their own UI type scale rather than
     inheriting the page's larger phone body size — a simulated interface
     that sets its labels at 17px stops looking like an interface. */
  .fmk-stage-in { min-width: 0; max-width: none; }

  .fmk-p-lab,
  .fmk-p-name,
  .fmk-p-sub,
  .fmk-p-head,
  .fmk-row,
  .fmk-pill { font-size: 14px; }

  .fmk-p-head { font-size: 15px; }

  /* --- footer ----------------------------------------------------------- */

  .ft__cols { gap: 32px 24px; }
}

/* =========================================================================
   Small phones
   ========================================================================= */

@media (max-width: 380px) {
  :root { --gutter: 20px; --h1: 36px; --h2: 27px; }
}

/* =========================================================================
   The seam band on a phone

   The two copy blocks straddle the line of light, as they do on desktop —
   that split is the whole idea of the section. What was wrong was the
   spacing: both were anchored a fixed distance from the band's centre, a
   distance measured against a 708px band, so on a 400px one they sat almost
   on top of each other with all the air pushed to the top and bottom.

   So on a phone neither block is anchored to the seam at all. The headline
   is pinned to the top of the band and the sub-copy to the bottom, both a
   fixed inset from the edge. Nothing floats near the middle, there is no
   space left over at either end, and the light gets the whole corridor
   between them. Both left-aligned, both on the page gutter.
   ========================================================================= */

@media (max-width: 699px) {

  .seam__band { height: 420px; }

  .seam__head,
  .seam__sub {
    padding-inline: var(--gutter);
    text-align: start;
    max-width: none;
    margin: 0;
  }

  /* Pinned to the top edge of the band. */
  .seam__head {
    inset-block-start: 34px;
    inset-block-end: auto;
    transform: none;
    font-size: 38px;
    line-height: 1.1;
  }

  /* Pinned to the bottom edge. */
  .seam__sub {
    inset-block-start: auto;
    inset-block-end: 34px;
    margin-block-start: 0;
    font-size: 17px;
    line-height: 1.55;
  }

  /* Two forced blocks, each sized to a 596px measure, wrapped mid-clause
     here. One paragraph instead. */
  .seam__sub span { display: inline; }
  .seam__sub span + span::before { content: " "; }

  .seam__sky { height: 84px; }

  .seam__ground {
    min-height: 320px;
    padding-block: 56px;
  }
}

/* =========================================================================
   The gap section's chat mockup

   .chat__lines is a grid, and a grid track sizes to max-content by default
   and will happily overflow its container rather than wrap. On a 342px card
   the mono lines ran off the right edge and were cut by the section's own
   overflow: clip. An explicit minmax(0, 1fr) track lets the column be
   narrower than its content wants, and the lines wrap.
   ========================================================================= */

@media (max-width: 699px) {
  .chat { max-width: none; }

  .chat__lines {
    grid-template-columns: minmax(0, 1fr);
    min-width: 0;
    font-size: 12px;
    line-height: 1.55;
    overflow-wrap: anywhere;
  }

  .chat__lines > p { min-width: 0; }

  .chat__card { padding: 16px; }

  /* The section and the shell inside it both carry a gutter, which on a
     phone spends 96px of a 390px screen on margins. */
  .gap { padding-inline: 0; }
}
