/* ═══════════════════════════════════════════════════════════════════════════
   updates — the reading skin.

   Deliberately not css/buttercup.css: that file is the machine, and gets
   inlined into one page by build.mjs. These are plain documents, so they carry
   their own copy of the palette and nothing else. Same colours, same tube.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  color-scheme: light dark;

  --ink:        light-dark(#4a3610, #ffd873);
  --ink-hot:    light-dark(#1c1203, #fff6d4);
  --ink-dim:    light-dark(#7d6528, #d3a955);
  --ink-faint:  light-dark(#9c8449, #907338);
  --paper:      light-dark(#fdf3d5, #191204);
  --paper-2:    light-dark(#f8e9c1, #241a09);
  --paper-3:    light-dark(#f2ddaa, #30230e);
  --rule:       light-dark(#d3ba82, #6a5122);
  --good:       light-dark(#2f6b26, #a6e078);
  --bloom:      light-dark(#fffbea, #3a2b0e);
  --edge:       light-dark(#eddfb4, #0d0903);
  --veil-line:  light-dark(rgba(0,0,0,.05), rgba(0,0,0,.20));
  --veil-edge:  light-dark(rgba(0,0,0,.10), rgba(0,0,0,.50));

  --mono: "Berkeley Mono", "IBM Plex Mono", "SFMono-Regular", Menlo, Consolas,
          "DejaVu Sans Mono", monospace;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  background-image:
    radial-gradient(120% 60% at 50% 0%, var(--bloom) 0%, var(--paper) 55%, var(--edge) 100%);
  background-attachment: fixed;
  color: var(--ink-dim);
  font: 15px/1.65 var(--mono);
}

/* Scanlines and vignette, same dressing as the harness — decoration only. */
.crt {
  position: fixed; inset: 0; z-index: 9; pointer-events: none;
  background:
    repeating-linear-gradient(to bottom,
      var(--veil-line) 0 1px, rgba(0,0,0,0) 1px 3px),
    radial-gradient(130% 100% at 50% 50%,
      rgba(0,0,0,0) 60%, var(--veil-edge) 100%);
  mix-blend-mode: multiply;
}

.sheet {
  position: relative; z-index: 1;
  max-width: 44rem; margin: 0 auto; padding: 1rem 1rem 5rem;
}

/* ── title bar ────────────────────────────────────────────────────────────── */

.titlebar {
  display: flex; align-items: baseline; gap: .8rem; flex-wrap: wrap;
  border: 2px solid var(--rule); border-bottom-width: 3px;
  background: linear-gradient(var(--paper-3), var(--paper-2));
  padding: .5rem .7rem; margin-bottom: 2rem;
}
.titlebar a.home {
  color: var(--ink-hot); font-weight: 600; letter-spacing: .04em;
  text-decoration: none;
}
.titlebar a.home:hover { text-decoration: underline; text-underline-offset: 3px; }
.titlebar .where {
  color: var(--ink-faint); font-size: .72rem; letter-spacing: .2em;
  text-transform: uppercase; margin-left: auto;
}
.lamps { display: inline-flex; gap: .3rem; }
.lamps i {
  width: .55rem; height: .55rem; border: 1px solid var(--rule); border-radius: 50%;
  background: var(--paper); display: block;
}
/* the power lamp, on the same 8s clock as the diagrams below it */
.lamps i:first-child { animation: lamp 8s ease-in-out infinite; }
@keyframes lamp {
  0%, 100% { background: var(--paper); box-shadow: none }
  50%      { background: var(--good);
             box-shadow: 0 0 .35rem var(--good) }
}
/* (frozen state for this lives with the diagrams, under `data-motion="off"`) */

/* ── prose ────────────────────────────────────────────────────────────────── */

article header { border-bottom: 1px dashed var(--rule); padding-bottom: 1rem; }
h1 {
  color: var(--ink-hot); font-size: 1.5rem; line-height: 1.3;
  margin: 0 0 .5rem; letter-spacing: -.01em;
}
/* the heading carries its own rule out to the margin — cheaper than an <hr>
   and it keeps the section breaks obvious at a glance while scrolling */
h2 {
  display: flex; align-items: center; gap: .7rem;
  color: var(--ink-hot); font-size: 1rem; letter-spacing: .12em;
  text-transform: uppercase; margin: 2.5rem 0 .75rem;
}
/* `0 2rem` basis so the rule is always drawn, even next to the longest
   heading, rather than collapsing to nothing */
h2::after { content: ""; flex: 1 0 2rem; border-top: 1px dashed var(--rule); }
time, .kicker {
  display: block; color: var(--ink-faint);
  font-size: .74rem; letter-spacing: .2em; text-transform: uppercase;
}
p { margin: 1rem 0; }
strong { color: var(--ink-hot); font-weight: 600; }
em { color: var(--ink); font-style: italic; }
a { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; }
a:hover { color: var(--ink-hot); }
code {
  color: var(--ink); background: var(--paper-2);
  border: 1px solid var(--rule); padding: 0 .25rem;
}
pre {
  border: 1px dashed var(--rule); border-left: 3px solid var(--rule);
  background: var(--paper-2);
  padding: .7rem .8rem; overflow-x: auto; color: var(--ink);
  font-size: .82rem; line-height: 1.5;
}
pre code { border: 0; padding: 0; background: none; }
ul { padding-left: 1.2rem; }
li { margin: .4rem 0; }
li::marker { color: var(--ink-faint); }
hr { border: 0; border-top: 1px dashed var(--rule); margin: 2.5rem 0; }

blockquote {
  margin: 1.5rem 0; padding: .1rem 0 .1rem 1rem;
  border-left: 3px solid var(--rule); color: var(--ink);
}

table { border-collapse: collapse; width: 100%; font-size: .85rem; }
th, td { border: 1px solid var(--rule); padding: .35rem .5rem; text-align: left; }
th { color: var(--ink-hot); background: var(--paper-2); font-weight: 600; }

/* ── animated diagrams ────────────────────────────────────────────────────────
   Five figures, all pure CSS on one shared 8s clock so they read as the same
   machine ticking. No JS, no SVG, no images — a diagram that needs a build step
   is a diagram nobody updates. Every figure also has a legible static state,
   kept in the `data-motion="off"` block at the end of this section, so the
   motion is never load-bearing — but it is on by default for everyone,
   including OS Reduce Motion. See that block for why. */

.diag {
  position: relative;
  margin: 1.8rem 0; padding: 1.1rem .9rem .7rem;
  border: 1px dashed var(--rule); background: var(--paper-2);
  /* graph paper, one cell per 14px, faint enough to read as texture rather
     than as a grid you are meant to count */
  background-image:
    linear-gradient(var(--veil-line) 1px, rgba(0,0,0,0) 1px),
    linear-gradient(90deg, var(--veil-line) 1px, rgba(0,0,0,0) 1px);
  background-size: 14px 14px, 14px 14px;
}
/* corner ticks: the panel reads as an instrument face, not a div */
.diag::before, .diag::after {
  content: ""; position: absolute; width: .5rem; height: .5rem;
  border: 1px solid var(--ink-faint); pointer-events: none;
}
.diag::before { top: -1px; left: -1px; border-width: 2px 0 0 2px; }
.diag::after  { top: -1px; right: -1px; border-width: 2px 2px 0 0; }
.diag figcaption {
  margin-top: .9rem; padding-top: .6rem; border-top: 1px dashed var(--rule);
  font-size: .74rem; line-height: 1.5; color: var(--ink-faint);
}
.diag figcaption b { color: var(--ink); font-weight: 600; }

/* ‥ figure 1: the loop ‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥
   Four stations in a ring. The middle column is only as wide as an arrow, so
   the two 1fr columns carry the boxes. Source order runs left-to-right by row,
   which is also reading order for a screen reader with the labels unhidden. */

.ring {
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; justify-items: center; gap: .35rem;
}
.node {
  position: relative; width: 100%;
  border: 1px solid var(--rule); background: var(--paper);
  padding: .5rem .55rem; text-align: center;
  font-size: .76rem; line-height: 1.4; color: var(--ink);
}
.node b {
  display: block; margin-bottom: .15rem;
  color: var(--ink-hot); font-size: .64rem;
  letter-spacing: .16em; text-transform: uppercase;
}
.node code { font-size: .72rem; }
/* the station light: an inset ring that flares as the pulse arrives */
.node::after {
  content: ""; position: absolute; inset: -3px; opacity: 0;
  border: 2px solid var(--good); pointer-events: none;
}
/* two animations per station on one delay: the ring flares outside the box,
   the box itself warms. Split so each can carry its own easing. */
@keyframes station {
  0%        { opacity: 0; inset: 1px }
  6%        { opacity: 1; inset: -4px }
  11%       { opacity: 1; inset: -3px }
  26%, 100% { opacity: 0; inset: -3px }
}
@keyframes stationlit {
  0%, 26%, 100% { background: var(--paper);   box-shadow: none }
  4%, 20%       { background: var(--paper-3);
                  box-shadow: 0 0 0 1px var(--good) inset }
}
/* both shorthands first — a shorthand resets `animation-delay`, so the
   per-station delays below must be the last word on the subject */
.ring .node { animation: stationlit 8s linear infinite; }
.ring .node::after { animation: station 8s ease-out infinite; }
.ring .n1, .ring .n1::after { animation-delay: -0.2s; }
.ring .n2, .ring .n2::after { animation-delay:  1.8s; }
.ring .n3, .ring .n3::after { animation-delay:  3.8s; }
.ring .n4, .ring .n4::after { animation-delay:  5.8s; }
/* station 3 is the one the lesson is about — it stays marked when unlit */
.ring .n3 { border-left: 3px solid var(--good); }

/* the wires. `.h` runs across the middle column, `.v` down a 1fr column. */
.wire { position: relative; color: var(--ink-faint); font-size: .66rem; }
/* wire labels sit outside the flow, so the middle column has to be wide enough
   to hold the longest of them (`tool_result`) or they run under the boxes */
.wire i {
  position: absolute; font-style: normal; font-size: .58rem;
  letter-spacing: .06em; white-space: nowrap; color: var(--ink-faint);
}
/* the track. A gradient rather than a dashed border, because only a gradient
   can march: `background-position` animates, `border-style` does not. */
.wire::before { content: ""; position: absolute; }
@keyframes ants { to { background-position: 8px 8px } }
.wire::before { animation: ants 1.1s linear infinite; }
/* the two return legs flow the other way, so their ants do too */
.wire.w3::before, .wire.w4::before { animation-direction: reverse; }
/* the pulse. Hidden by default so it does not park on a wire during its
   animation-delay — each travel keyframe fades it in for its own leg only. */
.wire::after {
  content: "▸"; position: absolute; opacity: 0; color: var(--good);
  font-size: .8rem; line-height: 1;
  text-shadow: 0 0 .4rem var(--good);
}

.wire.h { width: 5rem; height: 1.4rem; }
.wire.h::before {
  top: 50%; left: 0; right: 0; height: 1px;
  background: repeating-linear-gradient(90deg,
    var(--rule) 0 4px, rgba(0,0,0,0) 4px 8px);
}
.wire.h::after  { top: 50%; transform: translate(-50%, -50%); }
.wire.h i { bottom: -.15rem; left: 50%; transform: translateX(-50%); }
@keyframes runx {
  0%   { left: 0;    opacity: 1 }
  14%  { left: 100%; opacity: 1 }
  14.01%, 100% { left: -2rem; opacity: 0 }
}
@keyframes runx-back {
  0%   { left: 100%; opacity: 1 }
  14%  { left: 0;    opacity: 1 }
  14.01%, 100% { left: 120%; opacity: 0 }
}
.wire.w1::after { animation: runx 8s linear infinite 0.6s; }
/* the return leg travels right-to-left, so it points the way it is going */
.wire.w3::after { content: "◂"; animation: runx-back 8s linear infinite 4.6s; }

.wire.v { width: 100%; height: 2.1rem; }
.wire.v::before {
  left: 50%; top: 0; bottom: 0; width: 1px;
  background: repeating-linear-gradient(180deg,
    var(--rule) 0 4px, rgba(0,0,0,0) 4px 8px);
}
.wire.v::after  { left: 50%; transform: translate(-50%, -50%); }
.wire.v i { top: 50%; transform: translateY(-50%); left: calc(50% + .6rem); }
@keyframes runy {
  0%   { top: 0;    opacity: 1 }
  14%  { top: 100%; opacity: 1 }
  14.01%, 100% { top: -2rem; opacity: 0 }
}
.wire.w2::after { content: "▾"; animation: runy 8s linear infinite 2.6s; }
@keyframes runy-back {
  0%   { top: 100%; opacity: 1 }
  14%  { top: 0;    opacity: 1 }
  14.01%, 100% { top: 120%; opacity: 0 }
}
.wire.w4::after {
  content: "▴"; animation: runy-back 8s linear infinite 6.6s;
}
.wire.w4 i { left: auto; right: calc(50% + .6rem); }
.ring .gap { width: 100%; }

/* ‥ figure 2: the message tape ‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥
   The same four turns as the ring, but as the array you are actually building.
   Rows land one at a time on the shared clock, then the tape wipes and refills. */

.tape { list-style: none; margin: 0; padding: 0; counter-reset: turn; }
.tape li {
  margin: 0 0 .3rem; padding: .4rem .55rem .4rem 1.9rem;
  position: relative; border: 1px solid var(--rule); background: var(--paper);
  font-size: .74rem; line-height: 1.45; color: var(--ink);
  opacity: .16;
}
.tape li::before {
  counter-increment: turn; content: counter(turn);
  position: absolute; left: .55rem; top: .4rem;
  color: var(--ink-faint); font-size: .66rem;
}
.tape b {
  color: var(--ink-hot); font-weight: 600;
  font-size: .64rem; letter-spacing: .14em; text-transform: uppercase;
}
.tape .k { color: var(--good); }
.tape li:nth-child(3) { border-left: 3px solid var(--good); }
/* rows land from the left, flash as they land, then hold */
@keyframes arrive {
  0%      { opacity: .16; transform: translateX(-.5rem);
            border-color: var(--rule); background: var(--paper) }
  5%      { opacity: 1;   transform: translateX(.12rem);
            border-color: var(--good); background: var(--paper-3) }
  11%     { opacity: 1;   transform: none;
            border-color: var(--good); background: var(--paper-3) }
  22%, 92% { opacity: 1;  transform: none;
            border-color: var(--rule); background: var(--paper) }
  100%    { opacity: .16; transform: translateX(-.5rem);
            border-color: var(--rule); background: var(--paper) }
}
.tape li { animation: arrive 8s cubic-bezier(.2,.9,.3,1) infinite; }
.tape li:nth-child(1) { animation-delay: 0.2s; }
.tape li:nth-child(2) { animation-delay: 2.0s; }
.tape li:nth-child(3) { animation-delay: 4.0s; }
.tape li:nth-child(4) { animation-delay: 6.0s; }
/* the id contract, shown rather than asserted: the id in turn 2 and the id in
   turn 3 flare together at the moment turn 3 lands. This is the one thing in
   the figure the caption is about, so it gets the only colour. */
.tape .id { color: var(--good); }
@keyframes match {
  0%, 46%, 62%, 100% { background: var(--paper-2); border-color: var(--rule) }
  50%, 58%           { background: var(--good);    border-color: var(--good);
                       color: var(--paper) }
}
.tape .id { animation: match 8s linear infinite 0.2s; }

/* ‥ figure 3: schema in, arguments out ‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥
   Left is what you wrote; right types itself out, because the arguments are
   generated text and it helps to feel that. `steps()` counts characters, so the
   `ch` width and the step count must match the string — 17 of each here. */

.pair { display: grid; grid-template-columns: 1fr auto 1fr; gap: .6rem;
        align-items: center; }
.pair .side { min-width: 0; align-self: stretch; }
.pair .side > b {
  display: block; margin-bottom: .3rem;
  color: var(--ink-hot); font-size: .64rem;
  letter-spacing: .16em; text-transform: uppercase;
}
.pair pre { margin: 0; font-size: .72rem; }
/* the crossing: schema in, arguments out. Ticks over on the shared clock so
   the eye is pushed rightwards just before the typing starts. */
.pair .cross {
  align-self: stretch; position: relative; width: 1.4rem;
  display: grid; place-items: center;
  font-size: 1.05rem; line-height: 1; color: var(--ink-faint);
  animation: shove 8s ease-in-out infinite;
}
/* the seam between the two panels, broken in the middle so the arrow sits in
   the gap rather than on top of the line */
.pair .cross::before {
  content: ""; position: absolute; left: 50%; top: .2rem; bottom: .2rem;
  width: 1px; background: linear-gradient(180deg,
    var(--rule) 0 42%, rgba(0,0,0,0) 42% 58%, var(--rule) 58% 100%);
}
/* colour only — the seam is part of this element, so moving it would drag the
   line with the arrowhead */
@keyframes shove {
  0%, 44%, 100% { color: var(--ink-faint); text-shadow: none }
  8%, 34%       { color: var(--good); text-shadow: 0 0 .45rem var(--good) }
}
.pair .out {
  /* content-box so the 17ch is the text alone — padding and the caret border
     must not eat into the count, or the last characters never appear */
  box-sizing: content-box;
  overflow: hidden; white-space: nowrap; width: 17ch;
  border-right: .5rem solid var(--good);
  animation: typeout 8s steps(17, end) infinite, caret .9s step-end infinite;
}
@keyframes typeout { 0% { width: 0 } 55%, 100% { width: 17ch } }
@keyframes caret { 0%, 100% { border-right-color: var(--good) }
                   50% { border-right-color: transparent } }

/* ‥ figure 4: the syllabus ladder ‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥
   Six rungs, easiest at the top. A green line climbs the track and each rung
   lights as it is passed, then the whole thing winks out top-down and refills —
   the point being that this is a climb with an order, not a menu. */

.ladder {
  position: relative; list-style: none; counter-reset: rung;
  margin: 0; padding: 0 0 0 1.6rem;
}
.ladder::before, .ladder::after {
  content: ""; position: absolute; left: .42rem; top: .5rem; width: 0;
  /* behind the rungs, so the climbing line never covers a number */
  z-index: 0;
}
.ladder::before { bottom: .5rem; border-left: 1px dashed var(--rule); }
/* the climb. Height is a percentage of .ladder's padding box, so the 1rem
   deduction is the top and bottom insets the dashed track also leaves. */
.ladder::after { height: 0; border-left: 2px solid var(--good); }
@keyframes climb { 0% { height: 0 } 78%, 100% { height: calc(100% - 1rem) } }
.ladder::after { animation: climb 8s linear infinite; }

.ladder li {
  position: relative; z-index: 1; margin: 0 0 .55rem; padding-left: .3rem;
  font-size: .78rem; line-height: 1.45; color: var(--ink);
  opacity: .3;
}
.ladder li::before {
  counter-increment: rung; content: counter(rung);
  position: absolute; left: -1.55rem; top: .1rem;
  width: 1rem; height: 1rem; line-height: 1rem; text-align: center;
  font-size: .6rem; color: var(--paper); background: var(--good);
}
.ladder .t { color: var(--ink-hot); font-weight: 600; }
.ladder .m {
  display: block; color: var(--ink-faint); font-size: .72rem;
}
@keyframes lightup { 0% { opacity: .3 } 6%, 97% { opacity: 1 } 100% { opacity: .3 } }
.ladder li:nth-child(1) { animation: lightup 8s linear infinite 0.1s; }
.ladder li:nth-child(2) { animation: lightup 8s linear infinite 1.3s; }
.ladder li:nth-child(3) { animation: lightup 8s linear infinite 2.5s; }
.ladder li:nth-child(4) { animation: lightup 8s linear infinite 3.7s; }
.ladder li:nth-child(5) { animation: lightup 8s linear infinite 4.9s; }
.ladder li:nth-child(6) { animation: lightup 8s linear infinite 6.1s; }

/* ‥ figure 5: three calls, one message ‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥
   The rule that costs people the most speed for the least reason. Three chips
   light in the assistant's turn, three results drop into a single bracketed
   box, and the box seals green — one message, not three. Same 8s clock. */

.fan { display: grid; gap: .45rem; }
.fan .cap {
  color: var(--ink-faint); font-size: .62rem;
  letter-spacing: .16em; text-transform: uppercase;
}
.fan .calls, .fan .bundle ul { list-style: none; margin: 0; padding: 0; }
.fan .calls { display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem; }
.fan .calls li, .fan .bundle li {
  border: 1px solid var(--rule); background: var(--paper);
  padding: .3rem .4rem; font-size: .7rem; color: var(--ink);
  text-align: center; opacity: .2;
}
.fan .calls code { font-size: .68rem; border: 0; background: none; padding: 0; }
@keyframes chip {
  0%        { opacity: .2; border-color: var(--rule) }
  6%, 74%   { opacity: 1;  border-color: var(--good) }
  86%, 100% { opacity: .2; border-color: var(--rule) }
}
.fan .calls li { animation: chip 8s linear infinite; }
.fan .calls li:nth-child(1) { animation-delay: 0.3s; }
.fan .calls li:nth-child(2) { animation-delay: 0.7s; }
.fan .calls li:nth-child(3) { animation-delay: 1.1s; }

/* the funnel: three lines converging into one, drawn with borders */
.fan .funnel {
  height: 1.5rem; position: relative;
  border-bottom: 1px solid var(--rule);
  /* 1/6 either side puts the outer legs under the outer chips' centres */
  margin: 0 16.6%;
}
.fan .funnel i {
  position: absolute; top: 0; bottom: 0; width: 0;
  border-left: 1px solid var(--rule);
}
.fan .funnel i:nth-child(1) { left: 0 }
.fan .funnel i:nth-child(2) { left: 50% }
.fan .funnel i:nth-child(3) { left: 100% }
.fan .funnel::after {
  content: "▾"; position: absolute; left: 50%; bottom: -.75rem;
  transform: translateX(-50%); color: var(--good); font-size: .8rem;
  line-height: 1; opacity: 0; text-shadow: 0 0 .4rem var(--good);
  animation: drop 8s linear infinite 1.8s;
}
@keyframes drop { 0% { opacity: 0 } 4%, 12% { opacity: 1 } 16%, 100% { opacity: 0 } }

.fan .bundle {
  border: 1px solid var(--rule); background: var(--paper-2);
  padding: .45rem .5rem; animation: seal 8s linear infinite;
}
@keyframes seal {
  0%, 44%   { border-color: var(--rule);
              box-shadow: none }
  52%, 86%  { border-color: var(--good);
              box-shadow: 0 0 0 1px var(--good) }
  92%, 100% { border-color: var(--rule); box-shadow: none }
}
.fan .bundle > b {
  display: block; margin-bottom: .35rem;
  color: var(--ink-hot); font-size: .62rem;
  letter-spacing: .16em; text-transform: uppercase;
}
.fan .bundle ul { display: grid; gap: .3rem; }
.fan .bundle li { text-align: left; }
.fan .bundle .k { color: var(--good); }
@keyframes land {
  0%        { opacity: .2; transform: translateY(-.4rem) }
  5%, 80%   { opacity: 1;  transform: none }
  90%, 100% { opacity: .2; transform: translateY(-.4rem) }
}
.fan .bundle li { animation: land 8s cubic-bezier(.2,.9,.3,1) infinite; }
.fan .bundle li:nth-child(1) { animation-delay: 2.4s; }
.fan .bundle li:nth-child(2) { animation-delay: 2.8s; }
.fan .bundle li:nth-child(3) { animation-delay: 3.2s; }

@media (max-width: 34rem) {
  .pair { grid-template-columns: 1fr; }
  /* stacked: the arrow turns to point down the page and the seam goes away */
  .pair .cross {
    width: auto; height: 1.2rem; transform: rotate(90deg);
    animation: none; color: var(--good);
  }
  .pair .cross::before { display: none; }
  .wire.h { width: 1.8rem; }
  .wire i { display: none; }
  .node { font-size: .7rem; }
  .fan .calls { grid-template-columns: 1fr; }
  .fan .funnel { display: none; }
}

/* Motion off: freeze every figure in its finished state. The pulse parks on the
   wire, all four stations stay lit, the tape is full, the JSON is complete, and
   the ladder is climbed.

   NOT wired to `prefers-reduced-motion` any more, on purpose. These figures are
   the explanation, not decoration around it — a reader with Reduce Motion set
   at the OS level was getting the frozen picture and no diagram, which is the
   wrong trade for a lesson whose whole point is a loop going round. The motion
   is slow (one 8s cycle), small, and never flashes, so it stays on for
   everyone. Put `data-motion="off"` on <html> to get the frozen state back. */
[data-motion="off"] .diag *,
[data-motion="off"] .diag *::before,
[data-motion="off"] .diag *::after,
[data-motion="off"] .lamps i { animation: none !important; }
[data-motion="off"] .lamps i:first-child { background: var(--good); }
[data-motion="off"] .node::after { opacity: 1; }
[data-motion="off"] .ring .node { background: var(--paper-3); }
[data-motion="off"] .pair .cross { color: var(--good); }
[data-motion="off"] .fan .calls li,
[data-motion="off"] .fan .bundle li { opacity: 1; }
[data-motion="off"] .fan .bundle { border-color: var(--good); }
[data-motion="off"] .fan .funnel::after { opacity: 1; }
[data-motion="off"] .ladder li { opacity: 1; }
[data-motion="off"] .ladder::after { height: calc(100% - 1rem); }
[data-motion="off"] .tape li { opacity: 1; }
[data-motion="off"] .pair .out { width: 17ch; border-right-color: var(--good); }
[data-motion="off"] .wire::after { opacity: 1 }
[data-motion="off"] .wire.h::after { left: 50% }
[data-motion="off"] .wire.v::after { top: 50% }

/* ── the index list ───────────────────────────────────────────────────────── */

.log { list-style: none; padding: 0; margin: 0; }
.log li { border-bottom: 1px dashed var(--rule); padding: 1rem 0; margin: 0; }
.log a { color: var(--ink-hot); font-weight: 600; text-decoration: none; }
.log a:hover { text-decoration: underline; text-underline-offset: 3px; }
.log p { margin: .35rem 0 0; font-size: .88rem; }

/* ── call to action ───────────────────────────────────────────────────────── */

.cta {
  display: inline-block; margin-top: .5rem;
  border: 2px solid var(--good); color: var(--paper); background: var(--good);
  padding: .35rem .9rem; text-decoration: none;
  font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
  font-size: .8rem;
}
.cta:hover { color: var(--paper); filter: brightness(1.1); }

/* ── signup ───────────────────────────────────────────────────────────────────
   The same stack the harness wears on its boot notice — label, field, button,
   note, one per row, sigil out in the margin — restated here because these
   documents do not load css/buttercup.css. Kept a copy on purpose: the two can
   drift without either page breaking.

   HOUSE RULE for every page in this directory, the archive index included:
   the form appears TWICE — `.signup.lede` at the top (inside the article
   header, right under the standfirst) and `.signup` in the `.subscribe`
   section at the foot. Two ids, `signup-lede` and `signup`, so the labels
   stay unambiguous; one `<iframe name="signup-sink">` at the foot serves both.
   signup.js binds every `form.signup` it finds, so adding a copy needs no JS
   change. New posts start from an existing page rather than from scratch. */

.subscribe { margin: 3rem 0 0; border-top: 1px dashed var(--rule); padding-top: 1.5rem; }
/* the index's last entry already closed with a dashed rule; one is enough */
.log + .subscribe { border-top: 0; padding-top: 0; }
.subscribe > h2 { margin-top: 0; }

.signup {
  display: grid; grid-template-columns: auto minmax(0, 22rem);
  gap: .35rem .6rem; justify-content: start; margin: .8rem 0;
}
.signup .sigil { grid-row: 1; align-self: baseline;
  color: var(--ink-hot); font-weight: 700; }
.signup > :not(.sigil) { grid-column: 2; }
.signup .slabel { color: var(--ink-dim); font-size: .7rem; letter-spacing: .2em; }
/* the button sizes to its word rather than the field above it */
.signup button {
  justify-self: start; margin-top: .15rem; cursor: pointer;
  font: inherit; font-size: .68rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink); background: var(--paper-3);
  border: 1px solid var(--rule); padding: .18rem .5rem;
}
.signup button:hover { background: var(--ink); color: var(--paper); }
.signup input[type="email"] {
  font: inherit; color: var(--ink-hot); background: var(--paper);
  border: 1px solid var(--rule); padding: .35rem .5rem; min-width: 0;
}
.signup input[type="email"]:focus-visible { outline: 2px solid var(--ink); outline-offset: -1px; }
.signup input[type="email"]::placeholder { color: var(--ink-faint); }
/* the honeypot: off-screen rather than `display:none`, which the crude bots
   skip and the thorough ones read as a tell. Nothing here changes the layout. */
.signup .trap {
  position: absolute; left: -9999px; top: auto; width: 1px; height: 1px;
  overflow: hidden; border: 0; padding: 0;
}
.signup .note, .signup .thanks {
  grid-column: 2 / -1; margin: 0; font-size: .74rem; color: var(--ink-faint);
}
.signup .thanks { display: none; color: var(--good); }
/* sent: the fields have done their job, so the row collapses to the receipt */
.signup[data-sent="1"] .sigil, .signup[data-sent="1"] .slabel,
.signup[data-sent="1"] input, .signup[data-sent="1"] button,
.signup[data-sent="1"] .note { display: none; }
.signup[data-sent="1"] .thanks { display: block; grid-column: 1 / -1; }

/* the lede copy of the form — the one that used to be a green CTA button, so
   its button keeps the CTA's colour and weight. Everything else is the same. */
.signup.lede { margin: 1.2rem 0 .2rem; }
.signup.lede button {
  border: 2px solid var(--good); background: var(--good); color: var(--paper);
  font-size: .8rem; letter-spacing: .1em; padding: .35rem .9rem;
}
.signup.lede button:hover {
  background: var(--good); color: var(--paper); filter: brightness(1.1);
}
/* when a green CTA already sits above the form, that link is the loud thing on
   the page — the subscribe button steps back to the quiet outlined style. */
.cta + .signup.lede { margin-top: 1.4rem; }
.cta + .signup.lede button {
  border: 1px solid var(--rule); background: var(--paper-3); color: var(--ink);
  padding: .3rem .8rem; filter: none;
}
.cta + .signup.lede button:hover {
  background: var(--ink); color: var(--paper); filter: none;
}

@media (max-width: 40rem) {
  /* narrow: the field takes whatever is left rather than holding 22rem */
  .signup { grid-template-columns: auto minmax(0, 1fr); }
}

footer.foot {
  margin-top: 3rem; border-top: 2px solid var(--rule); padding-top: 1rem;
  color: var(--ink-faint); font-size: .8rem;
}

@media (max-width: 600px) {
  body { font-size: 14px; }
  .titlebar .where { display: none; }
}
