/* =========================================================================
   wovu.dev page styles

   Every colour, radius, duration, easing, type size, tracking and space below
   resolves to a var(--token) from tokens.css. The only literals are structural
   (1px hairlines, 50% for circles, percentage positions) and the environment
   recipe, which is copied verbatim from the product's globals.css and is
   marked design-token-exempt there for the same reason.

   Typographic rule for this page: the sans face is the human voice, the mono
   face is the machine voice. Mono appears only where a machine wrote the text
   or where the string is something you would literally type.
   ========================================================================= */

*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { scroll-behavior: smooth; }

body {
  font-family: var(--font-ui);
  color: var(--text-hi);
  background-color: var(--env-base);
  letter-spacing: var(--tracking-body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* --- the lit environment -------------------------------------------------
   Adapted from apps/web/app/globals.css. The product's recipe was tuned for a
   single-viewport app screen, where a viewport-fixed wash reads as one lit
   room. On a page you scroll for five screens the same wash is monotonous and
   reads as brown fill rather than light, so here the fixed layer is dimmed to
   ambience and the actual light sources are anchored to the page: warm light
   at the hero, warm light at the closing, dark and quiet in between where the
   reading happens. Same hues, same grain, same material. Only the intensity
   and the anchoring changed. design-token-exempt for the same reason it is in
   the product: this is one bespoke layered atmosphere, not a reusable colour.
   -------------------------------------------------------------------------- */
.environment {
  position: fixed;
  inset: 0;
  z-index: -2;
  background:
    radial-gradient(48% 40% at 12% 0%, hsl(28 92% 60% / 0.10), transparent 62%),
    radial-gradient(46% 46% at 92% 12%, hsl(24 90% 58% / 0.07), transparent 60%),
    radial-gradient(56% 50% at 84% 108%, hsl(214 86% 62% / 0.08), transparent 58%),
    linear-gradient(180deg, #12100e, #0c0b0a 46%);
}
.environment::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.4;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}
.orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(var(--blur-orb));
  z-index: -1;
  pointer-events: none;
}

/* --- glass primitives (copied from the product) -------------------------- */
.glass {
  position: relative;
  background: var(--glass-1);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  backdrop-filter: blur(var(--blur)) saturate(1.4);
  -webkit-backdrop-filter: blur(var(--blur)) saturate(1.4);
  box-shadow: var(--shadow-float);
}
.glass::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(180deg, var(--glass-specular), transparent 22%, transparent 82%, rgba(255,255,255,0.04));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}
.glass-raised {
  background: var(--glass-2);
  box-shadow: var(--shadow-lift);
  backdrop-filter: blur(var(--blur-heavy)) saturate(1.5);
  -webkit-backdrop-filter: blur(var(--blur-heavy)) saturate(1.5);
}

/* --- type utilities ------------------------------------------------------ */
.mono { font-family: var(--font-mono); letter-spacing: var(--tracking-mono); }
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-mid);
  margin: 0;
}
.lit {
  background: linear-gradient(180deg, var(--text-gradient-start), var(--accent));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Inline prose links. The page had none until half its content moved onto two
   other pages, and a link with no rule of its own renders in the browser's
   lavender, which is the only colour on this site that resolves to no token.
   The accent the machine voice already uses, underlined rather than coloured
   alone, so the link survives WCAG 1.4.1. Buttons are excluded: they carry
   their own surface. */
main a:not(.btn) {
  color: var(--accent);
  text-decoration-color: var(--accent-wash);
  text-underline-offset: 0.2em;
  transition: text-decoration-color var(--dur-1) var(--ease-out);
}
main a:not(.btn):hover { text-decoration-color: var(--accent); }

/* --- accessibility floor ------------------------------------------------- */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--accent);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--focus-ring-radius);
}
.skip {
  position: absolute;
  left: var(--space-16);
  top: calc(-1 * var(--space-72));
  z-index: 10;
  padding: var(--space-10) var(--space-16);
  border-radius: var(--r-sm);
  background: var(--glass-3);
  border: 1px solid var(--glass-border-strong);
  color: var(--text-hi);
  font-size: var(--text-sm-plus);
  text-decoration: none;
  transition: top var(--dur-1) var(--ease-out);
}
.skip:focus { top: var(--space-16); }

/* --- layout -------------------------------------------------------------- */
.shell {
  max-width: var(--size-1200);
  margin: 0 auto;
  padding-inline: var(--space-fluid-page-pad);
}
.section {
  padding-block: calc(var(--space-40) + var(--space-fluid-header-pad));
}
.rule {
  height: 1px;
  border: 0;
  margin: 0;
  background: linear-gradient(90deg, transparent, var(--glass-border), transparent);
}

/* =========================================================================
   THE THREAD

   The page's structural device: one continuous line running from under the
   wordmark to the footer, with every block of content hanging off it. It
   replaces the old two-column label/content layout, because a label column
   and a spine were two devices doing the same job in different places.

   Geometry. The line sits exactly on the shell's content edge, computed from
   the same max-width and page padding the shell itself uses, so it is
   pixel-aligned to the wordmark above it and the footer below it at every
   viewport with no measurement in JS. Content is then indented from it by
   --thread-inset, and every marker positions itself at --marker-x, which is
   that inset read backwards. One formula, so a marker can never drift off
   the line.

   Grammar. Three marker types, and the difference carries meaning:
     ring  a section begins here
     dot   a numbered step in a sequence (only the five stations, which are
           the one part of this page where order is information)
     tick  an item that is attached to the thread but is not a step
   Anything that is not a sequence never gets a numeral.

   Motion. A light travels the line at scroll position and the line behind it
   warms, so scrolling the page is the same gesture as scrubbing the session
   replay in the hero. Markers light as the light reaches them and stay lit.
   Under reduced motion the line is static and the light never appears.
   ========================================================================= */
body {
  --thread-inset: var(--space-40);
  --marker-x: calc(1px - var(--thread-inset));
}
.threaded { position: relative; }
.thread {
  position: absolute;
  top: var(--space-72);
  bottom: var(--space-72);
  left: calc(max(0px, (100% - var(--size-1200)) / 2) + var(--space-fluid-page-pad));
  right: calc(max(0px, (100% - var(--size-1200)) / 2) + var(--space-fluid-page-pad));
  pointer-events: none;
  --p: 0;
  --p-vis: 0;
}
/* the rail fades up out of nothing over its first stretch, so the line reads
   as descending from the wordmark rather than starting at an arbitrary y */
.thread-rail {
  position: absolute;
  left: 0;
  top: 0;
  bottom: var(--space-40);
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(180deg, transparent, var(--glass-border) var(--space-72));
}
.thread-fill {
  position: absolute;
  left: 0;
  top: 0;
  width: 2px;
  height: calc(var(--p) * (100% - var(--space-40)));
  border-radius: 2px;
  background: linear-gradient(180deg, var(--accent-wash), var(--accent-glow));
  opacity: var(--p-vis);
  transition: opacity var(--dur-2) var(--ease-out);
}
.thread-light {
  position: absolute;
  left: 1px;
  top: calc(var(--p) * (100% - var(--space-40)));
  width: var(--size-10);
  height: var(--size-10);
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 16px 4px var(--accent-glow);
  opacity: var(--p-vis);
  transition: opacity var(--dur-2) var(--ease-out);
}
/* the resolution: the line turns and becomes the hairline under the footer.
   A thread that simply stopped would read as a rendering fault. */
.thread-end {
  position: absolute;
  left: 0;
  bottom: 0;
  width: var(--space-64);
  height: var(--space-40);
  border-left: 2px solid var(--glass-border);
  border-bottom: 2px solid var(--glass-border);
  border-bottom-left-radius: var(--r-md);
  transition: border-color var(--dur-3) var(--ease-out);
}
.thread-tail {
  position: absolute;
  left: var(--space-64);
  bottom: 0;
  height: 2px;
  width: 46%;
  background: linear-gradient(90deg, var(--glass-border), transparent);
  transition: background var(--dur-3) var(--ease-out);
}
.thread.done .thread-end { border-color: var(--accent-wash); }
.thread.done .thread-tail { background: linear-gradient(90deg, var(--accent-wash), transparent); }

/* the tributary: the live session in the hero drains into the thread. Its
   width is derived from the hero's own grid ratio, so the elbow meets the
   slab's left edge exactly without anything being measured at runtime. */
.thread-origin {
  position: absolute;
  top: 100%;
  right: 100%;
  height: var(--space-30);
  width: calc(100% / 1.02 + var(--space-fluid-hero-gap) + var(--thread-inset));
  border-right: 2px solid var(--glass-border);
  border-bottom: 2px solid var(--glass-border);
  border-bottom-right-radius: var(--r-md);
  pointer-events: none;
}

/* --- content hangs off the thread ---------------------------------------- */
.band { padding-left: var(--thread-inset); }
.band-center { text-align: center; }
.band-center > * { margin-inline: auto; }

/* Inside a band, the claim sits left and the elaboration sits right. This is
   the same two-column shape as the hero, and it is what stops the page from
   becoming a single narrow ribbon down the left edge once the old label
   column is gone. The left column carries the heading, which is real content,
   where the label column carried four words of index. */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-fluid-hero-gap);
  align-items: start;
}
.split .lede { margin-top: 0; }
.split-r { min-width: 0; }

.node { position: relative; }
.node::before {
  content: "";
  position: absolute;
  left: var(--marker-x);
  top: var(--space-6);
  transform: translateX(-50%);
  width: var(--size-8);
  height: var(--size-8);
  border-radius: 50%;
  background: var(--env-base);
  border: 1px solid var(--glass-border-strong);
  transition: border-color var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out);
}
.node.lit::before {
  border-color: var(--accent);
  box-shadow: 0 0 10px var(--accent-glow);
}

/* --- header -------------------------------------------------------------- */
.head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  padding-block: var(--space-24);
}
.wordmark {
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: var(--tracking-heading);
  color: var(--text-hi);
  text-decoration: none;
}
.wordmark span { color: var(--accent); }
.head-right { display: flex; align-items: center; gap: var(--space-12); }
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  border-radius: var(--r-pill);
  padding: var(--space-7) var(--space-14);
  font-size: var(--text-xs-plus);
  color: var(--text-mid);
  box-shadow: none;
}
/* On this page motion means one thing: something is happening right now. So
   the dot only breathes inside the live session. The header's status dot,
   which marks a product that has not shipped, holds still. */
.dot {
  width: var(--size-6);
  height: var(--size-6);
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent-glow);
  flex: none;
}
.session-id .dot { animation: breathe 2.6s var(--ease-out) infinite; }
.head-link {
  font-size: var(--text-xs-plus);
  color: var(--text-mid);
  text-decoration: none;
  padding: var(--space-7) var(--space-10);
  border-radius: var(--r-sm);
  transition: color var(--dur-1) var(--ease-out);
}
.head-link:hover { color: var(--text-hi); }

/* --- hero ---------------------------------------------------------------- */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.02fr);
  gap: var(--space-fluid-hero-gap);
  align-items: center;
  padding-left: var(--thread-inset);
  padding-block: var(--space-40) var(--space-72);
  /* the hero owns the first screen: viewport height less the header and this
     block's own padding, all composed from spacing tokens */
  min-height: calc(100svh - var(--space-72) - var(--space-64) - var(--space-40));
}
.hero-copy { max-width: var(--size-560); }
.hero h1 {
  font-size: var(--text-hero);
  line-height: 1.04;
  letter-spacing: var(--tracking-display);
  font-weight: 600;
  margin: var(--space-22) 0 0;
  text-wrap: balance;
}
.hero-sub {
  margin: var(--space-24) 0 0;
  font-size: var(--text-hero-sub);
  line-height: 1.65;
  color: var(--text-mid);
  max-width: var(--size-460);
}
.hero-sub strong { color: var(--text-hi); font-weight: 500; }
.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-12);
  margin-top: var(--space-32);
}
.note {
  margin: var(--space-26) 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  color: var(--text-mid);
  line-height: 1.7;
}

/* --- buttons ------------------------------------------------------------- */
.btn {
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  gap: var(--space-9);
  padding: var(--space-13) var(--space-22);
  border-radius: var(--r-pill);
  font-size: var(--text-sm-plus);
  font-weight: 500;
  text-decoration: none;
  border: 1px solid transparent;
  transition: transform var(--dur-1) var(--ease-out), background var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
.btn:hover { transform: translateY(-1px); }
.btn-primary {
  background: linear-gradient(180deg, var(--button-primary-start), var(--accent));
  border-color: var(--button-primary-border);
  color: var(--button-primary-ink);
  box-shadow: 0 1px 0 0 var(--specular-soft) inset, 0 10px 30px -12px var(--accent-glow);
}
.btn-primary::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--size-60);
  background: linear-gradient(90deg, transparent, var(--sheen-color), transparent);
  transform: translateX(-120%);
  pointer-events: none;
}
.btn-primary:hover::after { animation: sheen var(--dur-3) var(--ease-out); }
.btn-ghost {
  background: var(--glass-1);
  border-color: var(--glass-border);
  color: var(--text-hi);
  backdrop-filter: blur(var(--blur));
  -webkit-backdrop-filter: blur(var(--blur));
}
.btn-ghost:hover { border-color: var(--glass-border-strong); background: var(--glass-2); }

/* --- the signature: one live session ------------------------------------- */
/* The stage stretches to the hero row rather than being centred in it, and
   centres the figure itself instead. Visually identical, because the figure
   is the stage's only content and centring it in a stretched box lands it
   exactly where centring the box did. Structurally it is the fix for a real
   bug: the tributary below hangs off `top: 100%` of this element, so while
   this element was centred, its bottom edge sat wherever the shorter of the
   two columns happened to end. Once the copy column grew taller than the
   session slab, that was a y-position still inside the copy column, and the
   tributary's 611px width then carried a 2px border straight across the
   column boundary and into the caption. Anchoring to the row's real bottom
   means the elbow cannot be drawn inside either column at any height. */
.stage {
  position: relative;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.session {
  width: 100%;
  padding: var(--space-24);
  border-radius: var(--r-xl);
  transform-style: preserve-3d;
  will-change: transform;
}
/* the warm light that shows through the material, so the glass actually
   refracts something (the product does this pointer-tracked; here it is a
   fixed source, because the page is read more than it is played with) */
.session::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(62% 55% at 28% 0%, var(--accent-wash), transparent 70%);
  pointer-events: none;
}
.session > * { position: relative; z-index: 1; }
/* The signature moment. When the second person's instruction lands, a single
   pass of warm light crosses the glass: the delivery is something you see
   happen, not a word in a status chip. It fires once, at one moment, and the
   rest of the page stays still. */
.sweep {
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    104deg,
    transparent 24%,
    var(--accent-wash) 44%,
    var(--accent-glow) 50%,
    var(--accent-wash) 56%,
    transparent 76%
  );
  mix-blend-mode: screen;
}
.sweep.fire { animation: deliver 1.1s var(--ease-out); }
@keyframes deliver {
  0%   { opacity: 0;    transform: translateX(-24%); }
  30%  { opacity: 0.42; }
  100% { opacity: 0;    transform: translateX(24%); }
}
.session-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-12);
  padding-bottom: var(--space-16);
  border-bottom: 1px solid var(--glass-border);
}
.session-id { display: flex; align-items: center; gap: var(--space-10); }
.session-id .label { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-hi); letter-spacing: var(--tracking-mono); }
.session-id .live { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--text-mid); letter-spacing: var(--tracking-mono-wide); }
.crew { display: flex; }
.who {
  width: var(--size-26);
  height: var(--size-26);
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--env-base);
  border: 2px solid var(--avatar-ring);
  margin-left: calc(-1 * var(--space-8));
}
/* NEVER-BLANK RULE, second half. These start invisible only when scripting is
   present to bring them back. Without the .js guard the session slab renders
   as an empty pane of glass anywhere scripts do not run, which is the same
   failure the reveal observer had and it lands on the page's one signature
   element. */
.js .who {
  opacity: 0;
  transform: translateY(var(--space-6)) scale(0.8);
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-spring);
}
.crew .who:first-child { margin-left: 0; }
.js .who.on { opacity: 1; transform: none; }

.stream {
  list-style: none;
  margin: 0;
  padding: var(--space-16) 0 0;
}
.ev {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  align-items: baseline;
  gap: var(--space-10);
  padding: var(--space-7) 0;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-mono);
}
.js .ev {
  opacity: 0;
  transform: translateY(var(--space-8));
}
.js .ev.on {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out);
}
/* --text-low measures 3.4:1 on this background, which is below the AA floor,
   so anything that is real text uses --text-mid and is de-emphasised by size
   and by the mono face instead of by contrast. */
.ev-t { font-size: var(--text-2xs); color: var(--text-mid); font-variant-numeric: tabular-nums; }
.ev-who {
  font-size: var(--text-2xs-plus);
  color: var(--ev-hue, var(--accent));
  min-width: 6ch;
}
.ev-body { color: var(--text-mid); min-width: 0; }
.ev-body b { color: var(--text-hi); font-weight: 500; }
.ev[data-hue="agent"] { --ev-hue: var(--accent); }
.ev[data-hue="rose"]  { --ev-hue: var(--p-rose); }
.ev[data-hue="gold"]  { --ev-hue: var(--p-gold); }
.ev-quote { color: var(--text-hi); }

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
  margin-left: var(--space-8);
  padding: var(--space-2) var(--space-8);
  border-radius: var(--r-pill);
  border: 1px solid var(--glass-border);
  background: var(--glass-1);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-mono-wide);
  color: var(--text-mid);
  white-space: nowrap;
  transition: color var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out);
}
.chip.done { color: var(--accent); border-color: var(--accent-wash); }

.session-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-12);
  margin-top: var(--space-20);
  padding-top: var(--space-16);
  border-top: 1px solid var(--glass-border);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-mono-wide);
  color: var(--text-mid);
}
.replay {
  appearance: none;
  background: none;
  border: 0;
  padding: var(--space-3) var(--space-6);
  border-radius: var(--focus-ring-radius);
  font: inherit;
  letter-spacing: inherit;
  color: var(--text-mid);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-out);
}
.replay:hover { color: var(--accent); }
.caption {
  margin: var(--space-14) 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-mono);
  color: var(--text-mid);
  text-align: center;
}

/* --- prose bands --------------------------------------------------------- */
/* Section headings share the hero's display register. The token scale has no
   step between 20px and the hero clamp, and the honest answer was not to
   invent one: it was to keep the headings short enough to carry display size. */
.h2 {
  font-size: var(--text-hero);
  line-height: 1.06;
  letter-spacing: var(--tracking-display);
  font-weight: 600;
  margin: 0;
  color: var(--text-hi);
  text-wrap: balance;
}
.lede {
  margin: var(--space-26) 0 0;
  font-size: var(--text-hero-sub);
  line-height: 1.7;
  color: var(--text-mid);
  max-width: var(--size-560);
}
.lede strong { color: var(--text-hi); font-weight: 500; }

/* --- the stations: the life of one task ----------------------------------
   These are the only blocks on the page that carry a numeral, because they
   are the only content where the order is part of the information. They hang
   off the page thread rather than owning a spine of their own. */
.spine { margin-top: var(--space-64); }
/* the station's own words on the left, and the thing the machine says on the
   right, aligned to the station title. The page's sans/mono rule, given a
   position on the page as well as a typeface. */
.stop {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-fluid-hero-gap);
  align-items: baseline;
  padding-bottom: var(--space-64);
}
.stop-main { min-width: 0; }
/* Most stations no longer carry a command in the right column, because a
   command that only decorates makes the two real ones invisible. A station
   without one takes the full width rather than leaving a hole; the body copy
   is capped at the same measure either way, so the two shapes read alike. */
.stop-main:only-child { grid-column: 1 / -1; }
.stop > .artifact { margin-top: 0; justify-self: start; }
.stop:last-child { padding-bottom: 0; }
.stop::before {
  content: "";
  position: absolute;
  left: var(--marker-x);
  top: var(--space-8);
  transform: translateX(-50%);
  width: var(--size-12);
  height: var(--size-12);
  border-radius: 50%;
  background: var(--env-base);
  border: 2px solid var(--glass-border-strong);
  transition: border-color var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out);
}
/* a step the light has passed is filled in. Hollow ahead, solid behind: the
   sequence reads as something being worked through, not a static list. */
.stop.lit::before {
  border-color: var(--accent);
  background: var(--accent);
  box-shadow: 0 0 12px var(--accent-glow);
}
.stop-head { display: flex; align-items: baseline; gap: var(--space-10); }
.stop-n {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-mono-wide);
  color: var(--text-mid);
}
.stop-t {
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: var(--tracking-heading);
  margin: 0;
}
.stop-b {
  margin: var(--space-10) 0 0;
  font-size: var(--text-base);
  line-height: 1.75;
  color: var(--text-mid);
  max-width: var(--size-560);
}
.artifact {
  display: inline-block;
  margin-top: var(--space-16);
  padding: var(--space-8) var(--space-14);
  border-radius: var(--r-sm);
  background: var(--surface-input);
  border: 1px solid var(--glass-border);
  font-family: var(--font-mono);
  font-size: var(--text-xs-plus);
  letter-spacing: var(--tracking-mono);
  color: var(--text-mid);
  max-width: 100%;
  /* commands wrap, they never scroll. A horizontal scroller nested inside a
     page you are already scrolling is a trap on any device, and now that
     these sit in a column of their own the longest of them is one character
     away from producing one. */
  white-space: normal;
}
.artifact b { color: var(--accent); font-weight: 500; }

/* --- clients ------------------------------------------------------------- */
.clients {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-10);
  margin-top: var(--space-26);
  padding: 0;
  list-style: none;
}
.clients li {
  padding: var(--space-8) var(--space-14);
  border-radius: var(--r-pill);
  border: 1px solid var(--glass-border);
  background: var(--glass-1);
  font-family: var(--font-mono);
  font-size: var(--text-xs-plus);
  letter-spacing: var(--tracking-mono);
  color: var(--text-mid);
}
.clients li:last-child { color: var(--text-hi); border-color: var(--accent-wash); }
/* This row is a set of examples, not the last item of a list, so the emphasis
   the rule above puts on a final entry does not belong here. The rule it
   carries is stated above the row instead, at the size a rule deserves. */
.clients.examples li:last-child { color: var(--text-mid); border-color: var(--glass-border); }

/* --- the connect panes ---------------------------------------------------
   Two panes holding what you would actually paste. Same material as .card,
   same machine voice as .artifact: nothing new is introduced except a <pre>,
   which is the one element on the page allowed to scroll sideways inside
   itself, because a JSON line cannot wrap without becoming a different file.
   Putting the same server side by side in two vendors' settings files argues
   the neutrality claim better than the sentence above them does. */
.panes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-20);
  margin: var(--space-40) 0 0;
  padding: 0;
  list-style: none;
}
/* the single full-width pane: the two values you paste into everything the
   writer does not know the file layout of, which is most of what exists */
.panes.one { grid-template-columns: minmax(0, 1fr); margin-top: var(--space-20); }
.pane {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: var(--space-24);
  border-radius: var(--r-lg);
}
.pane-h {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-12);
  padding-bottom: var(--space-14);
  border-bottom: 1px solid var(--glass-border);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-mono-wide);
  color: var(--text-mid);
}
.pane-h b {
  color: var(--text-hi);
  font-weight: 500;
  font-size: var(--text-xs-plus);
  letter-spacing: var(--tracking-mono);
}
/* Every .replay control (the pane copy buttons AND the hero session's replay
   button) does nothing without scripting, so none of them render until .js is
   set: the same rule the session slab follows, now applied to all of them so
   nothing on this page is ever a control that does nothing. The copy buttons
   and the replay button are the only .replay elements, and both need JS. */
.replay { display: none; }
.js .replay { display: inline-block; }
.pane pre {
  margin: var(--space-16) 0 0;
  padding: 0;
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  line-height: 1.8;
  color: var(--text-mid);
}
.pane pre b { color: var(--accent); font-weight: 500; }

/* --- the build log -------------------------------------------------------
   The same hairline row the capability table uses, in the machine voice,
   because this is git output and nothing else. Real commits, real dates. */
.log {
  margin: var(--space-40) 0 0;
  padding: 0;
  list-style: none;
  max-width: calc(var(--size-240) + var(--space-24) + var(--size-560));
}
.log li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-6) var(--space-24);
  align-items: baseline;
  padding-block: var(--space-12);
  border-top: 1px solid var(--glass-border);
  font-family: var(--font-mono);
  font-size: var(--text-xs-plus);
  letter-spacing: var(--tracking-mono);
  color: var(--text-mid);
}
.log .d { color: var(--accent); font-variant-numeric: tabular-nums; }

/* =========================================================================
   THE STORY

   One account of one project, told in six scenes down the page, in the same
   vocabulary the motion line already uses: straight segments, right-angle
   turns, a travelling light, and markers that fill when they light. Nothing
   here draws the product's interface, because the interface does not exist
   yet. These are the things the system actually holds: agents, the work they
   do, the entries they leave behind, the warnings that reach them.

   MECHANICS. Each scene carries exactly one custom property, --s, its own
   progress from 0 to 1, written once per frame by the same rAF loop that
   already drives the motion line. Every element inside derives its own local
   progress from --s and a start and end (--a, --b), so an entire scene is one
   number written to one element and the browser resolves the rest. Only
   transform and opacity are animated. A scene that is not on screen is never
   written to, so it costs nothing.

   TWO DEFAULTS CARRY THE ACCESSIBILITY. --s defaults to 1, so a scene with no
   scripting at all, or under reduced motion, renders its finished frame:
   never an empty box, never a pose caught halfway. And nothing in any scene
   animates away to nothing, so every finished frame is a composed image that
   states the same thing the motion states. That constraint shaped the scenes
   more than any other: the problem scene ends on drained rings rather than on
   an empty space, and the closing scene is the same composition with those
   rings filled.
   ========================================================================= */
/* Registering the scene properties as typed numbers rather than leaving them
   as untyped token streams is the single biggest performance decision in this
   whole piece. Untyped, every one of the hundred and twenty elements has to
   re-parse a string and re-resolve a calc tree each time its scene's --s
   changes; typed, the value is stored as a number and substituted as one.
   Browsers without @property ignore this block and fall back to the plain
   declarations below, which still work, just more slowly. --s initialises to
   1 so that no scripting and reduced motion both land on the finished frame. */
@property --s { syntax: "<number>"; inherits: true; initial-value: 1; }
@property --a { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --b { syntax: "<number>"; inherits: false; initial-value: 1; }
@property --c { syntax: "<number>"; inherits: true; initial-value: 2; }
@property --d { syntax: "<number>"; inherits: true; initial-value: 3; }
@property --f { syntax: "<number>"; inherits: true; initial-value: 8; }
@property --g { syntax: "<number>"; inherits: true; initial-value: 9; }
@property --rot { syntax: "<angle>"; inherits: false; initial-value: 0deg; }

.scene {
  position: relative;
  margin: var(--space-40) 0 0;
  --sh: var(--size-240);
  --s: 1;
  height: var(--sh);
  pointer-events: none;
}
.scene.tall { --sh: var(--size-380); }

/* local progress, the one formula every element runs on */
.sw, .sm, .sh2 {
  --r: clamp(0, calc((var(--s) - var(--a, 0)) / (var(--b, 1) - var(--a, 0))), 1);
}

/* a wire. One segment, drawn from its own left edge outward, turned by --rot
   so the same primitive draws down, up, left and right. Right angles only,
   which is the motion line's own geometry and also the cheapest thing a
   compositor can be asked to do. */
/* No opacity formula here on purpose. A wire either grows or it dims, and
   only the handful that dim need a second expression, so the seventy-odd that
   merely grow resolve one clamp instead of three. */
.sw {
  position: absolute;
  height: 2px;
  margin-top: -1px;
  border-radius: 2px;
  background: var(--glass-border-strong);
  transform-origin: 0 50%;
  transform: rotate(var(--rot, 0deg)) scaleX(var(--r));
}
/* a wire that is carrying light rather than merely existing */
.sw.on { background: linear-gradient(90deg, var(--accent-wash), var(--accent)); }
/* A run that ended. It dims hard but never to nothing, because a wire that
   vanished would read as a rendering fault rather than as a loss, and because
   the finished frame has to still show where the work got to. */
/* Only the dimming term. A dying wire never needs to fade in, because it
   arrives by growing, and carrying a rise term here was an actual bug once
   --c and --d became registered properties with their own initial values:
   the rise resolved against 2 and 3 rather than the fallbacks this rule used
   to supply, evaluated to zero, and took the whole product to zero with it.
   The wire that was supposed to floor at 0.45 disappeared completely. */
.sw.dies {
  opacity: calc(0.45 + 0.55 * clamp(0, calc((var(--g) - var(--s)) / (var(--g) - var(--f))), 1));
}
/* a wall: the same primitive, thicker and cooler, for the thing a run stops against */
.sw.wall { height: var(--space-6); margin-top: calc(var(--space-6) / -2); border-radius: var(--space-3); background: var(--glass-border-strong); }
/* the board: the one horizontal that everything eventually lands on */
.sw.board { background: linear-gradient(90deg, transparent, var(--glass-border-strong) 8%, var(--glass-border-strong) 92%, transparent); }

/* a marker. Hollow is a thing that happened; filled is a thing that survived.
   The whole story is the second kind accumulating. */
.sm {
  position: absolute;
  width: var(--size-8);
  height: var(--size-8);
  margin: calc(var(--size-8) / -2) 0 0 calc(var(--size-8) / -2);
  border-radius: 50%;
  border: 1px solid var(--glass-border-strong);
  background: var(--env-base);
  transform: scale(var(--r));
}
/* An agent node marks where a run started. It never lights, so it does not
   carry the lighting expression at all. */
.sm.big { width: var(--size-12); height: var(--size-12); margin: calc(var(--size-12) / -2) 0 0 calc(var(--size-12) / -2); border-width: 2px; }
.sm.big::after { content: none; }
/* a file, or any contended object: the one square thing on the page */
.sm.obj { width: var(--size-12); height: var(--size-12); margin: calc(var(--size-12) / -2) 0 0 calc(var(--size-12) / -2); border-radius: var(--space-3); border-width: 2px; }
.sm::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent-glow);
  opacity: calc(
    clamp(0, calc((var(--s) - var(--c, 2)) / (var(--d, 3) - var(--c, 2))), 1) *
    clamp(0, calc((var(--g, 9) - var(--s)) / (var(--g, 9) - var(--f, 8))), 1)
  );
}
.sm.obj::after { background: none; box-shadow: 0 0 12px var(--accent-glow) inset, 0 0 14px var(--accent-glow); border: 2px solid var(--accent); }

/* the travelling light, the same object as the one on the motion line */
.sh2 {
  position: absolute;
  width: var(--size-10);
  height: var(--size-10);
  margin: calc(var(--size-10) / -2) 0 0 calc(var(--size-10) / -2);
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 14px 3px var(--accent-glow);
  transform: translate(calc(var(--r) * var(--dx, 0px)), calc(var(--r) * var(--dy, 0px)));
  opacity: calc(
    clamp(0, calc((var(--s) - var(--c, -1)) / (var(--d, 0) - var(--c, -1))), 1) *
    clamp(0, calc((var(--g, 9) - var(--s)) / (var(--g, 9) - var(--f, 8))), 1)
  );
}

/* the scene's own caption, in the machine voice, because the motion is
   decorative and must never be the only thing carrying a claim */
.scene-cap {
  position: absolute;
  left: 0;
  bottom: calc(-1 * var(--space-26));
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-mono-wide);
  color: var(--text-mid);
}

/* --- the asks: the product's surface, by the question it answers ----------
   Deliberately not a feature grid. A grid of tiles flattens twenty-three
   capabilities into twenty-three equal squares and tells you nothing about
   which problem any of them belongs to. Instead each group opens with a
   sentence somebody actually says out loud, and under it the capabilities
   that answer it are listed the way the product itself stores things: a
   typed key on the left, in the machine voice, and what it means for a
   person on the right, in the human voice. The page's existing typographic
   rule does the structural work, so no new device is introduced. */
.asks { margin-top: var(--space-64); }
.ask { position: relative; padding-bottom: var(--space-64); }
.ask:last-child { padding-bottom: 0; }
/* a tick, not a dot: these are attached to the thread but they are not steps */
.ask::before {
  content: "";
  position: absolute;
  left: var(--marker-x);
  top: var(--space-14);
  width: var(--space-20);
  height: 1px;
  background: var(--glass-border-strong);
  transition: background var(--dur-2) var(--ease-out);
}
.ask.lit::before { background: var(--accent); }
.ask-q {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: var(--tracking-heading);
  color: var(--text-hi);
  text-wrap: balance;
}
.ask-a {
  margin: 0;
  font-size: var(--text-base);
  line-height: 1.75;
  color: var(--text-mid);
  max-width: var(--size-560);
}
/* the rules stop where the text stops: a hairline that runs on past the last
   word is the tell of a table that was never measured */
.caps {
  margin: var(--space-26) 0 0;
  padding: 0;
  max-width: calc(var(--size-240) + var(--space-24) + var(--size-560));
}
.cap {
  display: grid;
  grid-template-columns: minmax(0, var(--size-240)) minmax(0, 1fr);
  gap: var(--space-10) var(--space-24);
  align-items: baseline;
  padding-block: var(--space-14);
  border-top: 1px solid var(--glass-border);
}
/* The plain-language label leads and the API primitive sits underneath it in
   the machine voice. Same row, same hairline, same two columns; only the left
   cell gained a second line. The primitives stay on the page, because they are
   the best evidence any of this is real, but a reader scanning the left edge
   now learns what the product does without reading a paragraph. */
.cap-k { margin: 0; }
.cap-label {
  display: block;
  font-size: var(--text-sm-plus);
  font-weight: 500;
  letter-spacing: var(--tracking-heading);
  color: var(--text-hi);
  text-wrap: balance;
}
.cap-prim {
  display: block;
  margin-top: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  color: var(--accent);
  overflow-wrap: anywhere;
}
.cap-v {
  margin: 0;
  font-size: var(--text-sm-plus);
  line-height: 1.7;
  color: var(--text-mid);
  max-width: var(--size-560);
}
.cap-v strong { color: var(--text-hi); font-weight: 500; }

/* --- guarantees ---------------------------------------------------------- */
.cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-20);
  margin: var(--space-64) 0 0;
  padding: 0;
  list-style: none;
}
.card {
  display: flex;
  flex-direction: column;
  padding: var(--space-30);
  border-radius: var(--r-lg);
}
.card p { flex: 1; }
.card h3 {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: var(--tracking-heading);
  color: var(--text-hi);
}
.card p { margin: var(--space-14) 0 0; font-size: var(--text-base); line-height: 1.65; color: var(--text-mid); }
.card .mono-note {
  display: block;
  margin-top: var(--space-14);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-mono-wide);
  color: var(--accent);
}

/* --- closing ------------------------------------------------------------- */
/* the closing is the page's one centred moment: the bookend to the hero. The
   thread passes it on the left without a marker, because the closing is not
   another entry in the index, it is where the page runs out. */
.closing { position: relative; }
.closing .inner { margin-inline: auto; max-width: var(--size-560); }
.closing .cta-row { justify-content: center; }

/* --- footer -------------------------------------------------------------- */
.foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  padding-block: var(--space-32) var(--space-40);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-mono-wide);
  color: var(--text-mid);
}
.foot a { color: var(--text-mid); text-decoration: none; }
.foot a:hover { color: var(--text-hi); }
.foot-links { display: flex; flex-wrap: wrap; gap: var(--space-20); }

/* --- entrance + reveal --------------------------------------------------- */
@keyframes breathe { 0%, 100% { opacity: 0.5; transform: scale(1); } 50% { opacity: 1; transform: scale(1.25); } }
@keyframes sheen { 0% { transform: translateX(-120%); } 100% { transform: translateX(220%); } }
@keyframes rise { from { opacity: 0; transform: translateY(var(--space-16)); } to { opacity: 1; transform: none; } }

.js .enter { opacity: 0; animation: rise var(--dur-3) var(--ease-out) forwards; }
.js .enter-1 { animation-delay: 0.05s; }
.js .enter-2 { animation-delay: 0.10s; }
.js .enter-3 { animation-delay: 0.15s; }
.js .enter-4 { animation-delay: 0.20s; }
.js .enter-5 { animation-delay: 0.28s; }

.js .reveal { opacity: 0; transform: translateY(var(--space-16)); }
.js .reveal.in {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out);
}

/* --- responsive ---------------------------------------------------------- */
@media (max-width: 1000px) {
  .hero { grid-template-columns: minmax(0, 1fr); gap: var(--space-64); }
  .hero-copy { max-width: none; }
  .hero-sub { max-width: none; }
  .cards { grid-template-columns: minmax(0, 1fr); gap: var(--space-14); }
  .panes { grid-template-columns: minmax(0, 1fr); gap: var(--space-14); }
  .split, .stop { grid-template-columns: minmax(0, 1fr); gap: var(--space-24); }
  .stop { align-items: start; }
  /* the hero has stacked, so the slab already sits directly on the thread and
     an elbow drawn across a single column would only be a kink in a straight
     line. The tributary is a wide-screen idea; below this width the session
     joins the thread by simply being above it. */
  .thread-origin { display: none; }
  .cap { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }
  .cap-v { max-width: none; }
}
@media (max-width: 560px) {
  /* The thread's cost on a phone, decided rather than inherited: it keeps its
     position on the content edge and the indent drops from 40px to 20px, so
     it takes 20px of a 350px column. The stations previously indented 30px
     for a spine of their own, so the sections lose a little and the stations
     gain a little. What would not have been worth it is a full-width stripe:
     at this size the line is a hairline in the margin of the reading column,
     which is exactly what a thread should be. */
  body { --thread-inset: var(--space-20); }
  .section { padding-block: var(--space-64); }
  .hero { padding-block: var(--space-24) var(--space-64); }
  .session { padding: var(--space-16); border-radius: var(--r-lg); }
  .ev { grid-template-columns: auto minmax(0, 1fr); gap: var(--space-8); font-size: var(--text-xs-plus); }
  .ev-t { display: none; }
  .ev-who { min-width: 5ch; }
  .ask::before { width: var(--space-12); }
  .thread-end { width: var(--space-30); }
  .thread-tail { left: var(--space-30); }
  /* Header nav on a phone. Previously every .head-link was hidden here, which
     left questions.html and build-log.html reachable only from the footer. Now
     the two internal page links stay in the header where a reader looks first;
     only the external GitHub link (.head-ext) drops, because the footer still
     carries it. Gaps and link padding tighten so wordmark + both links + the
     status pill fit a 390px column without wrapping. */
  .head { gap: var(--space-10); }
  .head-right { gap: var(--space-6); }
  .head-link { padding-inline: var(--space-8); }
  .head-ext { display: none; }
  .artifact { font-size: var(--text-xs); }
  .pane { padding: var(--space-16); }
  .pane pre { font-size: var(--text-2xs); }
  /* The scenes are composed, not scaled. At this width five parallel strands
     sit 39px apart and the gap between them, which is the entire point of the
     first scene, stops being the most obvious thing on screen. So the outer
     pair drop out and the composition runs on three, which says the same
     thing with room to say it. The scenes that need vertical room take it. */
  .m-drop { display: none; }
  .scene { --sh: var(--size-300); }
  .scene.tall { --sh: var(--size-380); }
  .log li { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); }
  .foot { justify-content: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .js .enter, .js .reveal { opacity: 1; transform: none; }
  /* Every scene parks on its finished frame, which was composed to be read
     as a still: the drained rings, the long path beside the short one, the
     warning that arrived, the break with the work still lit past it. Not a
     blank space and not a pose caught in the middle. */
  .scene { --s: 1 !important; }
  /* the thread degrades to what it is underneath: a static line. No
     travelling light, no warming fill, and every marker already lit so
     nothing looks half finished. */
  .thread-light, .thread-fill { display: none; }
  .node::before { border-color: var(--accent); }
  .stop::before { border-color: var(--accent); }
  .ask::before { background: var(--accent); }
}
