/* ScrumIA — single stylesheet, no external dependency.
 *
 * Every colour, spacing, radius and duration comes from assets/tokens.css (generated
 * from design/tokens.css). A literal one here is a bug: it is a decision taken outside
 * the design system, and the next screen will take a different one.
 *
 * What stays local: the geometry of a single control — the 30px theme button, the 28px
 * step circle, a hairline's width. Those appear once, and a value that appears once is
 * not a token.
 */

/* ---------- Fonts ---------- *
 * Self-hosted variable woff2, latin subset — ~85 KB for three families and every
 * weight. The alternative was a system stack, which costs nothing and says nothing;
 * the identity asks for a voice, and this is what a voice costs.
 */
@font-face {
  font-family: "Sora";
  src: url("fonts/sora-latin.woff2") format("woff2-variations");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Source Sans 3";
  src: url("fonts/source-sans-3-latin.woff2") format("woff2-variations");
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("fonts/jetbrains-mono-latin.woff2") format("woff2-variations");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}

/* ---------- Base ---------- */

:root { color-scheme: var(--color-scheme); }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--text-md);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* The limb below is wider than the viewport on purpose. `clip` rather than
     `hidden`: hidden would make the body a scroll container and the sticky
     header would stop sticking to the viewport. */
  overflow-x: clip;
}

/* ---------- The sky ---------- *
 * The ground is never a flat fill. Two washes stand for one cold light source
 * sitting behind the page, near and far; fixed, so scrolling moves the content
 * past the light instead of dragging the light along with it.
 */

.sky {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(58% 44% at 74% 4%, var(--halo-near), transparent 68%),
    radial-gradient(96% 60% at 12% -6%, var(--halo-far), transparent 64%);
}

/* The grid belongs to peripheral vision, so the mask erases it wherever the eye
   settles to read. A mask reads alpha and nothing else, so the opaque end is
   written `currentColor` — whatever colour that is, it means "keep this", and
   naming a colour there would be inventing a value that is never seen. */
.sky::after {
  content: ""; position: absolute; inset: -2px;
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
  background-size: 88px 88px;
  -webkit-mask-image: radial-gradient(72% 58% at 50% 34%, transparent 0%, currentColor 88%);
  mask-image: radial-gradient(72% 58% at 50% 34%, transparent 0%, currentColor 88%);
}

/* Everything the reader can touch sits above the light. */
header.top, main, footer { position: relative; z-index: 1; }

/* ---------- Switching theme ---------- *
 * A theme change is not a state change anything travelled to, so nothing
 * animates across it: the page is one thing before and another thing after.
 *
 * It is also load-bearing. A theme here flips `color-scheme`, and every colour
 * resolves from it through light-dark(); a transition already running on such a
 * property never re-resolves it and holds the previous theme's colour for good.
 * Freezing every transition for the frame the scheme flips is what makes the
 * toggle correct, not just calm.
 */
:root[data-theme-switching] *,
:root[data-theme-switching] *::before,
:root[data-theme-switching] *::after {
  transition: none !important;
}

/* ---------- Accessibility ---------- */

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--accent); color: var(--accent-ink);
  padding: var(--space-2) var(--space-4);
  border-radius: 0 0 var(--radius-sm) 0;
  text-decoration: none; font-size: var(--text-sm); font-weight: var(--weight-strong);
}
.skip-link:focus { left: 0; }
:focus-visible {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-sm);
}

/* ---------- Header ---------- */

/* Translucent over the ground rather than a solid band, so the page visibly runs
   underneath it. The bottom edge is lit from the wordmark outwards, because that
   is where the page's light comes from: a flat rule would read as the edge of a
   container, and there is no container. */
header.top {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--ground) 82%, transparent);
  backdrop-filter: saturate(150%) blur(14px);
  border-bottom: 1px solid transparent;
  border-image: linear-gradient(90deg,
      color-mix(in srgb, var(--accent) 55%, transparent), var(--border) 34%, transparent) 1;
}
.top-inner {
  max-width: var(--page-max); margin: 0 auto;
  padding: 0 var(--space-5);
  height: var(--header-h);
  display: flex; align-items: center; gap: var(--space-5);
}
.brand {
  font-family: var(--font-display);
  font-weight: var(--weight-display); font-size: var(--text-lg);
  letter-spacing: var(--tracking-tight);
  color: var(--text); text-decoration: none; white-space: nowrap;
  display: flex; align-items: center; gap: var(--space-3);
}
.brand-mark { width: 22px; height: 22px; flex-shrink: 0; display: block; }
.brand span { color: var(--accent); }

.top nav {
  display: flex; gap: var(--space-4); margin-left: auto; align-items: center;
  overflow-x: auto; scrollbar-width: none; position: relative;
}
.top nav::-webkit-scrollbar { display: none; }
/* Mono at label size: the navigation names things, it does not talk. */
.top nav a {
  color: var(--text-faint); text-decoration: none;
  font-family: var(--font-mono); font-size: var(--text-sm);
  white-space: nowrap; padding: var(--space-1) 0;
  border-bottom: 2px solid transparent;
  transition: color var(--dur-1) var(--ease-move);
}
/* Hover only ever brightens the label. The accent underline belongs to
   aria-current alone — a hover rule that also lit it double-pointed the
   moment a reader hovered a link on a different page, which identity
   decision 4 forbids. */
.top nav a:hover { color: var(--text); }
.top nav a[aria-current="page"] { color: var(--text); border-bottom-color: var(--accent); }

/* The rail: the header's one accent mark once script can measure the links.
   It starts at zero width, so no-JS and touch readers keep the static
   current-page underline above — header.js is what takes over from it. */
.nav-rail {
  position: absolute; left: 0; bottom: 0; height: 2px; width: 0;
  background: var(--accent); border-radius: var(--radius-pill);
  pointer-events: none;
}
/* The first placement is instant — a transition here would say something
   arrived, and the current page was already the current page. header.js
   adds this class only after that first placement. */
.nav-rail.is-ready {
  transition: transform var(--dur-2) var(--ease-move), width var(--dur-2) var(--ease-move);
}
@media (prefers-reduced-motion: reduce) {
  .nav-rail.is-ready { transition: none; }
}
/* Once the rail is live it is the header's one accent mark; leaving the
   static underline lit as well would double it up the instant a reader
   points at a different link — see design/components/site-header/spec.md. */
.top.nav-rail-on nav a[aria-current="page"] { border-bottom-color: transparent; }

.lang { display: flex; gap: var(--space-tight); flex-shrink: 0; }
.lang a {
  padding: var(--space-tight) var(--space-2); font-size: var(--text-xs); line-height: 1.6;
  font-family: var(--font-mono); font-weight: var(--weight-medium);
  color: var(--text-faint); text-decoration: none;
  border-radius: var(--radius-sm); border: 1px solid transparent;
}
.lang a:hover { color: var(--text); }
.lang a[aria-current="true"] { color: var(--accent); }

.theme-btn {
  background: none; border: 1px solid var(--border); border-radius: var(--radius-pill);
  width: 30px; height: 30px; cursor: pointer; color: var(--text-faint);
  font-size: var(--text-base); line-height: 1;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; padding: 0;
  transition: color var(--dur-1) var(--ease-move), border-color var(--dur-1) var(--ease-move);
}
.theme-btn:hover { border-color: var(--accent); color: var(--accent); }

/* ---------- Layout and type ---------- */

main { max-width: var(--page-max); margin: 0 auto; padding: 0 var(--space-5) var(--space-20); }
section { padding-top: var(--space-16); scroll-margin-top: calc(var(--header-h) + var(--space-4)); }

h1, h2, h3, .count b, .brand {
  font-family: var(--font-display);
}
h1 {
  font-size: var(--text-3xl); line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  margin: var(--space-12) 0 var(--space-4);
  font-weight: var(--weight-display);
}
h2 {
  font-size: var(--text-2xl); line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  margin: 0 0 var(--space-2); font-weight: var(--weight-display);
}
h3 {
  font-size: var(--text-lg); margin: var(--space-8) 0 var(--space-2);
  font-weight: var(--weight-strong); letter-spacing: var(--tracking-tight);
}
h4 {
  font-size: var(--text-base); margin: var(--space-5) 0 var(--space-1);
  font-weight: var(--weight-strong); color: var(--text-soft);
}

p { margin: 0 0 var(--space-4); color: var(--text-soft); }
main > p, section > p { max-width: var(--measure); }
strong { color: var(--text); font-weight: var(--weight-strong); }
a {
  color: var(--accent);
  text-decoration-thickness: 1px; text-underline-offset: 2px;
}
ul, ol { color: var(--text-soft); padding-left: var(--space-5); margin: 0 0 var(--space-4); max-width: var(--measure); }
li { margin-bottom: var(--space-1); }
li::marker { color: var(--accent); }

.section-intro {
  color: var(--text-soft); max-width: var(--measure);
  margin: 0 0 var(--space-6); font-size: var(--text-md);
}

/* A section heading gets the accent rule that says "this is a step of the argument". */
section > h2::before {
  content: ""; display: block;
  width: var(--space-8); height: 2px;
  background: var(--accent); border-radius: var(--radius-pill);
  margin-bottom: var(--space-4);
}

/* ---------- Page head ---------- *
 * The opening of every page that is not the home page: a kicker naming the
 * page's role in the argument (never the title restated), the h1, and a lead.
 * `--text-display` is the rung tokens.css reserved for exactly this — above
 * the documentation `h1` the rest of the site uses. The hero now sets its own
 * `h1` at the same rung, so size is no longer what separates them: the hero
 * keeps `--leading-hero` and `--tracking-hero`, which this block does not.
 */

.page-head { margin: var(--space-4) 0 var(--space-2); }
.page-head .kicker {
  display: flex; align-items: center; gap: var(--space-3);
  font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--text-faint); margin: 0 0 var(--space-5);
}
/* The horizon in miniature — the same idiom as the accent rule above every
   h2, spent here as a dash instead of a bar because it sits beside text
   rather than above it. Scoped to .page-head, not a bare .kicker: the class
   name is generic enough that a future component would otherwise inherit
   this layout and dash by accident. */
.page-head .kicker::before {
  content: ""; flex: none; width: var(--space-8); height: 1px;
  background: var(--accent);
}
.page-head h1 {
  font-size: var(--text-display); line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display); margin: 0;
}
.page-head .lead {
  font-size: var(--text-lg); line-height: var(--leading-snug);
  color: var(--text-soft); max-width: var(--measure);
  margin: var(--space-5) 0 0;
}

/* ---------- Hero ---------- */

.hero {
  position: relative;
  padding: var(--space-16) 0 var(--space-24);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: repeat(4, auto);
  column-gap: clamp(var(--space-8), 6vw, var(--space-20));
}

/* The limb: a planet's edge, lit from behind. It is what closes the hero — a rim
   light instead of a border, so the page ends on a horizon rather than on a line
   somebody drew. Wider than the viewport by design; body clips it.
   Drawn as ::after rather than as an element: it is decoration with no content,
   and a seventh child would shift the stagger indices the hero's arrival counts on. */
.hero::after {
  content: ""; position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
  width: 210vw; aspect-ratio: 2.4 / 1;
  border-radius: 50%;
  border-top: 1px solid var(--border);
  box-shadow: 0 -2px 90px -28px var(--limb-glow);
  pointer-events: none;
}
.eyebrow {
  grid-column: 1; grid-row: 1; justify-self: start;
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--accent); background: var(--accent-surface);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  padding: var(--space-1) var(--space-3); border-radius: var(--radius-pill);
  margin-bottom: var(--space-5);
}
.hero h1 {
  grid-column: 1; grid-row: 2;
  margin: 0; max-width: none;
  font-size: var(--text-display); line-height: var(--leading-hero);
  letter-spacing: var(--tracking-hero);
}
.hero .lead {
  grid-column: 1; grid-row: 3;
  font-size: var(--text-xl); line-height: var(--leading-snug);
  color: var(--text-soft); max-width: 44ch;
  margin: var(--space-10) 0 0;
}
.hero .cta-row { grid-column: 1; grid-row: 4; margin: var(--space-8) 0 0; }

.cta-row { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: var(--space-8) 0 var(--space-1); }
.btn {
  display: inline-block; padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-sm);
  font-size: var(--text-base); font-weight: var(--weight-strong);
  text-decoration: none; border: 1px solid transparent;
  transition: transform var(--dur-1) var(--ease-summon),
              background var(--dur-1) var(--ease-move),
              border-color var(--dur-1) var(--ease-move);
}
/* The only filled control in the hero. One accent means one thing points. */
.btn-primary { background: var(--accent); color: var(--accent-ink); box-shadow: var(--glow-accent); }
.btn-primary:hover { transform: translateY(-1px); }
.btn-ghost { border-color: var(--border-strong); color: var(--text); }
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
.btn:active { transform: translateY(0); }

/* ---------- Hop ---------- *
 * The mascot: four segments and one eye, built from the same segments the flow
 * diagrams use. `.hop-arrive` plays once, gated by the same `.js` class the
 * pre-paint script in head.html sets — so a reader with no JavaScript, a script
 * that failed, or `prefers-reduced-motion` sees Hop assembled and lit from the
 * first frame rather than mid-flight or invisible.
 */
.hero-figure {
  grid-column: 2; grid-row: 1 / 5;
  align-self: center; justify-self: end;
  position: relative; isolation: isolate;
  display: grid; place-items: center; margin: 0;
  width: clamp(160px, 20vw, 240px); aspect-ratio: 1;
}
/* The ring is lit from load, exempted from the summon fade every other direct
   child of `.hero` gets: an empty slot has to read as already there and
   waiting, not as a gap that later fills in. Only Hop's own assembly inside it
   — `.hop-arrive`, below — is the thing that arrives; the ring never does. */
.js .hero-figure { opacity: 1 !important; animation: none !important; }
/* The ring is the only closed shape in the hero, so Hop reads as a body at a
   distance instead of a sticker dropped on a dark background. */
.hero-figure::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: radial-gradient(circle at 50% 42%, var(--halo-near), transparent 64%);
}
.hero-figure .hop { position: relative; z-index: 1; width: 54%; height: auto; }

.hop { display: block; }
.hop-seg {
  fill: var(--surface-raised); stroke: var(--border-strong); stroke-width: 1.5;
  transform-box: fill-box; transform-origin: 50% 100%;
}
.hop-eye {
  fill: var(--accent);
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 65%, transparent));
  transform-box: fill-box; transform-origin: 50% 50%;
}
.hop-head { fill: var(--accent-surface); }
.hop-s1 { --dx: -9px; --sy: 2.29; }
.hop-s2 { --dx: -3px; --sy: 1.39; }
.hop-s3 { --dx:  1px; --sy: 1.23; }
.hop-s4 { --dx:  5px; --sy: 1.88; }

@keyframes hop-in {
  from { transform: translateX(var(--dx)) scaleY(var(--sy)); }
  to   { transform: none; }
}
@keyframes hop-eye-in {
  0%, 45%  { opacity: 0; transform: scale(0.3); }
  100%     { opacity: 1; transform: scale(1); }
}
.js .hop-arrive .hop-seg {
  animation: hop-in var(--dur-3) var(--ease-hop) both;
  animation-delay: calc(var(--i) * var(--stagger-hop));
}
.js .hop-arrive .hop-eye {
  animation: hop-eye-in var(--dur-4) var(--ease-move) both;
  animation-delay: var(--dur-2);
}
@media (prefers-reduced-motion: reduce) {
  .hop-seg, .hop-eye { animation: none !important; transform: none !important; opacity: 1 !important; }
}

/* ---------- Counts ---------- *
 * Plain numbers, not a card: the hero closes on a horizon, not a bordered box.
 */
.counts {
  grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; gap: clamp(var(--space-8), 5vw, var(--space-16));
  margin: clamp(var(--space-12), 7vh, var(--space-20)) 0 0;
  padding-top: var(--space-6);
}
.count b {
  display: block; font-weight: var(--weight-display);
  font-size: var(--text-2xl); letter-spacing: var(--tracking-tight);
  line-height: 1.1; color: var(--text); font-variant-numeric: tabular-nums;
}
.count span {
  display: block; color: var(--text-faint);
  font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
}

/* ---------- Principles ---------- */

.principles {
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  margin: var(--space-8) 0 0;
}
.principle { border-left: 2px solid var(--accent); padding: var(--space-1) 0 var(--space-1) var(--space-4); }
.principle b {
  display: block; color: var(--text); font-weight: var(--weight-strong);
  font-size: var(--text-base); margin-bottom: var(--space-tight);
}
.principle span { color: var(--text-soft); font-size: var(--text-base); line-height: var(--leading-snug); }

/* ---------- The run, as a horizon ---------- *
 * Seven steps standing on one line. Human copy above it, agent copy below, and
 * the two positions carry the three-of-seven ratio before a word is read. That
 * is also why this section refuses to scroll sideways: four of seven columns in
 * view makes the section's whole argument a lie until the reader drags.
 *
 * No wash anywhere here. #52 moved coral onto the agent, the abundant actor, so
 * a wash would paint the page's most common state as its loudest. Colour is
 * spent on the mark, the step title and the actor word — nothing wider.
 *
 * The rail below is the base state and the horizon is the enhancement, in that
 * order, because the horizon is only honest at --page-wide: measured, a column
 * narrower than that holds twelve to sixteen characters and every step runs to
 * four lines. One threshold, written once, at the bottom of this block.
 */

.run {
  margin: var(--space-10) 0 0;
  /* One mark size for the run and for the legend that explains it: a key drawn
     smaller than the thing it keys is a second component wearing the same name. */
  --mark: 11px;
  /* On the rail the legend comes first — the digits are the only place the ratio
     is countable when the seven marks are 900px apart. On the horizon it comes
     last, where it explains what the reader has just taken in. */
  display: flex; flex-direction: column;
}
.run-legend { order: -1; }

/* The rail: the same run turned on its side. Same marks, same order, same words
   — only the axis changes, so nothing has to be cut for it. The spine is drawn in
   --border-strong, not the page's hairline: at 1.29:1 against cream the scenery
   token reads as one more background grid line, and the branches hanging off it
   would be stronger than the line they hang from. */
.run-track { position: relative; padding-left: var(--space-8); }
.run-track::before {
  content: ""; position: absolute; left: 5px; top: 6px; bottom: 6px;
  width: 1px; background: var(--border-strong);
}
.run-horizon { display: none; }
.step { position: relative; padding-bottom: var(--space-8); }
.step-mark {
  position: absolute; left: calc(-1 * var(--space-8)); top: 4px;
  display: flex; align-items: center;
}

.step-dot {
  width: var(--mark); height: var(--mark); border-radius: 50%;
  background: var(--ground); border: 2px solid var(--agent);
  position: relative; z-index: 1;
}
/* Filled, not just recoloured. --human and --agent sit at nearly the same
   luminance in both themes, so two rings are the same mark in greyscale; solid
   versus hollow survives that, and the solid one is the rare actor. */
.step-human .step-dot { border-color: var(--human); background: var(--human); }

.step-no {
  display: block; margin-bottom: var(--space-1);
  font-family: var(--font-mono); font-size: var(--text-xs);
  color: var(--text-faint); letter-spacing: var(--tracking-label);
}
.step-title {
  font-size: var(--text-xl); line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight); font-weight: var(--weight-strong);
  margin: 0 0 var(--space-1);
}
.step-human .step-title { color: var(--human); }
/* This is the section's content, not a caption — the outcome is what the step
   leaves behind, so it gets the strongest ink on the step. */
.step-out { margin: 0; font-size: var(--text-base); color: var(--text-soft); line-height: var(--leading-snug); }
.step-out b { display: block; color: var(--text); font-weight: var(--weight-medium); }

/* The actor in words, on every step — the split has to survive being read by
   someone who cannot see the two hues, and by anyone holding a printout. An
   absent label is not the opposite of a present one; it just reads unfinished. */
.step-who {
  display: block; margin-top: var(--space-2);
  font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--agent);
}
.step-human .step-who { color: var(--human); }

.run-legend {
  display: flex; flex-wrap: wrap; gap: var(--space-6);
  margin: 0 0 var(--space-6); font-size: var(--text-sm); color: var(--text-faint);
}
.run-legend span { display: inline-flex; align-items: center; gap: var(--space-2); }
.run-legend b { color: var(--text-soft); font-weight: var(--weight-medium); }
/* The legend's marks are the run's own dots — same size, same fill: a key that
   draws a different shape from the thing it explains is a second component. */
.run-legend i {
  width: var(--mark); height: var(--mark); border-radius: 50%; flex: none;
  border: 2px solid var(--agent);
}
.run-legend .is-human i { border-color: var(--human); background: var(--human); }

/* --page-wide plus the two gutters main already reserves. Below it the rail above
   stands; at it and past it the run becomes the horizon and steps out of
   --page-max. A media query cannot read a custom property, so this 1440 and
   --page-wide's 1400 are two numbers that have to stay --space-5 apart: widen
   --page-wide alone and the run reaches past the viewport, where body's
   overflow-x: clip would hide the loss rather than show a scrollbar.
   tools/check_run_horizon.py asserts the run's own edges instead of trusting the
   pair. The breakout margin is computed from 100% — the width main actually
   gives the run — rather than from --page-max, so it stays exact whatever
   padding main carries, as long as main itself stays centred. */
@media (min-width: 1440px) {
  .run {
    width: var(--page-wide);
    margin-inline: calc((100% - var(--page-wide)) / 2);
  }
  .run-track {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    grid-template-rows: auto var(--space-12) auto;
    column-gap: var(--space-3);
    padding-left: 0;
  }
  /* The legend goes back under the run here, and back onto the page's own left
     edge: it is prose, and prose pinned to the breakout edge sits 160px left of
     every other sentence in the section. */
  .run-legend {
    order: 0; margin: var(--space-8) 0 0;
    padding-inline: calc((100% - var(--page-max)) / 2);
  }
  .run-track::before { content: none; }
  .run-horizon {
    display: block;
    grid-row: 2; grid-column: 1 / -1; align-self: center;
    height: 1px; width: 100%;
    background: linear-gradient(90deg, transparent, var(--border-strong) 6%, var(--border-strong) 94%, transparent);
  }
  /* display:contents lets one step own two cells in two different rows, so every
     mark lands on exactly the same y however tall the copy above it runs. */
  .step { display: contents; }
  .step-mark {
    position: static;
    grid-row: 2; grid-column: var(--c);
    align-self: center; justify-self: start;
    height: var(--space-12);
  }
  .step-body {
    grid-column: var(--c); grid-row: 3; align-self: start;
    border-left: 1px solid var(--border);
    padding: var(--space-5) 0 0 var(--space-3);
    margin-left: 5px;  /* the dot's own radius: the rule falls from its centre */
  }
  .step-human .step-body {
    grid-row: 1; align-self: end;
    padding: 0 0 var(--space-5) var(--space-3);
    border-left-color: color-mix(in srgb, var(--human) 45%, transparent);
  }
}

/* ---------- The three moments ---------- *
 * What the horizon leaves implicit: the three marks above the line, named. They
 * carry the human blue because they are the human's own section of the page.
 */

.moments {
  display: grid; gap: clamp(var(--space-6), 4vw, var(--space-12));
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin: clamp(var(--space-12), 7vh, var(--space-20)) 0 0;
}
.moment { border-left: 2px solid var(--human); padding-left: var(--space-4); }
.moment h3 {
  font-size: var(--text-lg); letter-spacing: var(--tracking-tight);
  font-weight: var(--weight-strong); color: var(--human);
  margin: 0 0 var(--space-2);
}
.moment p { margin: 0; color: var(--text-soft); font-size: var(--text-base); line-height: var(--leading-snug); }
@media (max-width: 760px) { .moments { grid-template-columns: minmax(0, 1fr); } }

/* ---------- Slots ---------- *
 * A slot is a question, drawn as one row of a typographic index rather than a
 * card — the card read as isolated claims; the index reads as one composition.
 * Native <details name="slot"> gives the accordion open/close for free: no
 * JavaScript writes or reads any state in this section, which is the whole
 * point of choosing it over a card that would need script to expand.
 * Filled and empty are two states of the same row — an empty one keeps its
 * dashed leader **and** spells `nothing installed`, because a dashed line
 * alone is not perceivable to everyone and "deliberately empty" is the idea
 * this section exists to carry.
 */

.slots { margin: var(--space-6) 0 0; }

/* ---------- Label register ---------- *
 * The mono, uppercase, faint-tracked register that names what the nearby
 * controls are for. Five declarations, five call sites — the call sites
 * consume the class and add only their own contextual layout. Documented as
 * design/components/label-register/spec.md; the variant API is the
 * attribute selector, after the site-header precedent. */

.label-register {
  font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--text-faint);
}
.label-register[data-weight="medium"] { font-weight: var(--weight-medium); }
.label-register[data-size="sm"] { font-size: var(--text-sm); }

/* The four declarations that tell the states apart are held as custom
   properties, so a row's state is one block that sets them rather than four
   selectors repeated per state. #56 added a third state — choosable — and
   without this it would have meant a third copy of the same four rules. */
.slot {
  --slot-name-color: var(--text);
  --slot-lead-style: dotted;
  --slot-lead-color: var(--border-strong);
  --slot-lead-opacity: 0.7;
  --slot-fill-color: var(--text-soft);
}
.slot[open] { --slot-fill-color: var(--accent); }
.slot-empty,
.slot-empty[open] {
  --slot-name-color: var(--text-faint);
  --slot-lead-style: dashed;
  --slot-lead-color: var(--border);
  --slot-lead-opacity: 1;
  --slot-fill-color: var(--text-faint);
}

.slot { border-top: 1px solid var(--border); }
.slot:last-of-type { border-bottom: 1px solid var(--border); }
.slot summary {
  list-style: none; cursor: pointer;
  padding-block: var(--space-5);
  transition: background var(--dur-2) var(--ease-move);
}
.slot summary::-webkit-details-marker { display: none; }
.slot summary:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
.slot summary:focus-visible { outline-offset: -2px; }

.slot-row {
  display: flex; align-items: center; gap: var(--space-4);
  margin: 0; font-weight: var(--weight-body); font-size: inherit; font-family: inherit;
}
.slot-sign {
  font-family: var(--font-mono); color: var(--text-faint);
  width: 1.2em; flex: none; text-align: center;
  transition: transform var(--dur-2) var(--ease-summon), color var(--dur-2);
}
.slot[open] .slot-sign { transform: rotate(45deg); color: var(--accent); }
.slot-name { color: var(--slot-name-color); width: 9.5rem; flex: none; }
.slot-q { color: var(--text-soft); }
.slot-lead {
  flex: 1 1 40px; min-width: 24px; height: 0;
  border-bottom: 1px var(--slot-lead-style) var(--slot-lead-color);
  opacity: var(--slot-lead-opacity);
}
.slot-fill {
  font-family: var(--font-mono); font-size: var(--text-sm);
  color: var(--slot-fill-color); flex: none; text-align: right;
}

.slot-answer {
  color: var(--text-soft);
  max-width: var(--measure);
  margin: 0 0 var(--space-6);
  padding-left: calc(1.2em + var(--space-4));
}
.js .slot[open] .slot-answer { animation: answer-in var(--dur-3) var(--ease-summon) both; }
@keyframes answer-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .js .slot[open] .slot-answer { animation: none; } }

@media (max-width: 800px) {
  .slot-row { flex-wrap: wrap; row-gap: var(--space-2); }
  .slot-name { width: auto; }
  .slot-lead { display: none; }
  .slot-q { flex: 1 1 100%; padding-left: calc(1.2em + var(--space-4)); }
  .slot-fill { text-align: left; }
  .slot-answer { padding-left: 0; }
  .slots-choose .slot-fill { justify-content: flex-start; }
}

/* ---------- Extends ---------- *
 * DECLARES / REGISTER / CONTRIBUTES — identity.md's labelled-column idiom, drawn
 * from the composition instead of a request. Every row carries its own labels,
 * like .slot-name — there is no shared header band, which reads as a second
 * <table> next to the real one three beats later. Mobile-first: the vertical
 * list AC-7 asks for is the base state, the three-column grid the enhancement.
 */

.ext-figure { margin: var(--space-8) 0 var(--space-6); --ext-label-w: 6.5rem; }

.ext-row { border-top: 1px solid var(--border); padding: var(--space-4) 0; display: grid; gap: var(--space-2); }
.ext-row:last-of-type { border-bottom: 1px solid var(--border); }

.ext-label {
  display: inline-block; min-width: var(--ext-label-w); flex: none;
}
.ext-declares { margin: 0; }
.ext-declares code, .ext-register code { color: var(--text); font-size: var(--text-base); }
.ext-skill { color: var(--text-soft); font-size: var(--text-sm); }

.ext-register { display: flex; align-items: center; gap: var(--space-3); margin: 0; }
.ext-lead { display: none; }

/* Flex, not the inline flow .ext-declares gets for free: a <ul> is a block, so
   without this its contributors start a new line at the row's own left edge
   instead of the column .ext-declares/.ext-register already established. */
.ext-contributes, .ext-contributes-none { display: flex; align-items: flex-start; gap: var(--space-3); margin: 0; }
.ext-contributes ul { margin: 0; padding: 0; list-style: none; }
.ext-contributes li { margin: 0 0 var(--space-1); }
/* A <code> chip, same as the declaring module's — one kind of object, one
   treatment, rather than the faint bare text a "several modules contribute"
   claim would otherwise render as the section's least confident-looking fact. */
.ext-contributes li code { font-size: var(--text-sm); }
/* .conn-none (AC-12's own empty case) draws the identical absence — one rule, not two. */
.ext-contributes-none, .conn-none { color: var(--text-faint); font-family: var(--font-mono); font-size: var(--text-sm); }

.ext-facts { margin: 0 0 var(--space-6); padding-left: var(--space-5); color: var(--text-soft); max-width: var(--measure); }
.ext-facts li { margin: 0 0 var(--space-2); line-height: var(--leading-snug); }
/* li::marker inherits --accent site-wide; --accent belongs to the invocation alone (ux.md). */
.ext-facts li::marker { color: var(--text-faint); }
.ext-facts code { color: var(--text); }

.ext-ask { margin: 0 0 var(--space-2); color: var(--text-soft); }

/* The complement of .slot-row's own `max-width: 800px` turn, so the two index
   figures on this page change layout at the same width rather than one lagging
   the other by a stray breakpoint. */
@media (min-width: 801px) {
  .ext-row { grid-template-columns: 1fr 1fr 2fr; gap: var(--space-4); align-items: start; }
  .ext-lead { display: block; flex: 1; height: 0; border-bottom: 1px dotted var(--border-strong); opacity: 0.7; }
  .ext-row-empty .ext-lead { border-bottom-style: dashed; border-bottom-color: var(--border); opacity: 1; }
}

/* ---------- Modules ---------- */

.modules { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
.mod {
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface); padding: var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-3);
  transition: border-color var(--dur-2) var(--ease-move), transform var(--dur-2) var(--ease-summon);
}
.mod:hover { border-color: var(--border-strong); transform: translateY(-2px); }
.mod-head { display: flex; align-items: flex-start; gap: var(--space-3); }
.mod-ico {
  font-size: var(--text-md); line-height: 1; width: 32px; height: 32px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-raised); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.mod-id { min-width: 0; flex-grow: 1; }
.mod-name {
  font-family: var(--font-mono); font-size: var(--text-base); font-weight: var(--weight-strong);
  color: var(--text); display: block; word-break: break-word;
}
/* .mod-slot right below already carries the card's one accent; the name's link
   stays text-coloured and underlined at rest so the two don't compete. */
a.mod-name:hover, a.mod-name:focus-visible { color: var(--accent); }
.mod-slot { font-size: var(--text-xs); color: var(--accent); font-family: var(--font-mono); }
.mod-desc { font-size: var(--text-base); color: var(--text-soft); line-height: var(--leading-snug); flex-grow: 1; margin: 0; }
.mod-foot { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.pill {
  font-size: var(--text-xs); font-family: var(--font-mono);
  padding: var(--space-tight) var(--space-2); border-radius: var(--radius-pill);
  border: 1px solid var(--border); color: var(--text-faint);
}
.pill-ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
.mod-cmd {
  font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-soft);
  background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: var(--space-2); overflow-x: auto; white-space: nowrap; width: 100%;
}

/* ---------- Code ---------- */

pre {
  background: var(--surface-sunken); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: var(--space-4); overflow-x: auto;
  font-family: var(--font-mono); font-size: var(--text-sm); line-height: var(--leading-body);
  margin: 0 0 var(--space-4); color: var(--text);
}
pre .c { color: var(--text-faint); }
pre .k { color: var(--accent); }
/* A module name inside a config file marks an actor, not a link: coral, not the
   accent, or every generated composition would be full of things that point. */
pre .m { color: var(--agent); }
code {
  font-family: var(--font-mono); font-size: 0.88em; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.08em 0.34em; color: var(--text);
}
pre code { background: none; border: 0; padding: 0; font-size: inherit; }
h2 code, h3 code, td code { font-size: 0.9em; }

/* ---------- Tables ---------- */

.table-wrap { overflow-x: auto; margin: 0 0 var(--space-5); -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; min-width: 440px; font-size: var(--text-base); }
th, td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); vertical-align: top; }
th {
  font-weight: var(--weight-strong); color: var(--text-faint);
  font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  border-bottom: 1px solid var(--border-strong); white-space: nowrap;
}
td { color: var(--text-soft); }
tbody tr:last-child td { border-bottom: 0; }

/* A module name is text at rest here, not accent — one page can name up to seven
   others (AC-12) plus a pairs_with line, and accent on every one of them is the
   pointing-inflation identity.md refuses. `code`'s own `color` (above) beats
   inheritance, so the child has to be told explicitly, same as .ext-declares. */
#plugs-into a, #pairs-with a { color: var(--text); }
#plugs-into a:hover, #plugs-into a:focus-visible,
#pairs-with a:hover, #pairs-with a:focus-visible { color: var(--accent); }
#plugs-into a code, #pairs-with a code { color: inherit; }

/* ---------- Cards ---------- */

.grid { display: grid; gap: var(--space-3); margin: 0 0 var(--space-5); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: var(--space-4) var(--space-5);
}
.card h3 { margin: 0 0 var(--space-2); font-size: var(--text-md); }
.card p { margin: 0; font-size: var(--text-base); line-height: var(--leading-snug); }
.card .tag {
  font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-faint);
  display: block; margin-bottom: var(--space-2);
}
.card-accent { border-left: 3px solid var(--accent); }

/* One rule for all three: every lead role is an agent, which is what coral now
   says. Three hues told them apart by asserting a falsehood — that the business
   role is a human — and their <h3> names already distinguish them. */
.card-manager, .card-business, .card-tech { border-left: 3px solid var(--agent); }

/* ---------- Notes ---------- */

.note {
  border-left: 3px solid var(--accent); background: var(--accent-surface);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  padding: var(--space-3) var(--space-5); margin: 0 0 var(--space-5);
}
.note p:last-child { margin-bottom: 0; }
.note-title {
  font-weight: var(--weight-strong); color: var(--text); font-size: var(--text-base);
  display: block; margin-bottom: var(--space-1);
}
.note-human { border-left-color: var(--human); background: var(--human-surface); }
.note-human .note-title { color: var(--human); }

/* ---------- Detailed steps ---------- */

.flow { margin: 0 0 var(--space-6); }
.flow-step {
  display: grid; grid-template-columns: 28px 1fr; gap: var(--space-4);
  position: relative; padding-bottom: var(--space-5);
}
.flow-step:last-child { padding-bottom: 0; }
.flow-step::before {
  content: ""; position: absolute; left: 13px; top: 28px; bottom: 0;
  width: 2px; background: var(--border);
}
.flow-step:last-child::before { display: none; }
.flow-num {
  width: 28px; height: 28px; border-radius: 50%; background: var(--surface);
  border: 1.5px solid var(--agent); display: flex; align-items: center;
  justify-content: center; font-family: var(--font-mono); font-size: var(--text-xs);
  font-weight: var(--weight-medium); color: var(--agent);
  position: relative; z-index: 1; flex-shrink: 0;
}
.flow-human .flow-num { border-color: var(--human); color: var(--human); background: var(--human-surface); }
.flow-body { padding-top: var(--space-tight); min-width: 0; }
.flow-body h4 { margin: 0 0 var(--space-1); color: var(--text); font-size: var(--text-md); font-weight: var(--weight-strong); }
.flow-body p {
  margin: 0 0 var(--space-2); font-size: var(--text-base);
  line-height: var(--leading-snug); max-width: var(--measure);
}
.flow-body p:last-child { margin-bottom: 0; }
.flow-body pre { margin-top: var(--space-3); margin-bottom: 0; }
.badge {
  display: inline-block; font-family: var(--font-mono); font-size: var(--text-xs);
  padding: var(--space-tight) var(--space-2); border-radius: var(--radius-sm);
  background: var(--agent-surface); border: 1px solid transparent; color: var(--agent);
  margin-left: var(--space-2); vertical-align: 1px; white-space: nowrap;
}
.badge-human { background: var(--human-surface); color: var(--human); }

/* ---------- Composer ---------- *
 * The same index as #slots, in its choosable state. What separates the two is
 * one thing and it is not decoration: a reported fill is a fact and stays
 * --text-soft; a chosen one is a decision, and decisions are the human's, so
 * they carry --human. Everything else — the row, the leader, the empty state —
 * is the component the slot index already ships.
 *
 * Nothing here needs a script. <details> opens, the inputs record the choice,
 * and :has() picks which pre-rendered fill the row shows. composer.js writes
 * the two files and nothing else.
 */

.presets { display: none; }
.js .presets {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  margin: 0 0 var(--space-2);
}
.presets-label { margin-right: var(--space-1); }
.preset {
  font-family: var(--font-body); font-size: var(--text-sm);
  padding: var(--space-1) var(--space-3); border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--surface); color: var(--text-soft);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-move), border-color var(--dur-1) var(--ease-move);
}
.preset:hover { border-color: var(--human); color: var(--human); }

/* A chosen fill is the reader's decision, and an unanswered slot cannot exist:
   the index loads on a decided composition, which is also what keeps the two
   files below correct with no script running. */
.slots-choose .slot { --slot-fill-color: var(--human); }
.slots-choose .slot[open] { --slot-fill-color: var(--human); }
.slots-choose .slot:has(.opt-empty input:checked),
.slots-choose .slot-multi:not(:has(.opt-mod input:checked)) {
  --slot-name-color: var(--text-faint);
  --slot-lead-style: dashed;
  --slot-lead-color: var(--border);
  --slot-lead-opacity: 1;
  --slot-fill-color: var(--text-faint);
}

/* A reporting row holds one fill and can afford `flex: none`. A choosable one
   holds several module names — the implementation row in French is the worst
   case — so it shrinks and wraps rather than pushing the row off its edge. */
.slots-choose .slot-fill {
  display: flex; flex-wrap: wrap; justify-content: flex-end;
  gap: 0 var(--space-3);
  flex: 0 1 auto; min-width: 0;
}
/* A chosen module is emphasised; `nothing installed` is not. Bold-but-faint on
   the one row whose message is absence sends two signals at once, and it would
   also draw the empty state differently here than in the reporting index. */
.slots-choose .fill-1, .slots-choose .fill-2, .slots-choose .fill-3,
.slots-choose .fill-4, .slots-choose .fill-5 { font-weight: var(--weight-strong); }
.slots-choose .slot:has(.opt-empty input:checked) .slot-fill > span { font-weight: var(--weight-body); }
.slots-choose .slot-q { flex: 1 1 auto; min-width: 0; }
.slots-choose .slot-fill > span { display: none; }
.slots-choose .slot:has(.opt-1 input:checked) .fill-1,
.slots-choose .slot:has(.opt-2 input:checked) .fill-2,
.slots-choose .slot:has(.opt-3 input:checked) .fill-3,
.slots-choose .slot:has(.opt-4 input:checked) .fill-4,
.slots-choose .slot:has(.opt-5 input:checked) .fill-5,
.slots-choose .slot-multi:not(:has(.opt-mod input:checked)) .fill-none { display: block; }

/* A fill arriving is a state change, so it is allowed to move. Summoned when a
   module answers the slot, resolved when the answer is "nothing" — the two
   gestures the identity gives to appearing and finishing. */
.slots-choose .slot-fill > span { animation: fill-in var(--dur-2) var(--ease-summon) both; }
.slots-choose .slot:has(.opt-empty input:checked) .slot-fill > span,
.slots-choose .slot-multi:not(:has(.opt-mod input:checked)) .fill-none {
  animation-timing-function: var(--ease-resolve);
}
@keyframes fill-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }

/* The sign glyph plus the row's gutter — one fact, named once rather than
   expressed inline wherever something has to line up under a question. Declared
   at the root, not on #composer-choices: a shelf also stands alone in the design
   system's previews, and an undefined var() there is an invalid declaration that
   silently falls to no indent at all. Reset at the same scope below 800px, where
   the leader is gone and nothing is left to line up under — scoping the reset
   narrower than the declaration would leave a shelf indented under a leader that
   is no longer drawn. */
:root { --shelf-indent: calc(1.2em + var(--space-4)); }
@media (max-width: 800px) { :root { --shelf-indent: 0px; } }
.shelf {
  display: grid; gap: var(--space-1);
  max-width: var(--measure); margin: 0 0 var(--space-6);
  padding-left: var(--shelf-indent);
}
.opt {
  display: grid; grid-template-columns: 16px 1fr; gap: var(--space-1) var(--space-3);
  padding: var(--space-2) var(--space-3); margin-left: calc(var(--space-3) * -1);
  border-radius: var(--radius-sm); cursor: pointer;
  transition: background var(--dur-1) var(--ease-move);
}
.opt:hover { background: var(--surface-raised); }
/* `> input`, not `input`: the free entry puts a text field inside an .opt, and a
   descendant selector would hand it the checkbox's 16px box. */
.opt > input { margin: var(--space-1) 0 0; accent-color: var(--human); width: 16px; height: 16px; }
.opt b { font-size: var(--text-base); font-weight: var(--weight-body); color: var(--text); }
.opt-1 b, .opt-2 b, .opt-3 b, .opt-4 b, .opt-5 b, .opt-add b { font-family: var(--font-mono); font-size: var(--text-sm); }
.opt-empty b, .opt-other b, .opt-free b { font-family: var(--font-body); font-size: var(--text-base); }
.opt span { grid-column: 2; font-size: var(--text-sm); color: var(--text-soft); line-height: var(--leading-snug); }
.opt:has(input:checked) { background: var(--human-surface); }
.opt:has(input:checked) b { color: var(--human); font-weight: var(--weight-strong); }

/* The gap idiom: an agent reporting what a composition cannot do. Mono, small,
   a hairline rule and no colour — an absence stated by the system is not a
   person deciding, so it takes no human blue, and no accent because it points
   at nothing. Recorded in design/components/slot-index/spec.md so the next use
   copies a named idiom rather than these declarations.
   `.opt-hint` carries it while a repeating slot is empty — those two rows have
   no `leave it empty` option, since checking nothing is that choice — and
   `.composer-note` carries it under the two files. */
.opt-hint,
.composer-note,
.key-entry-refused {
  padding-left: var(--space-4);
  border-left: var(--space-tight) solid var(--border-strong);
  font-family: var(--font-mono); font-size: var(--text-xs);
  line-height: var(--leading-snug); color: var(--text-soft);
  max-width: var(--measure);
}

.opt-hint { display: none; margin: var(--space-2) 0 0; }
.slots-choose .slot-multi:not(:has(.opt-mod input:checked)) .opt-hint { display: block; }

/* ---------- Additions ---------- *
 * A shelf without a row (AC-9): no sign, no leader, no fill, no <details>. The
 * modules that fill no slot are not a choice between alternatives, so they get
 * none of the anatomy that exists to frame one. The heading is what stops the
 * shelf reading as the seventh row's open body — and it wears the label register
 * .presets-label already uses, not slot-name's: borrowing the row's typography
 * borrows the row. It pulls its own weight down from an <h3>'s, which is the one
 * thing it does not share with that span.
 */
.additions { margin: var(--space-8) 0 0; padding-left: var(--shelf-indent); }
.additions .shelf { padding-left: 0; margin-bottom: 0; }
.shelf-label { margin: 0 0 var(--space-2); }
.additions-intro {
  margin: 0 0 var(--space-3); max-width: var(--measure);
  font-size: var(--text-sm); color: var(--text-soft);
  line-height: var(--leading-snug);
}

/* ---------- The free entry: design/components/key-entry ---------- *
 * The one control here a script must be running for — text cannot become YAML
 * through :has(), so without script it is absent rather than a field that
 * silently does nothing. Gated on .has-js, never .js: that one also means the
 * reader wants motion, and a motion preference must cost no capability.
 */
.opt-free { display: none; }
.has-js .opt-free { display: grid; cursor: default; }

/* A <div>, not a <label>: a label wrapping the text field would toggle the
   checkbox on every click into the field. The clickable region is the head. */
.opt-free > .opt-free-head {
  grid-column: 2; display: grid; gap: var(--space-1); cursor: pointer;
}
.opt-free-head > span { grid-column: 1; }
.opt-free > .key-entry { grid-column: 2; }

.key-entry { display: none; min-width: 0; }
/* Revealed on demand is what keeps the section a composition rather than a form,
   and a thing that appears because you asked for it is summoned. */
.opt-free:has(> input:checked) .key-entry {
  display: grid; gap: var(--space-1); margin-top: var(--space-2);
  animation: fill-in var(--dur-2) var(--ease-summon) both;
}

/* .key-entry-label takes the typography from .label-register; the class is
   the whole rule. */

/* Every UA default is overwritten: an input keeping its browser chrome is the
   one thing in this section that would genuinely read as an OS form.
   min-width: 0 is load-bearing — a text input's intrinsic min-content width
   overflows .opt's 1fr track below roughly 420px without it. */
.key-entry-field {
  width: 100%; min-width: 0; box-sizing: border-box;
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-mono); font-size: var(--text-sm);
  color: var(--text); background: var(--surface-sunken);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  caret-color: var(--human);
  transition: border-color var(--dur-1) var(--ease-move);
}
.key-entry-field::placeholder { color: var(--text-faint); opacity: 1; }

/* Refused. Dashed is this system's mark for a decided absence — the empty slot's
   leader — so a key that will not be emitted wears it, and the --human wash is
   withdrawn because the decision did not land. No new colour: a refusal is
   neither a person deciding nor an agent running, so every token that could
   carry it would be naming the wrong thing. */
.key-entry-field[aria-invalid="true"] { border-style: dashed; }
.opt-free:has(.key-entry-field[aria-invalid="true"]) { background: none; }
.opt-free:has(.key-entry-field[aria-invalid="true"]) b {
  color: var(--text); font-weight: var(--weight-body);
}
.key-entry-refused { display: none; margin: var(--space-1) 0 0; }
.opt-free:has(.key-entry-field[aria-invalid="true"]) .key-entry-refused { display: block; }

/* ---------- The two files you take away ---------- */

.artifacts {
  display: grid; gap: var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  margin: var(--space-6) 0 0;
}
.artifact { min-width: 0; }
.artifact-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-2);
  font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
}
.artifact-name { color: var(--text); text-transform: uppercase; }
.artifact-role { color: var(--text-faint); flex: 1 1 auto; letter-spacing: normal; text-transform: none; }
.artifact pre { margin: 0; max-height: 380px; overflow: auto; }

/* Not a note in a box: a report of what the composition implies. */
.composer-note { margin: var(--space-5) 0 0; }
.composer-note:empty { display: none; }
.composer-note span { display: block; }
.composer-note span + span { margin-top: var(--space-2); }

/* Announced, not shown: the rows and the files already say it on screen. */
.composer-live {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; margin: 0;
}

.copy-btn {
  font-size: var(--text-xs); font-family: var(--font-mono);
  padding: var(--space-1) var(--space-3); border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong); background: none; color: var(--text-soft);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-move), border-color var(--dur-1) var(--ease-move);
}
.copy-btn:hover { border-color: var(--accent); color: var(--accent); }

/* ---------- Footer ---------- */

/* No fill: the footer is separated from the page by emptiness, like every other
   boundary here, and the sky carries on underneath it. */
footer {
  border-top: 1px solid var(--border); margin-top: var(--space-section);
  padding: var(--space-12) var(--space-5) var(--space-16);
  color: var(--text-faint); font-size: var(--text-base);
}
.footer-inner {
  max-width: var(--page-max); margin: 0 auto; display: flex;
  flex-wrap: wrap; gap: var(--space-3); justify-content: space-between;
}
footer a { color: var(--text-soft); text-decoration: none; }
footer a:hover { color: var(--accent); }

/* ---------- Motion ---------- *
 * Every rule that hides something is gated on `.js`, a class set by the inline
 * script in <head> before the first paint — and set only when a script is
 * running that can also do the un-hiding, and when the reader has not asked for
 * reduced motion. No JavaScript, a script that failed, an old browser, or
 * prefers-reduced-motion: the class is absent and nothing here matches, so the
 * page renders complete. That is the only acceptable failure mode for an
 * animation whose whole job is to say "this arrived".
 *
 * The gate is inline rather than in motion.js on purpose: an external file that
 * hides content arrives after the first paint, so the reader sees the content,
 * then sees it vanish, then sees it come back.
 */

@keyframes summon {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes unhide { to { opacity: 1; } }

/* The hiding carries its own expiry. The gate proves a script *can* un-hide, not
   that it will ever arrive: the HTML can land and the connection drop before
   motion.js is fetched, and then no timeout inside that file will ever run. So
   the un-hiding of last resort is in the stylesheet that did the hiding. */
.js .summon > * {
  opacity: 0;
  animation: unhide 0s linear 6s forwards;
}
.js .summon.is-in > * {
  animation: summon var(--dur-3) var(--ease-summon) both;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

/* The page itself is a summoned thing: eyebrow, claim, lead, next step,
   numbers — in DOM order, which is also arrival order. nth-child(5) (the
   figure) still gets an --i, but the figure itself is exempted from the fade
   above (see .hero-figure): the ring is lit before anything is summoned, and
   only Hop's own assembly plays inside it, on its own gate. */
.js .hero.is-in > *:nth-child(2) { --i: 1; }
.js .hero.is-in > *:nth-child(3) { --i: 2; }
.js .hero.is-in > *:nth-child(4) { --i: 3; }
.js .hero.is-in > *:nth-child(5) { --i: 4; }
.js .hero.is-in > *:nth-child(6) { --i: 5; }

/* The run arrives left to right: the sequence is the content. A .step is
   display:contents and generates no box, so it cannot be faded itself — the two
   cells it owns are what arrive, and they inherit the step's own --i, which is
   what keeps the stagger travelling along the horizon rather than down it. */
.js .run-track.summon > .step > * {
  opacity: 0;
  animation: unhide 0s linear 6s forwards;
}
/* A step is a pass-through in both layouts — display:contents on the horizon, a
   plain box on the rail — so it never arrives itself. Without this the rail would
   fade the same pixels twice, once through the step and once through its cells. */
.js .run-track.summon > .step,
.js .run-track.summon.is-in > .step { opacity: 1; animation: none; }
.js .run-track.summon.is-in > .step > * {
  animation: summon var(--dur-3) var(--ease-summon) both;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}
/* The marks only fade, and so does the line under them. A dot that travelled
   would leave the line it stands on — and a line that travels under dots that do
   not is the same defect drawn the other way round. */
@keyframes land { from { opacity: 0; } to { opacity: 1; } }
.js .run-track.summon.is-in > .step > .step-mark,
.js .run-track.summon.is-in > .run-horizon { animation-name: land; }

/* The one flare on the page. The human mark stands for "this one waits for
   you", so its arrival is the single state change worth a pulse — and the human
   is the rare actor, three of seven. The agent steps never bloom: what is
   abundant does not get to flare. */
@keyframes bloom {
  from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--human) 55%, transparent); }
  to   { box-shadow: 0 0 0 14px transparent; }
}
.js .run-track.summon.is-in > .step-human .step-dot {
  animation: bloom var(--dur-4) var(--ease-resolve) both;
  animation-delay: calc(var(--i, 0) * var(--stagger) + var(--dur-2));
}

.js .slots.is-in > .slot:nth-child(2) { --i: 1; }
.js .slots.is-in > .slot:nth-child(3) { --i: 2; }
.js .slots.is-in > .slot:nth-child(4) { --i: 3; }
.js .slots.is-in > .slot:nth-child(5) { --i: 4; }
.js .slots.is-in > .slot:nth-child(6) { --i: 5; }
.js .slots.is-in > .slot:nth-child(7) { --i: 6; }

/* The two rows of the extends figure, then the invocation before the table it
   produces — the same DOM-order-is-arrival-order rule as the hero and the run. */
.js .ext-figure.is-in > *:nth-child(2) { --i: 1; }
.js .ext-invoke.is-in > *:nth-child(2) { --i: 1; }
.js .ext-invoke.is-in > *:nth-child(3) { --i: 2; }

@media (prefers-reduced-motion: reduce) {
  /* Reveal without movement, and drop every transition to a single frame —
     the state change still reads, it just stops travelling. */
  .js .summon > *,
  .js .summon.is-in > *,
  /* the run's cells are grandchildren, and its dot a great-grandchild: a step
     generates no box, so the blanket reset above never reaches down to them */
  .js .run-track.summon > .step > *,
  .js .run-track.summon.is-in > .step > *,
  .js .run-track.summon.is-in > .step > .step-mark,
  .js .run-track.summon.is-in > .step-human .step-dot { opacity: 1; animation: none; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .btn:hover, .mod:hover { transform: none; }
}

/* ---------- Narrow screens ---------- */

/* Hop moves above the headline rather than beside it: at this width the ring
   competes with the text for a column neither can spare. */
@media (max-width: 860px) {
  .hero { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; }
  .hero-figure {
    grid-column: 1; grid-row: 1; justify-self: start;
    width: clamp(120px, 32vw, 180px);
    margin: 0 0 var(--space-4);
  }
  .eyebrow { grid-row: 2; }
  .hero h1 { grid-row: 3; }
  .hero .lead { grid-row: 4; }
  .hero .cta-row { grid-row: 5; }
  .counts { grid-row: 6; }
}

@media (max-width: 640px) {
  .top-inner { gap: var(--space-3); }
  .top nav { gap: var(--space-3); }
  .hero { padding-top: var(--space-10); }
  .hero .lead { font-size: var(--text-lg); }
}
