/* ==========================================================================
   Academy — site stylesheet

   Structure:
     1.  Design tokens (light + dark)
     2.  Reset & base
     3.  Layout primitives
     4.  Header & navigation
     5.  Buttons
     6.  Hero
     7.  Section scaffolding
     8.  Cards (feature / use case / post)
     9.  Architecture diagram
     10. Code blocks & syntax highlighting
     11. Steps
     12. Prose (blog posts & content pages)
     13. Tables
     14. Publications & team
     15. Placeholder markers
     16. Footer
     17. Responsive
     18. Motion preferences
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Design tokens
   Light is the default on bare :root. Dark is redefined twice: once for the
   system preference (unless the reader explicitly chose light) and once for an
   explicit data-theme="dark", so the toggle wins in both directions.
   -------------------------------------------------------------------------- */

:root {
  color-scheme: light;

  --bg:            #ffffff;
  --bg-subtle:     #f7f8fa;
  --bg-raised:     #fbfcfd;
  --bg-inset:      #f2f4f7;

  --border:        #e3e6eb;
  --border-strong: #818da2;   /* 3.35:1 on --bg, for control edges */

  --text:          #15181d;
  --text-muted:    #59626f;
  --text-faint:    #676f7c;   /* 4.60-5.07:1 on all light surfaces */

  --accent:        #4b41c4;
  --accent-hover:  #3a319f;
  --accent-subtle: #efeefb;
  --accent-border: #cfccf3;

  --code-bg:       #f7f8fa;
  --code-text:     #24292f;

  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.04);
  --shadow-md: 0 1px 2px rgba(16, 24, 40, 0.04), 0 6px 16px rgba(16, 24, 40, 0.06);

  --radius-sm: 6px;
  --radius:    10px;
  --radius-lg: 14px;

  --container:  1120px;
  --measure:    68ch;
  --header-h:   64px;

  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
               "Liberation Mono", monospace;

  /* Syntax highlighting — light */
  --hl-comment:  #6b7280;
  --hl-keyword:  #8250df;
  --hl-string:   #0a7d33;
  --hl-number:   #b4531b;
  --hl-name:     #0550ae;
  --hl-operator: #24292f;
  --hl-error:    #b3261e;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --bg:            #0f1216;
    --bg-subtle:     #14181e;
    --bg-raised:     #171c23;
    --bg-inset:      #1b212a;

    --border:        #262d38;
    --border-strong: #5d6c81;   /* 3.51:1 on --bg, for control edges */

    --text:          #e7ecf2;
    --text-muted:    #9aa4b2;
    --text-faint:    #7e8895;   /* 4.50-5.22:1 on all dark surfaces */

    --accent:        #9d97f5;
    --accent-hover:  #b5b0ff;
    --accent-subtle: #1c1e34;
    --accent-border: #343764;

    --code-bg:       #14181e;
    --code-text:     #e7ecf2;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 1px 2px rgba(0, 0, 0, 0.3), 0 6px 16px rgba(0, 0, 0, 0.35);

    --hl-comment:  #8b949e;
    --hl-keyword:  #d2a8ff;
    --hl-string:   #7ee787;
    --hl-number:   #ffa657;
    --hl-name:     #79c0ff;
    --hl-operator: #e7ecf2;
    --hl-error:    #ff7b72;
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg:            #0f1216;
  --bg-subtle:     #14181e;
  --bg-raised:     #171c23;
  --bg-inset:      #1b212a;

  --border:        #262d38;
  --border-strong: #5d6c81;   /* 3.51:1 on --bg, for control edges */

  --text:          #e7ecf2;
  --text-muted:    #9aa4b2;
  --text-faint:    #7e8895;   /* 4.50-5.22:1 on all dark surfaces */

  --accent:        #9d97f5;
  --accent-hover:  #b5b0ff;
  --accent-subtle: #1c1e34;
  --accent-border: #343764;

  --code-bg:       #14181e;
  --code-text:     #e7ecf2;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 1px 2px rgba(0, 0, 0, 0.3), 0 6px 16px rgba(0, 0, 0, 0.35);

  --hl-comment:  #8b949e;
  --hl-keyword:  #d2a8ff;
  --hl-string:   #7ee787;
  --hl-number:   #ffa657;
  --hl-name:     #79c0ff;
  --hl-operator: #e7ecf2;
  --hl-error:    #ff7b72;
}

/* --------------------------------------------------------------------------
   2. Reset & base
   -------------------------------------------------------------------------- */

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

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 16px);
}

body {
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.65;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

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

a {
  color: var(--accent);
  text-decoration: none;
  transition: color 0.15s ease;
}

a:hover { color: var(--accent-hover); text-decoration: underline; }

h1, h2, h3, h4, h5, h6 {
  line-height: 1.25;
  font-weight: 650;
  letter-spacing: -0.018em;
  color: var(--text);
  text-wrap: balance;
}

ul, ol { padding-left: 1.25rem; }

code, pre, kbd, samp { font-family: var(--font-mono); }

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

::selection {
  background: var(--accent);
  color: #fff;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  padding: 0.75rem 1.25rem;
  background: var(--accent);
  color: #fff;
  border-radius: 0 0 var(--radius) 0;
  font-weight: 600;
}

.skip-link:focus {
  left: 0;
  text-decoration: none;
}

/* The accent is light in dark mode, so a white label on it drops to 2.57:1.
   Mirror the .button--primary treatment. */
:root[data-theme="dark"] ::selection,
:root[data-theme="dark"] .skip-link { color: #15181d; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) ::selection,
  :root:not([data-theme="light"]) .skip-link { color: #15181d; }
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   3. Layout primitives
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: 24px;
}

.container--narrow { max-width: 760px; }

main { flex: 1 0 auto; }

.section { padding-block: 5.5rem; }
.section--tight { padding-block: 4rem; }
.section--subtle { background: var(--bg-subtle); }

/* Sections are separated by a hairline rule rather than alternating background
   fills — stacked colour bands were the main thing making the page read as a
   template. Keep --subtle for at most one section. */
.section--ruled { border-top: 1px solid var(--border); }

/* --------------------------------------------------------------------------
   4. Header & navigation
   -------------------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: color-mix(in srgb, var(--bg) 85%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--border);
}

/* color-mix is well supported, but fall back to an opaque bar if it is not. */
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  .site-header { background: var(--bg); }
}

.site-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  min-height: var(--header-h);
}

.site-logo {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}

.site-logo:hover { text-decoration: none; }

.site-logo img { height: 34px; width: auto; }

/* The logo ships in two variants; show the one that suits the active theme. */
.logo--dark { display: none; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .logo--light { display: none; }
  :root:not([data-theme="light"]) .logo--dark  { display: block; }
}

:root[data-theme="dark"] .logo--light { display: none; }
:root[data-theme="dark"] .logo--dark  { display: block; }
:root[data-theme="light"] .logo--light { display: block; }
:root[data-theme="light"] .logo--dark  { display: none; }

.nav-menu {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav-menu a {
  display: block;
  padding: 0.45rem 0.7rem;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: 0.9rem;
  font-weight: 500;
  white-space: nowrap;
  transition: color 0.15s ease, background-color 0.15s ease;
}

.nav-menu a:hover {
  color: var(--text);
  background: var(--bg-inset);
  text-decoration: none;
}

.nav-menu a[aria-current="page"] {
  color: var(--accent);
  font-weight: 600;
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}

.nav-divider {
  width: 1px;
  height: 22px;
  background: var(--border);
}

.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 0.15s ease, background-color 0.15s ease;
}

.icon-button:hover {
  color: var(--text);
  background: var(--bg-inset);
  text-decoration: none;
}

.icon-button svg { width: 18px; height: 18px; }

/* Theme toggle — only the icon for the theme you'd switch *to* is shown. */
.theme-toggle .icon-moon { display: none; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-sun  { display: none; }
  :root:not([data-theme="light"]) .theme-toggle .icon-moon { display: block; }
}

:root[data-theme="dark"] .theme-toggle .icon-sun  { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: block; }
:root[data-theme="light"] .theme-toggle .icon-sun  { display: block; }
:root[data-theme="light"] .theme-toggle .icon-moon { display: none; }

.nav-toggle { display: none; }

/* --------------------------------------------------------------------------
   5. Buttons
   -------------------------------------------------------------------------- */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.65rem 1.25rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease,
              color 0.15s ease, transform 0.15s ease;
}

.button:hover { text-decoration: none; transform: translateY(-1px); }

.button--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.button--primary:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
  color: #fff;
}

/* The accent is light in dark mode, so flip the label to dark for contrast. */
:root[data-theme="dark"] .button--primary { color: #15181d; }
:root[data-theme="dark"] .button--primary:hover { color: #15181d; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .button--primary,
  :root:not([data-theme="light"]) .button--primary:hover { color: #15181d; }
}

.button--secondary {
  background: var(--bg-raised);
  border-color: var(--border-strong);
  color: var(--text);
}

.button--secondary:hover {
  background: var(--bg-inset);
  border-color: var(--text-faint);
  color: var(--text);
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.button-row--center { justify-content: center; }

/* --------------------------------------------------------------------------
   6. Hero
   -------------------------------------------------------------------------- */

.hero {
  padding-block: 6rem 5rem;
  background:
    radial-gradient(60% 120% at 50% 0%,
      var(--accent-subtle) 0%, transparent 70%);
}

/* Split hero: the pitch on the left, a real working example on the right. The
   code is the most informative thing on the page, so it sits above the fold. */
.hero--split { padding-block: 4.5rem 4rem; }

.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.12fr);
  align-items: center;
  gap: 3rem;
}

.hero__pitch { min-width: 0; }
.hero__code { min-width: 0; }

/* Fits the longest sample line at 1000px, where it previously clipped. */
.hero__code pre { font-size: 0.82rem; }

.hero__eyebrow {
  display: inline-block;
  margin-bottom: 1.25rem;
  padding: 0.3rem 0.75rem;
  border: 1px solid var(--accent-border);
  border-radius: 100px;
  background: var(--accent-subtle);
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.hero h1 {
  font-size: clamp(2.25rem, 4.6vw, 3.25rem);
  font-weight: 700;
  letter-spacing: -0.035em;
  margin-bottom: 1.25rem;
}

.hero__lead {
  font-size: clamp(1.05rem, 2.2vw, 1.25rem);
  line-height: 1.6;
  color: var(--text-muted);
  margin-bottom: 2rem;
}

.hero__install {
  margin-top: 1.75rem;
  max-width: 420px;
}

/* Single-line code with the copy button on the same row. */
.code-block--inline {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding-right: 0.6rem;
}

.code-block--inline pre {
  flex: 1;
  min-width: 0;
  padding: 0.65rem 0.9rem;
  border: 0;
  background: none;
}

.code-block--inline .copy-button { flex-shrink: 0; }

/* --------------------------------------------------------------------------
   7. Section scaffolding
   -------------------------------------------------------------------------- */

.section-head {
  max-width: var(--measure);
  margin-bottom: 3rem;
}

/* Heading on the left, a single action on the right. */
.section-head--row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  max-width: none;
}

.section-eyebrow {
  display: block;
  margin-bottom: 0.75rem;
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.section-title {
  font-size: clamp(1.75rem, 3.5vw, 2.35rem);
  font-weight: 680;
  letter-spacing: -0.025em;
}

.section-lead {
  margin-top: 0.85rem;
  font-size: 1.075rem;
  line-height: 1.65;
  color: var(--text-muted);
}

/* --------------------------------------------------------------------------
   8. Cards
   -------------------------------------------------------------------------- */

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1.25rem;
}

.card {
  display: flex;
  flex-direction: column;
  padding: 1.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-raised);
  box-shadow: var(--shadow-sm);
  transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}

.card:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

.card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin-bottom: 1.1rem;
  border: 1px solid var(--accent-border);
  border-radius: var(--radius);
  background: var(--accent-subtle);
  color: var(--accent);
  font-size: 1.15rem;
  line-height: 1;
}

.card__icon svg { width: 20px; height: 20px; }

.card h3 {
  font-size: 1.075rem;
  font-weight: 640;
  margin-bottom: 0.5rem;
}

.card h3 a { color: inherit; }
.card h3 a:hover { color: var(--accent); text-decoration: none; }

.card p {
  color: var(--text-muted);
  font-size: 0.95rem;
  line-height: 1.6;
  /* Rows are equal-height; without a clamp one long summary stretches every
     card in the row and short ones end in a quarter-screen of nothing. */
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
  color: var(--text-faint);
  font-size: 0.8rem;
}

.card__date {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.01em;
}

.tag {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 100px;
  background: var(--bg-inset);
  color: var(--text-muted);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.card__more {
  margin-top: auto;
  padding-top: 1rem;
  font-size: 0.9rem;
  font-weight: 600;
}

/* Whole-card link target, so the card body is clickable without nesting <a>. */
.card--link { position: relative; }

.card--link .stretched::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

/* --------------------------------------------------------------------------
   9. Architecture figure
   Replaces a hand-built CSS box diagram. The paper's own figure carries the
   data-plane / control-plane split, which is the whole point of the section
   and was previously only asserted in prose.
   -------------------------------------------------------------------------- */

/* Capped and centred: at full container width the figure rendered ~1020px
   wide, which dominated the section it is meant to support. */
.arch-figure {
  max-width: 820px;
  /* Top margin matches .vision's, for the same reason: the heading that used
     to sit above this figure was removed, and it had been carrying all of the
     separation from the section lead. */
  margin: 2.25rem auto 3rem;
}

/* Drawn on white; give it a light card in both themes rather than inverting,
   since the colours carry meaning. */
.arch-figure__media {
  padding: 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: #ffffff;
}

.arch-figure__media img { width: 100%; height: auto; }

.arch-figure figcaption {
  max-width: var(--measure);
  margin: 0.9rem auto 0;
  color: var(--text-muted);
  font-size: 0.875rem;
  line-height: 1.55;
  text-align: center;
}

.arch-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 3.5rem;
  align-items: start;
}

.arch-split__map dt strong { color: var(--text); }

/* The .arch-split__text rules that used to sit here are gone with the column
   they styled — see the note in the markup for what was cut and why.

   With one child left, the comparison list would otherwise stretch across the
   whole container. Two things follow. It takes the figure's 820px so the two
   read as one block, and each row splits term from description: a full-width
   single column would set the description at roughly 100 characters, where
   splitting holds it near 70. */
.arch-split--single {
  grid-template-columns: minmax(0, 1fr);
  max-width: 820px;
  margin-inline: auto;
}

.arch-split--single .dl-list__row {
  grid-template-columns: 210px minmax(0, 1fr);
  gap: 1.25rem;
}

.arch-split__more { margin-top: 1.25rem; }

/* --------------------------------------------------------------------------
   9b. Vision figure — "where agents live"

   Hand-authored inline SVG animated with CSS. No canvas, no library: the site
   is bundled into a single file under a CSP that blocks every external host,
   so a CDN runtime would simply not load.

   The figure's one idea is a unit that repeats: a DASHED boundary around a
   resource Academy does not own, containing a SOLID box that is the Academy
   agent — identical in all four. So the two rules that carry the whole thing
   are .wgd-frame and .wgd-agent, and they are deliberately as far apart as the
   palette allows: neutral, dashed, corner-bracketed vs. accent, solid, filled,
   with an inset kind band.

   There is no legend, no step list, no numbered markers and no caption beside
   the drawing. What they used to carry is now either drawn — the loop order is
   read off the behaviour, and the mailbox flags say when a message is waiting
   to be collected — or written into the text alternative and the prose above
   the figure (the sourcing). Nothing here should grow a narrative column back.

   TWO ANIMATIONS, SIMULTANEOUS AND CONTINUOUS. The bubble focus cycle (13s)
   and the message loop (24s) run at the same time and neither stops:
   sequencing them, or running one pass and resting, read as the figure getting
   stuck. The one thing that stops the motion outright is
   prefers-reduced-motion; the other is scrolling the figure off screen. There
   is no pause control — it was removed at the client's request, which is a
   live WCAG 2.2.2 question and is written up in the handover rather than
   papered over here.

   THE MESSAGE LOOP IS A QUEUE, NOT A RELAY. Messages accumulate in a mailbox
   and are collected as a batch. Three arrive at the cloud's mailbox and wait
   there while the cloud agent — the only agent in the figure that reasons — is
   busy reasoning; it takes all three at once when it finishes, and only then
   acts. That is the paper's claim for persistent mailboxes ("providing
   resilience when individual agents may sporadically be online") drawn rather
   than asserted, and the think halo's window is what gives the queue a cause.
   The two must stay aligned: separate them and the figure is back to showing
   arbitrary traffic. The full timetable and the per-mailbox depth step
   functions are in the motion block at the foot of this section.

   REPEATING ONE EVENT AT SEVERAL POINTS OF THE CYCLE. There are three ways to
   do this and only two of them work, so be exact about which is which:

     1. Several WINDOWS inside ONE keyframe timeline, on one element, at their
        own percentages of the loop. One animation, several events. Safe under
        `infinite`, and it is what everything on the cloud mailbox uses — its
        flag, its flash and each of its pips fires three times off one keyframe.
     2. Several ELEMENTS, one per occurrence, each on the same one-window
        keyframe at its own delay. Only safe where the element's resting state
        is invisible. Nothing in the message layer relies on it any more: a
        lowered flag and an unlit pip are both VISIBLE resting states, so a
        spare copy is a drawn artefact sitting under the live one, not a
        dormant duplicate. Where several mailboxes share one behaviour the
        figure now uses one element each on a shared keyframe with its own
        delay — one element per occurrence per mailbox, never stacked.
     3. Several ANIMATIONS on one element — `animation-name: x, x` with two
        delays. This is the trap. Two infinite animations are BOTH always in
        their active phase, so the later-declared one wins forever and the
        earlier never shows. It worked only while the loop stopped after one
        pass, and it has bitten this figure once already. Every pip and every
        flag below carries exactly ONE animation-name.

   The flags were once built with (2) and that was the wrong choice, for the
   reason above: the cloud mailbox received three hops, carried three flag
   elements, and raising one drew the other two, lowered, underneath it. That
   was the ghost. The pips would have reproduced it exactly — an unlit socket
   is as visible as a lowered flag — which is why each pip is one element with
   every window it needs written into its own keyframe.

   Every colour below resolves through a global token, and the tokens are
   already defined three times over (bare :root, the dark media query, and
   [data-theme="dark"]). Nothing here may declare a colour of its own inside a
   media or [data-theme] block — that is exactly how you end up painting one
   theme's text onto the other theme's background in the un-stamped default
   state.

   Unlike .arch-figure this is NOT on a white card. That figure is a raster
   drawn on white and has to keep its own background; this one is vector and
   themes itself, so it sits directly on the page in both themes.
   -------------------------------------------------------------------------- */

.vision {
  /* The top margin used to come from the heading and sourcing clause that sat
     above the figure. Both were removed — the drawing is self-describing and
     the problem statement frames it — which left the figure butting directly
     against the last line of prose with a measured gap of zero. */
  margin: 2.25rem auto 3.5rem;

  /* Local names for the global tokens, so the SVG reads as a palette rather
     than as a list of var() calls. Every pairing was computed against the
     surface it actually lands on, not eyeballed. Worst cases, light / dark:

       rail, junction beads, mailbox body edge and lowered
         flag, all --border-strong on bg-inset                   3.04 / 3.03 [3]
       border-strong on bg-subtle (boundary inner AND outer
         halves, tethers, wires, bubble rings)                  3.15 / 3.33 [3]
       bubble icon at its 0.62 dim, composited on bg-subtle     4.88 / 6.38 [3]
       text-faint on bg-raised   (ACADEMY AGENT part label)     4.94 / 4.76 [4.5]
       text-faint on bg-subtle   (EXCHANGE part label, which
         sits outside the box and so lands on the page)         4.77 / 4.96 [4.5]
       text-muted on accent-sub  (LLM-driven)                   5.38 / 6.48 [4.5]
       text-muted on bg-inset    (kind text)                    5.60 / 6.42 [4.5]
       text-muted on bg-subtle   (entity reason line)           5.81 / 7.06 [4.5]
       accent on accent-subtle   (LLM band edge and mark)       6.41 / 6.37 [3]
       accent on bg-inset        (letter and raised flag over
         the bus, mailbox flash, LIT PIP)                       6.68 / 6.30 [3]
       border-strong at 0.6 on bg-inset (pip SOCKET)            1.85 / 1.92 [—]
       accent on bg-subtle       (letter over an entity, letter
         between entities, halo)                                6.93 / 6.94 [3]
       accent on bg-raised       (agent box edge, mark, port,
         letter over an agent box)                              7.17 / 6.67 [3]
       text on bg-subtle         (bubble icon)                 16.74 /14.99 [4.5]

     The figure lives inside #why, which is a section--subtle, so the page
     ground behind it is --bg-subtle and there is no longer any pairing in this
     figure that lands on plain --bg. Moving it back to an unshaded section
     would raise every one of these, not lower them, but the numbers would then
     be wrong in the safe direction rather than right.

     The socket is the one entry with no threshold beside it, and that is
     deliberate rather than an omission. A socket carries no information on its
     own: it is the outline of a slot that is not filled, and what a reader has
     to see is the LIT pip, which clears 3:1 twice over in both themes. Depth is
     carried by how many pips are lit and by which slots they sit in — count and
     position — so nothing is conveyed by the socket's colour alone. Making the
     sockets clear 3:1 would put four permanent high-contrast marks under every
     mailbox and drown the pips they exist to frame.

     Every figure above was recomputed from the tokens in section 1 rather than
     carried forward. One did not reproduce: the dimmed bubble icon was written
     as 4.28 / 5.75 and is 4.88 / 6.38 — --text at 0.62 composited on
     --bg-subtle is #6b6d71 in light and #979ba1 in dark. It passed either way,
     which is exactly why it survived: a number in a comment fails silently.
     Everything else in the table reproduced to the digit.

     The letter's own fill is --bg-raised, which is a 1.03-1.10:1 step from
     every surface it crosses: it is there to stop wires and the rail showing
     through the envelope, not to carry contrast. The accent outline is what
     has to be seen, and it is the figure's best-contrasting stroke.

     One pairing failed and was changed rather than shipped: --border-strong on
     --accent-subtle comes out at 2.92:1 in light, so anything sitting on the
     LLM band uses --accent (6.41 / 6.37) instead.

     The dashed boundary is the one stroke where the computed number is not the
     whole story: contrast maths has no notion of a dash pattern, so a dashed
     stroke scores identically to a solid one while reading lighter. It is
     therefore drawn at 1.75 rather than 1.5, with a high duty cycle (7 on,
     5 off) and solid corner brackets, so its apparent weight matches the
     3.35:1 it computes. */
  --wgd-surface:  var(--bg-subtle);
  --wgd-agent-bg: var(--bg-raised);
  --wgd-exchange: var(--bg-inset);
  --wgd-edge:     var(--border-strong);
  --wgd-title:    var(--text);
  --wgd-body:     var(--text-muted);
  --wgd-faint:    var(--text-faint);
  --wgd-accent:   var(--accent);
  --wgd-llm:      var(--accent-subtle);
}

/* .vision__source is gone. It held the sourcing clause above the figure, and
   the clause was removed along with the figure's heading — the drawing is
   self-describing and the problem statement above it does the framing. The
   sourcing now lives only in #vision-desc; see the note in the markup. */

/* Full width, no narrative column beside it — the drawing carries itself, so
   it gets the whole container.

   The scroll container is for phones only. SVG text scales with the drawing,
   so at 460px of container the 820-unit viewBox would put the kind labels near
   6px. Below 700px the drawing therefore stops shrinking and the figure
   scrolls sideways instead, which keeps every label at the size the previous
   version rendered at its narrowest. WCAG 1.4.10 exempts diagrams from reflow
   for exactly this reason; 2.1.1 does not, hence the tabindex in the markup
   and the focus ring below. */
.vision__media { overflow-x: auto; overscroll-behavior-x: contain; }

/* Horizontally scrollable regions must be keyboard reachable. */
.vision__media:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

.vision__svg {
  display: block;
  width: 100%;
  height: auto;
}

.vision__svg text {
  font-family: var(--font-sans);
  /* Lengths inside an SVG are user units, so these track the viewBox. */
}

/* ---- Type ---------------------------------------------------------------- */

.wgd-title      { font-size: 15px;   font-weight: 640; letter-spacing: 1.3px; fill: var(--wgd-title); }
.wgd-reason     { font-size: 12px;   fill: var(--wgd-body); }
.wgd-kind       { font-size: 10.5px; letter-spacing: 0.2px; fill: var(--wgd-body); }

/* The part label: small, tracked, faint, and sitting outside the top of the
   thing it names. ACADEMY AGENT is the idiom and EXCHANGE now shares its
   declaration rather than approximating it, so the two cannot drift apart.

   EXCHANGE was 12.5px/640 in --text — the entity titles' own colour and
   weight — on a baseline above every one of theirs. The exchange is plumbing
   and it was typeset as the most important thing in the drawing. A part label
   is what it is: it names a component, it does not announce a place. */
.wgd-agent-name,
.wgd-ex         { font-size: 10.5px; font-weight: 620; letter-spacing: 1px; fill: var(--wgd-faint); }

/* ---- Theirs: the dashed boundary ---------------------------------------- */

.wgd-frame {
  fill: var(--wgd-surface);
  stroke: var(--wgd-edge);
  stroke-width: 1.75;
  stroke-dasharray: 7 5;
  stroke-linecap: round;
}

/* Solid legs at the four corners. A purely dashed rectangle reads as an
   unfinished box; bracketed corners read as a surveyed boundary, which is what
   "someone else's facility" is. */
.wgd-corner {
  fill: none;
  stroke: var(--wgd-edge);
  stroke-width: 1.75;
  stroke-linecap: round;
}

/* ---- Ours: the Academy agent -------------------------------------------- */

.wgd-agent {
  fill: var(--wgd-agent-bg);
  stroke: var(--wgd-accent);
  stroke-width: 1.75;
}

.wgd-agent-mark { fill: none; stroke: var(--wgd-accent); stroke-width: 1.3; }
.wgd-agent-core { fill: var(--wgd-accent); }

/* The kind band is the only thing that differs between the four agents, and
   the difference is a fill, not a shape — see WCAG 1.4.1 for why the words are
   in there too. */
.wgd-kindband {
  fill: var(--wgd-exchange);
  stroke: var(--wgd-edge);
  stroke-width: 1;
}

.wgd-kindmark { fill: var(--wgd-edge); }

.wgd-kindband--llm { fill: var(--wgd-llm); stroke: var(--wgd-accent); }
.wgd-kindmark--llm { fill: var(--wgd-accent); }

/* ---- The exchange -------------------------------------------------------- */

/* A closed rounded rectangle with the inset fill, sized to its contents:
   y 149-417 around two mailbox rows that span 169-397, so a 20-unit margin at
   each end and a centre of 283, which is where the rows centre. It was
   68-458, taller than any entity and centred 20 units above its own contents.

   Everything else in this figure is a closed rounded form. This one stays one
   — an open-ended version of it was tried and rejected.

   THE RAIL OUTWEIGHS WHAT FEEDS IT. Wires, dashed boundaries and agent box
   edges are all 1.75 and the rail used to be 1.75 as well, which left the one
   part of the exchange that carries meaning weighing exactly what the eight
   wires hanging off it weigh. At 3.5 it is twice the wire and reads as a
   shared bus. Contrast is unaffected — same colour on the same fill, 3.04
   light / 3.03 dark — and a heavier stroke can only help it.

   The junction bead is the same colour again, deliberately: see the markup
   for why it is not --accent. */
.wgd-bus      { fill: var(--wgd-exchange); stroke: var(--wgd-edge); stroke-width: 1.5; }
.wgd-rail     { fill: none; stroke: var(--wgd-edge); stroke-width: 3.5; stroke-linecap: round; }
.wgd-junction { fill: var(--wgd-edge); }

/* A curbside mailbox, in two strokes: the arched body and the flag. That is
   the whole glyph. It had a door seam and a knob as well, and at the size this
   figure renders — the 34-unit body is about 30 CSS px at a 900px container —
   they were sub-pixel marks that read as dirt on the body rather than as a
   door. The arch plus the flag is what says "mailbox"; the rest was detail
   that did not survive the reduction.

   The body path is unchanged, so the 34x22 footprint is exactly what the
   abstract shape had and no wire endpoint or proportion of the spine moves.
   The flag is still the only part that leaves the footprint, upwards, into
   space that was already empty. */
.wgd-mbox { fill: var(--wgd-surface); stroke: var(--wgd-edge); stroke-width: 1.25; }

/* The flag, and what it means: raised while a letter is waiting in this
   mailbox, lowered once the agent has collected it. Academy's mailboxes are
   persistent — a message sent to an agent that is not there waits until it
   comes back — so this is a property of the system being drawn, not a mailbox
   ornament. #vision-desc states it in words as well.

   Down is the base style, which is what reduced motion and scripting-off
   render, and what a mailbox with nothing waiting in it shows. Down lays the
   flag along the box's roof rather than down its side, where a real one would
   go: the sides are where the wires arrive and where the letters pass, and a
   flag there would be crossed by both. Up is the mailbox flag everyone draws —
   a short pole on the shoulder with the blade at the top.

   The pole and the blade are authored so the flag's own bounding box has its
   bottom-left corner exactly at the pivot, which is what lets
   transform-box: fill-box put the rotation origin there. The pivot itself sits
   on the arch, so the flag is visibly bolted to the box in both states.

   The right-hand pair of mailboxes is mirrored as a whole, so this one
   rotation reads correctly on all four without a second rule.

   DOWN IS A VISIBLE STATE, which is why there is exactly one flag element per
   mailbox however many hops arrive at it. A second flag resting at -90deg is
   not a spare copy waiting its turn; it is a lowered flag drawn on the roof,
   and it showed under every raised one. See the numbered note at the top of
   this section, and wgd-flag-cloud below.

   Colour rides on `color` so one animated property drives the pole's stroke
   and the blade's fill together, and so the raised state can reinforce
   position with the message accent rather than leaning on position alone. */
.wgd-flag {
  color: var(--wgd-edge);
  transform-box: fill-box;
  transform-origin: 0% 100%;
  transform: rotate(-90deg);
}

.wgd-flag-arm   { fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; }
.wgd-flag-blade { fill: currentColor; }

/* ---- The pip tray -------------------------------------------------------
   How many messages are waiting in this mailbox, out of the four it can hold.
   The client's reference is Mini Motorways, where a building carries small
   marks for the demand it has pending; the same read applies here, and it is
   the one thing in this figure that a still frame states outright — a moving
   letter can only ever say "a message exists", never "three are waiting".

   Four sockets, always drawn. They are the capacity, which is what turns
   "some are waiting" into "three of four". Four pips light in them, left-packed
   in the mailbox's own frame: pip N is lit whenever depth >= N. A pip is never
   bound to a particular letter — at this size nobody tracks an individual pip,
   and depth-based windows keep one clean keyframe per pip instead of one per
   (letter, pip) pair.

   HOLLOW VERSUS FILLED, NOT JUST GREY VERSUS ACCENT. The socket is a ring and
   the lit pip is a disc that covers it exactly (socket r 2.4 + 0.9 stroke =
   2.85 outer; pip r 2.9), so the difference between waiting and empty is a
   difference in form as well as in colour. With the count and the position
   carrying the depth on top of that, nothing here depends on colour alone.

   Lit pips are --wgd-accent, which in this figure already means "a live
   message". A message parked in a mailbox is a live message, so this is the
   same meaning and not a new one — see the note on colour rules above.

   The .wgd-tray group around them carries no styles and is not meant to: it is
   a DOM grouping so that the eight-or-so circles under a mailbox read as one
   part in the markup, and so that a future change has something to hang off. */
.wgd-socket {
  fill: none;
  stroke: var(--wgd-edge);
  stroke-width: 0.9;
  opacity: 0.6;
}

.wgd-pip {
  fill: var(--wgd-accent);
  opacity: 0;
}

/* The receiving mailbox, while a letter is on its way to it. One element per
   mailbox, resting at opacity 0, carrying every one of its own windows.

   RECEIVER ONLY, and this is a fix rather than a preference. It used to be
   stacked one element per (mailbox, hop) INCIDENCE, sender's and receiver's
   alike, so a mailbox lit up on hops where its agent was posting — the two
   left-hand mailboxes never did anything else, and the HPC mailbox lit on its
   own return leg. A flash on the sender says nothing about the sender's
   mailbox: what is happening there is a letter passing overhead.

   It ends at arrival rather than running the length of the hop, so the flash
   reads as the approach and the flag and the pip read as the arrival. No two
   windows on any one mailbox overlap, so the accent stroke is never painted
   twice over itself. */
.wgd-mflash {
  fill: none;
  stroke: var(--wgd-accent);
  stroke-width: 2.2;
  opacity: 0;
}

/* ---- Wires --------------------------------------------------------------- */

.wgd-wires path {
  fill: none;
  stroke: var(--wgd-edge);
  stroke-width: 1.75;
  stroke-linecap: round;
}

/* Painted in the container fill and drawn over the dashed boundary, so each
   wire cuts a clean notch through it instead of colliding with a dash. Where a
   casing overhangs onto the page it paints --bg-subtle on --bg, a 1.6% step
   that is not visible at a 16px run. */
.wgd-casings path {
  fill: none;
  stroke: var(--wgd-surface);
  stroke-width: 7;
}

.wgd-port { fill: var(--wgd-accent); }

/* A message in flight, as a letter.

   The marker used to be a near-zero dash with a round cap on a stroked path,
   which is why it could only ever be a dot: a dash has no shape of its own. An
   envelope has to be a real element moved along the route, so it is one, and
   CSS motion path is what moves it. That keeps the whole thing declarative and
   inside the existing gating — .is-running still starts it and the reduced
   motion block still stops it — where <animateMotion> would have needed its
   own begin/end plumbing and would have ignored both.

   offset-rotate: 0deg is not a default. The initial value is `auto`, which
   turns the envelope to face along the path, and three of the four routes
   finish travelling right to left: with `auto` those letters arrive upside
   down. Pinning the rotation to 0deg also leaves the transform free for the
   flutter and for the shrink into the mailbox.

   transform-box: fill-box makes the envelope's own centre the point that rides
   the path and the origin that it scales about. The flutter rotates about that
   same centre, so the group's bounding box stays symmetrical about it and the
   two never drift apart.

   SIX ROUTES, AND THE TWO NEW ONES ARE REVERSALS. h1-h4 are the four the dots
   used to run on, unchanged. h5 and h6 are h2 and h1 read backwards — the same
   points in the opposite order, with each quadratic's control point kept and
   its endpoints swapped. Nothing new was drawn: they land on the same wires,
   turn on the same two junction beads and stop at the same mailbox centres, so
   no wire endpoint and no mailbox moves. They exist because the flash is now
   receiver-only, and without a hop that ends at them the instrument and the
   data store mailboxes would never light at all.

   --wgd-stop is how far along each route its RECEIVING mailbox sits, measured
   on the real geometry rather than normalised. Recomputed for v8 rather than
   carried over, and the reversals come out identical to the forward routes,
   which is the check that they really are reversals:

     quarter-turn quadratic (450,180) (460,180) (460,190)   16.2323 units
     cornered route  139 + 16.2323 + 186 + 16.2323 + 139 =  496.465
       receiving mailbox centre at 139+16.2323+186+16.2323+37 = 394.465
       394.465 / 496.465                                    = 79.4547%
     straight route  609 - 311                              = 298
       receiving mailbox centre 196 units along, 196/298    = 65.7718%

   The mailbox centres those resolve to are (507,386) for the cloud, (507,180)
   for HPC, (413,386) for the data store and (413,180) for the instrument —
   each the middle of a 34x22 body, which is where a letter has to vanish for
   the pause to read as "posted" rather than as "dropped nearby". */
.wgd-letter {
  offset-rotate: 0deg;
  offset-distance: 0%;
  transform-box: fill-box;
  transform-origin: center;
  opacity: 0;
}

/* h1  instrument -> cloud   */
.wgd-letter--h1 {
  offset-path: path("M311,180 H450 Q460,180 460,190 V376 Q460,386 470,386 H609");
  --wgd-stop: 79.45%;
}

/* h2  data store -> cloud   */
.wgd-letter--h2 {
  offset-path: path("M311,386 H609");
  --wgd-stop: 65.77%;
}

/* h3  HPC -> cloud          */
.wgd-letter--h3 {
  offset-path: path("M609,180 H470 Q460,180 460,190 V376 Q460,386 470,386 H609");
  --wgd-stop: 79.45%;
}

/* h4  cloud -> HPC          */
.wgd-letter--h4 {
  offset-path: path("M609,386 H470 Q460,386 460,376 V190 Q460,180 470,180 H609");
  --wgd-stop: 79.45%;
}

/* h5  cloud -> data store   — h2 reversed */
.wgd-letter--h5 {
  offset-path: path("M609,386 H311");
  --wgd-stop: 65.77%;
}

/* h6  cloud -> instrument   — h1 reversed */
.wgd-letter--h6 {
  offset-path: path("M609,386 H470 Q460,386 460,376 V190 Q460,180 450,180 H311");
  --wgd-stop: 79.45%;
}

/* Opaque, so wires and the rail do not show through the paper. The fill is
   nearly the value of everything it crosses; the accent outline is the graphic. */
.wgd-letter-body { fill: var(--wgd-agent-bg); stroke: var(--wgd-accent); stroke-width: 1.5; }

.wgd-letter-flap {
  fill: none;
  stroke: var(--wgd-accent);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.wgd-flutter { transform-box: fill-box; transform-origin: center; }

.wgd-think { fill: none; stroke: var(--wgd-accent); stroke-width: 1.75; opacity: 0; }

/* ---- Instrument tethers -------------------------------------------------
   Each bubble is joined to the instrument's own boundary by a line that draws
   itself out from the boundary, holds, and then retracts into the bubble.
   Forming and fading is the content: it says the slot takes any number of
   these, not that it takes five.

   The retract is done with the dash pattern rather than with opacity, so the
   stroke is either at full 3.35:1 contrast or absent, never sitting at some
   in-between weight. Base state is fully drawn — that is what reduced motion
   and scripting-off render. */
.wgd-tether {
  fill: none;
  stroke: var(--wgd-edge);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-dasharray: 100 100;
  stroke-dashoffset: 0;
}

/* ---- Instrument bubbles -------------------------------------------------
   Base state is the informative one and it is what reduced motion renders:
   five equal circles, every icon at full opacity, nothing scaled, every tether
   drawn. The orbit and the focus drift only ever move away from this and back.

   The ring is opaque, and each tether ends at its bubble's HOME point rather
   than at its live position. The orbit radius is smaller than the ring, so the
   join stays hidden under the ring wherever the bubble has drifted to — which
   is what lets a moving bubble keep a fixed-geometry tether in pure CSS. */

.wgd-bub-ring {
  fill: var(--wgd-surface);
  stroke: var(--wgd-edge);
  stroke-width: 1.9;
}

.wgd-ico {
  fill: none;
  stroke: var(--wgd-title);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Only ever translated, so it needs no transform-origin: a translation is the
   one transform whose result does not depend on where the origin is. That
   matters in SVG, where transform-box defaults make a rotate or a scale about
   a remote centre unreliable. */
.wgd-bub-orbit { will-change: transform; }

/* fill-box puts the scale origin at the mark's own centre. The hover label is
   deliberately a sibling rather than a child, so it cannot pull that bounding
   box sideways. */
.wgd-bub-mark {
  transform-box: fill-box;
  transform-origin: center;
}

.wgd-bub-lbl {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.14s ease;
}

.wgd-bub-lbl rect {
  fill: var(--wgd-agent-bg);
  stroke: var(--wgd-edge);
  stroke-width: 1;
}

.wgd-bub-lbl text { font-size: 12px; fill: var(--wgd-title); }

.wgd-bub:hover .wgd-bub-lbl { opacity: 1; }

/* ---- Motion ------------------------------------------------------------
   Nothing below runs until JS adds .is-running, which tracks visibility. With
   scripting blocked the figure is complete and static: five instrument bubbles
   at full opacity with all five tethers drawn, the whole wire network, four
   mailboxes with their flags down, and no letters in flight. */

/* ==========================================================================
   THE MESSAGE LOOP — ONE 24s CYCLE. THIS IS THE TIMETABLE.
   ==========================================================================

   It was 15s and ran four hops one in, one out. v8 makes it a queue: three
   messages arrive at the cloud's mailbox and WAIT there while the cloud agent
   reasons, are collected as a BATCH, and only then does the cloud act. The
   period moved to 24s to fit six hops, a 9.4s reasoning window and a rest.

     t (s)    %        event
     ------------------------------------------------------------------
      0.50     2.083   h1  instrument -> cloud    departs
      1.60     6.667   h1  posted. cloud depth 0->1, flag up, pip 1 lit
      1.60     6.667   the cloud agent starts reasoning
      3.00    12.500   h2  data store -> cloud    departs
      4.10    17.083   h2  posted. depth 1->2, pip 2
      5.50    22.917   h3  HPC -> cloud           departs
      6.60    27.500   h3  posted. depth 2->3, pip 3
      6.60-11.00       queue holds at 3. The agent is busy; the mail waits.
     10.80    45.000   reasoning ends
     11.00    45.833   h1 collected. depth 3->2
     11.90    49.583   h2 collected. depth 2->1
     12.80    53.333   h3 collected. depth 1->0, flag down, tray empty
     13.60    56.667   h4  cloud -> HPC           departs
     14.70    61.250   h4  posted.  HPC depth 0->1
     15.10    62.917   h5  cloud -> data store    departs
     16.20    67.500   h5  posted.  store depth 0->1
     16.60    69.167   h6  cloud -> instrument    departs
     17.50    72.917   h4 collected. HPC 1->0
     17.70    73.750   h6  posted.  instrument depth 0->1
     19.00    79.167   h5 collected. store 1->0
     20.50    85.417   h6 collected. instrument 1->0
     20.87    86.958   the last letter leaves the drawing
     20.87-24         rest. Nothing in flight, every tray empty.
     24.00            round again, without pausing

   DEPTH STEP FUNCTION PER MAILBOX. This is the one thing a reader cannot
   recover from the keyframes, because each pip keyframe only carries the
   union of the intervals where depth >= N, not the depth itself:

     cloud       0 on [0, 1.6)    1 on [1.6, 4.1)    2 on [4.1, 6.6)
                 3 on [6.6, 11.0) 2 on [11.0, 11.9)  1 on [11.9, 12.8)
                 0 on [12.8, 24)
     HPC         0 elsewhere,  1 on [14.7, 17.5)
     data store  0 elsewhere,  1 on [16.2, 19.0)
     instrument  0 elsewhere,  1 on [17.7, 20.5)

   and everything else is derived from it, not hand-authored:

     pip N       lit exactly where depth >= N. Cloud: pip 1 [1.6, 12.8),
                 pip 2 [4.1, 11.9), pip 3 [6.6, 11.0), pip 4 never — so the
                 cloud has three pip elements and four sockets. The other
                 three mailboxes reach 1, so one pip element each.
     flag        up exactly where depth >= 1. That is pip 1's window with
                 rotation instead of opacity, and it is why all four mailboxes
                 animate their flag now: on the old four-hop schedule two of
                 them never received anything.
     flash       runs from departure to arrival, on the RECEIVER only.

   TWO CLOCKS, AND KNOW WHICH ONE YOU ARE READING.

     Absolute. The cloud mailbox and the three letters queued in it carry no
     delay, so every percentage in their keyframes is a position in the 24s
     loop and can be read straight off the table above. Their behaviour is
     unique, so sharing would buy nothing.

     Delayed. The three peripheral mailboxes do the same thing three times,
     1.5s apart, so they share four keyframes — wgd-letter-out, wgd-mflash-rx,
     wgd-flag-rx, wgd-pip-rx — written relative to their own departure and
     placed by .wgd-to-hpc / .wgd-to-store / .wgd-to-instr at the foot of this
     section. Every element of one delivery — the letter, and the receiving
     mailbox's flash, flag and pip — carries the SAME delay class, so retiming
     a delivery is one number. Add the delay to a relative percentage to get an
     absolute one; 13.6s + 4.583% of 24s = 14.7s is h4's arrival.

   ONE OVERLAP, AND IT IS DELIBERATE. The three dispatches are 1.5s apart and
   each occupies 4.27s, so h4's collection (17.50-17.87) overlaps the tail of
   h6's outbound flight (16.60-17.70) by 0.2s. Two letters are on screen at
   once for that fifth of a second, at opposite ends of the drawing — h4 is
   running into the HPC box at top right, h6 is arriving at the instrument
   mailbox at top left. The old "one letter in the air at a time" rule came
   from a strictly linear five-step story; v8's story is a batch fanning out,
   and the order still reads. Closing the gap would need the parks cut to
   under 1.7s, which is too short for a flag to rise, hold and fall.

   PHASE-LOCKING. Nothing else in the figure may share a period with this loop.
   The bubble focus cycle is 13s and the five orbits are 17, 22, 14, 19 and 16s;
   none of them divides 24 and none equals it, which is the failure this figure
   already had once (an 11s orbit against an 11s loop, phase-locked for good).
   The weakest of them is the 16s orbit, gcd(16,24)=8, so that one bubble and
   the letters realign every 48s — two full message loops, on a 7-unit drift
   that nobody tracks. Stated rather than glossed: the previous comment here
   claimed every orbit was coprime with the loop, and against 24 that is no
   longer true. The flutter is 1.4s and 24/1.4 = 17.142..., still not a whole
   number of flutters per cycle. */

/* One letter's hop, in three parts: out of the sending agent to the receiving
   mailbox, a wait inside that mailbox with its flag up and a pip lit, and then
   the short run from the mailbox into the receiving agent.

   The wait is why this is a keyframe and not a straight 0 -> 100% sweep. A
   letter that arrives and vanishes says "delivered"; a letter that is posted,
   sits, and is then collected says what Academy's exchange actually does. v8
   does not invent that pause — it was already here — it LENGTHENS it, from a
   fixed 0.75s to as much as 9.4s for the first letter into the cloud, and puts
   a tray under the mailbox so the wait has something to count.

   The letter shrinks and fades as it is posted rather than parking on top of
   the box, so what is visible during the wait is the raised flag and the pips
   — the mailbox is holding the message, not wearing it.

   Every leg is the same shape, and only the length of the park differs:

     +0.00s  leaves the agent box, scaled to 0.7 and transparent
     +0.19s  full size and opaque
     +0.91s  still travelling
     +1.10s  posted: at --wgd-stop, shrunk to 0.62 and gone
      park   invisible, parked at the mailbox centre
     +0.16s  reappears full size as the agent collects it
     +0.26s
     +0.37s  clear of the drawing at offset-distance 100%

   Each travelling leg starts with an ease out of rest — the timing function is
   declared on the keyframe that begins it, which is where CSS takes it from —
   and runs linear from there; the arrival itself is masked by the shrink into
   the mailbox, so the constant-time split does not show as a jolt on the two
   straight routes, whose mailbox sits at 65.77% rather than 79.45%.

   The three cornered routes cover 394.5 units in that 1.10s and the two
   straight ones 196, so a letter on the long way round moves half again as
   fast. That is unchanged from the 15s loop and is what the easing is for. */

/* h1  instrument -> cloud. Posted at 1.60s, waits 9.4s, collected at 11.00s —
   the longest wait in the figure, and the one that carries the whole idea. */
@keyframes wgd-letter-q1 {
  0%       { offset-distance: 0%; opacity: 0; transform: scale(0.7); }
  2.083%   { offset-distance: 0%; opacity: 0; transform: scale(0.7);
             animation-timing-function: cubic-bezier(0.4, 0, 0.35, 1); }
  2.875%   { opacity: 1; transform: scale(1); }
  5.875%   { opacity: 1; transform: scale(1); }
  6.667%   { offset-distance: var(--wgd-stop); opacity: 0; transform: scale(0.62); }
  45.833%  { offset-distance: var(--wgd-stop); opacity: 0; transform: scale(0.62);
             animation-timing-function: cubic-bezier(0.4, 0, 0.35, 1); }
  46.5%    { opacity: 1; transform: scale(1); }
  46.917%  { opacity: 1; transform: scale(1); }
  47.375%  { offset-distance: 100%; opacity: 0; transform: scale(1); }
  100%     { offset-distance: 100%; opacity: 0; transform: scale(0.7); }
}

/* h2  data store -> cloud. Posted at 4.10s, collected at 11.90s. */
@keyframes wgd-letter-q2 {
  0%       { offset-distance: 0%; opacity: 0; transform: scale(0.7); }
  12.5%    { offset-distance: 0%; opacity: 0; transform: scale(0.7);
             animation-timing-function: cubic-bezier(0.4, 0, 0.35, 1); }
  13.292%  { opacity: 1; transform: scale(1); }
  16.292%  { opacity: 1; transform: scale(1); }
  17.083%  { offset-distance: var(--wgd-stop); opacity: 0; transform: scale(0.62); }
  49.583%  { offset-distance: var(--wgd-stop); opacity: 0; transform: scale(0.62);
             animation-timing-function: cubic-bezier(0.4, 0, 0.35, 1); }
  50.25%   { opacity: 1; transform: scale(1); }
  50.667%  { opacity: 1; transform: scale(1); }
  51.125%  { offset-distance: 100%; opacity: 0; transform: scale(1); }
  100%     { offset-distance: 100%; opacity: 0; transform: scale(0.7); }
}

/* h3  HPC -> cloud. Posted at 6.60s, collected at 12.80s — last in, last out,
   because a mailbox is a queue and this one drains in the order it filled. */
@keyframes wgd-letter-q3 {
  0%       { offset-distance: 0%; opacity: 0; transform: scale(0.7); }
  22.917%  { offset-distance: 0%; opacity: 0; transform: scale(0.7);
             animation-timing-function: cubic-bezier(0.4, 0, 0.35, 1); }
  23.708%  { opacity: 1; transform: scale(1); }
  26.708%  { opacity: 1; transform: scale(1); }
  27.5%    { offset-distance: var(--wgd-stop); opacity: 0; transform: scale(0.62); }
  53.333%  { offset-distance: var(--wgd-stop); opacity: 0; transform: scale(0.62);
             animation-timing-function: cubic-bezier(0.4, 0, 0.35, 1); }
  54%      { opacity: 1; transform: scale(1); }
  54.417%  { opacity: 1; transform: scale(1); }
  54.875%  { offset-distance: 100%; opacity: 0; transform: scale(1); }
  100%     { offset-distance: 100%; opacity: 0; transform: scale(0.7); }
}

/* h4, h5, h6 — the three dispatches. One keyframe, written relative to a
   departure at 0, placed by the three .wgd-to-* delays. The park is 2.80s for
   all three: long enough for the flag to rise, hold and fall, short enough to
   keep the fan-out reading as one action. */
@keyframes wgd-letter-out {
  0%       { offset-distance: 0%; opacity: 0; transform: scale(0.7);
             animation-timing-function: cubic-bezier(0.4, 0, 0.35, 1); }
  0.792%   { opacity: 1; transform: scale(1); }
  3.792%   { opacity: 1; transform: scale(1); }
  4.583%   { offset-distance: var(--wgd-stop); opacity: 0; transform: scale(0.62); }
  16.25%   { offset-distance: var(--wgd-stop); opacity: 0; transform: scale(0.62);
             animation-timing-function: cubic-bezier(0.4, 0, 0.35, 1); }
  16.917%  { opacity: 1; transform: scale(1); }
  17.333%  { opacity: 1; transform: scale(1); }
  17.792%  { offset-distance: 100%; opacity: 0; transform: scale(1); }
  100%     { offset-distance: 100%; opacity: 0; transform: scale(0.7); }
}

/* Flutter: a few degrees, on a period that is not a factor of the cycle, so no
   two passes of the same letter are identical. Paper moving through air, not a
   wobble — the amplitude is deliberately below what reads as a gesture. It is
   a property of the paper rather than of the schedule, so it did not move with
   the loop. 24 / 1.4 = 17.143, so it still does not divide the cycle. */
@keyframes wgd-flutter {
  0%    { transform: rotate(-3.4deg) translateY(0.5px); }
  50%   { transform: rotate(3.4deg)  translateY(-0.5px); }
  100%  { transform: rotate(-3.4deg) translateY(0.5px); }
}

/* THE FLAG IS PIP 1 WITH ROTATION INSTEAD OF OPACITY. Up whenever depth >= 1,
   down otherwise; it is derived from the step function in the timetable above
   and must not be authored independently of it. The overshoot past vertical is
   the spring of a real flag being flicked up.

   ONE ELEMENT PER MAILBOX, HOWEVER MANY ARRIVALS. A lowered flag lies at
   rotate(-90deg) along the roof and is VISIBLE, so a second element is not a
   dormant copy, it is a drawn lowered flag under the raised one. That was the
   ghost an earlier revision fixed, and it is why the cloud's three arrivals
   are three windows in one timeline rather than three elements.

   All four mailboxes have a flag animation now. Two of them used to sit at the
   base style with no modifier, because on the old four-hop schedule they only
   ever sent; the two return hops added in v8 are what changed that.

   Every window is the same five-stop shape, hung off the arrival:

     arrival     still down. The letter has just reached the mailbox.
     +0.20s      flicked up, overshooting to +7deg, and turning accent.
     +0.32s      settles at vertical.
     collection  held. The batch is being collected.
     +0.20s      back down, back to edge colour.

   The "still down" stop at the head of each window is not padding. Without it
   the flag would interpolate continuously from the previous window's fall
   straight into the next one's rise, and would spend the gap drifting upward
   instead of resting. */

/* Cloud. Depth reaches 1 at 1.60s and returns to 0 at 12.80s, so this is ONE
   raise spanning all three arrivals and the whole batch collection — not three.
   That is the visible difference between a queue and a relay: on the old
   schedule this flag went up and down three times. Undelayed, so every
   percentage here is an absolute position in the 24s loop. */
@keyframes wgd-flag-cloud {
  0%       { transform: rotate(-90deg); color: var(--wgd-edge); }
  6.667%   { transform: rotate(-90deg); color: var(--wgd-edge); }
  7.5%     { transform: rotate(7deg);   color: var(--wgd-accent); }
  8%       { transform: rotate(0deg); }
  53.333%  { transform: rotate(0deg);   color: var(--wgd-accent); }
  54.167%  { transform: rotate(-90deg); color: var(--wgd-edge); }
  100%     { transform: rotate(-90deg); color: var(--wgd-edge); }
}

/* The three peripheral mailboxes. One window, relative to their own departure:
   up at +1.10s when the letter is posted, down at +3.90s when it is collected.
   Placed by .wgd-to-hpc / .wgd-to-store / .wgd-to-instr. */
@keyframes wgd-flag-rx {
  0%       { transform: rotate(-90deg); color: var(--wgd-edge); }
  4.583%   { transform: rotate(-90deg); color: var(--wgd-edge); }
  5.417%   { transform: rotate(7deg);   color: var(--wgd-accent); }
  5.917%   { transform: rotate(0deg); }
  16.25%   { transform: rotate(0deg);   color: var(--wgd-accent); }
  17.083%  { transform: rotate(-90deg); color: var(--wgd-edge); }
  100%     { transform: rotate(-90deg); color: var(--wgd-edge); }
}

/* THE PIPS. Each one is the union of the intervals where its mailbox's depth
   reaches its own number, and nothing else — read them against the step
   function in the timetable above. The 0.10s edges are so a pip lights and
   clears rather than snapping, at a duration short enough that the count is
   never ambiguous.

   ONE ANIMATION-NAME EACH. `animation-name: a, a` with two delays would put
   two infinite animations on one pip and the second would win forever; the
   windows go inside the keyframe instead. There is no pip in this figure that
   needs more than the one keyframe it has. */

/* cloud pip 1 — depth >= 1 on [1.6, 12.8) */
@keyframes wgd-pip-q1 {
  0%, 6.667%       { opacity: 0; }
  7.083%, 53.333%  { opacity: 1; }
  53.75%, 100%     { opacity: 0; }
}

/* cloud pip 2 — depth >= 2 on [4.1, 11.9) */
@keyframes wgd-pip-q2 {
  0%, 17.083%      { opacity: 0; }
  17.5%, 49.583%   { opacity: 1; }
  50%, 100%        { opacity: 0; }
}

/* cloud pip 3 — depth >= 3 on [6.6, 11.0) */
@keyframes wgd-pip-q3 {
  0%, 27.5%        { opacity: 0; }
  27.917%, 45.833% { opacity: 1; }
  46.25%, 100%     { opacity: 0; }
}

/* the peripheral mailboxes' single pip — depth >= 1 on [+1.10s, +3.90s) */
@keyframes wgd-pip-rx {
  0%, 4.583%       { opacity: 0; }
  5%, 16.25%       { opacity: 1; }
  16.667%, 100%    { opacity: 0; }
}

/* The receiving mailbox while a letter is inbound to it, ending at arrival —
   so the flash is the approach and the flag and pip are the arrival. Three
   windows for the cloud, which receives three; one for each of the others. */
@keyframes wgd-mflash-cloud {
  0%, 2.083%        { opacity: 0; }
  2.75%, 6%         { opacity: 1; }
  6.667%, 12.5%     { opacity: 0; }
  13.167%, 16.417%  { opacity: 1; }
  17.083%, 22.917%  { opacity: 0; }
  23.583%, 26.833%  { opacity: 1; }
  27.5%, 100%       { opacity: 0; }
}

@keyframes wgd-mflash-rx {
  0%                { opacity: 0; }
  0.667%, 3.917%    { opacity: 1; }
  4.583%, 100%      { opacity: 0; }
}

/* The cloud agent reasoning, and THE REASON THE QUEUE EXISTS. One window, up
   at 1.60s as the first message lands and down at 10.80s just before the batch
   is collected: the second and third messages arrive at 4.10s and 6.60s and
   have to wait, because the agent that would take them is busy. It used to be
   two 2.25s windows on two stacked elements.

   It breathes between full and 0.45 on a 1.5s beat rather than holding, so a
   9.2s window reads as work in progress rather than as a static selection
   ring, and lands on a peak before it goes — a conclusion, not a fade. */
@keyframes wgd-think {
  0%, 6.667%   { opacity: 0; }
  7.917%       { opacity: 1; }
  14.167%      { opacity: 0.45; }
  20.417%      { opacity: 1; }
  26.667%      { opacity: 0.45; }
  32.917%      { opacity: 1; }
  39.167%      { opacity: 0.45; }
  43.333%      { opacity: 1; }
  45%, 100%    { opacity: 0; }
}

.vision.is-running :is(.wgd-letter, .wgd-flag, .wgd-pip, .wgd-mflash, .wgd-think) {
  animation-duration: 24s;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
}

/* The three letters queued at the cloud each carry their own park length, so
   each names its own keyframe; the three dispatches are identical apart from
   when they happen, so they share one and are placed by delay. */
.vision.is-running .wgd-letter--h1 { animation-name: wgd-letter-q1; }
.vision.is-running .wgd-letter--h2 { animation-name: wgd-letter-q2; }
.vision.is-running .wgd-letter--h3 { animation-name: wgd-letter-q3; }
.vision.is-running :is(.wgd-letter--h4, .wgd-letter--h5, .wgd-letter--h6) { animation-name: wgd-letter-out; }

.vision.is-running .wgd-mflash--cloud { animation-name: wgd-mflash-cloud; }
.vision.is-running .wgd-mflash--rx    { animation-name: wgd-mflash-rx; }

.vision.is-running .wgd-pip--q1 { animation-name: wgd-pip-q1; }
.vision.is-running .wgd-pip--q2 { animation-name: wgd-pip-q2; }
.vision.is-running .wgd-pip--q3 { animation-name: wgd-pip-q3; }
.vision.is-running .wgd-pip--rx { animation-name: wgd-pip-rx; }

.vision.is-running .wgd-think   { animation-name: wgd-think; animation-timing-function: ease-in-out; }

/* All four mailboxes animate their flag now. The cloud's keyframe is undelayed
   and absolute; the other three share one keyframe and are placed by the delay
   class they already carry for their letter, their flash and their pip. If the
   cloud's flag ever picks up a .wgd-to-* class by accident, every window in it
   shifts by that delay and nothing will fail loudly. */
.vision.is-running .wgd-flag--cloud { animation-name: wgd-flag-cloud; animation-timing-function: ease-out; }
.vision.is-running .wgd-flag--rx    { animation-name: wgd-flag-rx;    animation-timing-function: ease-out; }

/* The flutter is on its own clock: it is a property of the paper, not of the
   cycle, so it does not belong in the timetable below. */
.vision.is-running .wgd-flutter {
  animation-name: wgd-flutter;
  animation-duration: 1.4s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}

/* Motion path is the one thing in this figure without a graceful failure of
   its own: with no offset-path, offset-distance moves nothing, and the six
   letters would fade in and out on top of each other at the origin of the
   viewBox rather than travelling. So the letters and their flutter come out.
   (Chrome 46+, Firefox 72+, Safari 16+ all support it; Safari 15 is who this
   is for.)

   The FLAGS AND PIPS NOW STAY IN, which is a change. They used to be dropped
   with the letters, because a flag rising with no letter to explain it said
   nothing. That is no longer true: the trays fill, hold and drain on their
   own, and mailbox depth over time is the content of this figure now rather
   than an annotation on the letters. A Safari 15 reader gets the queue without
   the envelopes, which is the whole argument minus its illustration. */
@supports not (offset-path: path("M0,0 H1")) {
  .vision.is-running :is(.wgd-letter, .wgd-flutter) { animation-name: none; }
}

/* The three dispatches out of the cloud, and the only delays in the figure.
   Everything belonging to one delivery — the letter, and the receiving
   mailbox's flash, flag and pip — carries the same class, so retiming a
   delivery is one number here. They are 1.5s apart.

   Nothing on the CLOUD mailbox may carry one of these. Its keyframes are
   written as absolute positions in the 24s loop, because its behaviour is
   unique and there is nothing for it to share with; a delay class landing on
   one of them would shift every window in it silently. */
.wgd-to-hpc   { animation-delay: 13.6s; }
.wgd-to-store { animation-delay: 15.1s; }
.wgd-to-instr { animation-delay: 16.6s; }

/* The instrument bubbles. A 13s focus-and-tether cycle against the message
   loop's 24s means the two never settle into a shared beat, and an orbit
   period per bubble that shares no factor with the focus cycle means the five
   never line up with each other.

   NOTHING BELOW MOVED IN v8, and the claim that used to sit here has been
   corrected rather than carried forward. It read "every period below is
   coprime with both 15 and 13", which was true of the 15s loop and is not true
   of the 24s one: gcd(22,24)=2, gcd(14,24)=2 and gcd(16,24)=8. What actually
   matters is the failure this figure had once — an orbit of exactly 11s
   against a loop of exactly 11s, phase-locked for good — and none of 17, 22,
   14, 19, 16 or 13 either equals 24 or divides it. The tightest coupling left
   is the 16s orbit realigning with the letters every 48s, on a 7-unit drift.
   If you retime either cycle, check equality and division first, and write
   down what you actually computed. */

@keyframes wgd-tether {
  0%    { stroke-dashoffset: 100; }
  22%   { stroke-dashoffset: 0; }
  62%   { stroke-dashoffset: 0; }
  84%   { stroke-dashoffset: -100; }
  100%  { stroke-dashoffset: -100; }
}

/* The ring changes weight and colour rather than fading, because
   --border-strong at 0.6 opacity composites to about 1.9:1 and would drop a
   visible stroke below the 3:1 floor. Both ends clear it: --border-strong
   3.35 / 3.51 and --text 17.79 / 15.81. The icon's own dip is fine at
   4.88 / 6.38, because it composites against the bubble fill rather than
   disappearing into it.

   The in-focus ring is --text and deliberately NOT --accent. Accent means one
   thing in this figure — "the Academy component". Borrowing it to mean "this
   bubble is in focus" would put the figure's one colour rule to two uses. */
@keyframes wgd-bub-ring {
  0%, 100% { stroke: var(--wgd-edge);  stroke-width: 1.6; }
  22%, 62% { stroke: var(--wgd-title); stroke-width: 2.6; }
  84%      { stroke: var(--wgd-edge);  stroke-width: 1.6; }
}

@keyframes wgd-bub-ico {
  0%, 100% { opacity: 0.62; }
  22%, 62% { opacity: 1; }
  84%      { opacity: 0.62; }
}

@keyframes wgd-bub-mark {
  0%, 100% { transform: scale(0.93); }
  22%, 62% { transform: scale(1.02); }
  84%      { transform: scale(0.93); }
}

/* Two orbits, mirrored in direction, so neighbouring bubbles never drift as a
   block. Eight stops approximate the circle closely enough at this radius that
   the corners are not visible, and translate-only keeps it origin-independent. */
@keyframes wgd-orbit-a {
  0%     { transform: translate(0px, -7px); }
  12.5%  { transform: translate(4.95px, -4.95px); }
  25%    { transform: translate(7px, 0px); }
  37.5%  { transform: translate(4.95px, 4.95px); }
  50%    { transform: translate(0px, 7px); }
  62.5%  { transform: translate(-4.95px, 4.95px); }
  75%    { transform: translate(-7px, 0px); }
  87.5%  { transform: translate(-4.95px, -4.95px); }
  100%   { transform: translate(0px, -7px); }
}

@keyframes wgd-orbit-b {
  0%     { transform: translate(0px, -6px); }
  12.5%  { transform: translate(-6.36px, -4.24px); }
  25%    { transform: translate(-9px, 0px); }
  37.5%  { transform: translate(-6.36px, 4.24px); }
  50%    { transform: translate(0px, 6px); }
  62.5%  { transform: translate(6.36px, 4.24px); }
  75%    { transform: translate(9px, 0px); }
  87.5%  { transform: translate(6.36px, -4.24px); }
  100%   { transform: translate(0px, -6px); }
}

.vision.is-running :is(.wgd-tether, .wgd-bub-ring, .wgd-ico, .wgd-bub-mark) {
  animation-duration: 13s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}

.vision.is-running .wgd-tether   { animation-name: wgd-tether; }
.vision.is-running .wgd-bub-ring { animation-name: wgd-bub-ring; }
.vision.is-running .wgd-ico      { animation-name: wgd-bub-ico; }
.vision.is-running .wgd-bub-mark { animation-name: wgd-bub-mark; }

.vision.is-running .wgd-bub-orbit {
  animation-iteration-count: infinite;
  animation-timing-function: linear;
}

/* Negative delays so the five are already spread across the focus cycle on the
   first frame rather than starting in lockstep — at any moment one tether is
   drawing, two are held, one is retracting and one is away. Five phases evenly
   across 13s is 2.6s apart. */
.vision.is-running .wgd-tethers > :nth-child(1),
.vision.is-running .wgd-bubbles > :nth-child(1) :is(.wgd-bub-ring, .wgd-ico, .wgd-bub-mark) { animation-delay:   0s; }
.vision.is-running .wgd-tethers > :nth-child(2),
.vision.is-running .wgd-bubbles > :nth-child(2) :is(.wgd-bub-ring, .wgd-ico, .wgd-bub-mark) { animation-delay: -2.6s; }
.vision.is-running .wgd-tethers > :nth-child(3),
.vision.is-running .wgd-bubbles > :nth-child(3) :is(.wgd-bub-ring, .wgd-ico, .wgd-bub-mark) { animation-delay: -5.2s; }
.vision.is-running .wgd-tethers > :nth-child(4),
.vision.is-running .wgd-bubbles > :nth-child(4) :is(.wgd-bub-ring, .wgd-ico, .wgd-bub-mark) { animation-delay: -7.8s; }
.vision.is-running .wgd-tethers > :nth-child(5),
.vision.is-running .wgd-bubbles > :nth-child(5) :is(.wgd-bub-ring, .wgd-ico, .wgd-bub-mark) { animation-delay: -10.4s; }

/* Orbit period and direction per bubble. No two share a period; none is a
   multiple of the 13s focus cycle; and none equals or divides the 24s message
   loop, so no bubble ever locks to the letters. 17, 22, 14, 19, 16 — none
   divisible by 13, and 24 divided by each of them is 1.41, 1.09, 1.71, 1.26
   and 1.5. See the note above for what "coprime" used to claim here. */
.vision.is-running .wgd-bub:nth-child(1) .wgd-bub-orbit { animation-name: wgd-orbit-a; animation-duration: 17s; animation-delay: -2s; }
.vision.is-running .wgd-bub:nth-child(2) .wgd-bub-orbit { animation-name: wgd-orbit-b; animation-duration: 22s; animation-delay: -9s; }
.vision.is-running .wgd-bub:nth-child(3) .wgd-bub-orbit { animation-name: wgd-orbit-a; animation-duration: 14s; animation-delay: -5s; }
.vision.is-running .wgd-bub:nth-child(4) .wgd-bub-orbit { animation-name: wgd-orbit-b; animation-duration: 19s; animation-delay: -3s; }
.vision.is-running .wgd-bub:nth-child(5) .wgd-bub-orbit { animation-name: wgd-orbit-a; animation-duration: 16s; animation-delay: -7s; }

/* Hovering one holds it where it is and brings it to full focus while its
   label is up. The orbit is paused, so the bubble does not slide out from
   under the pointer. The focus animations are dropped rather than paused, so
   the icon and ring land on their base state — full opacity, normal weight —
   instead of freezing at whatever the drift had reached, which left a hovered
   bubble reading as faded exactly when it was being asked about. */
.vision .wgd-bub:hover .wgd-bub-orbit { animation-play-state: paused; }
.vision .wgd-bub:hover :is(.wgd-bub-mark, .wgd-ico, .wgd-bub-ring) { animation-name: none; }

/* ---- Controls -----------------------------------------------------------
   There are none, and there is no .is-paused state left for one to drive. The
   pause button was removed at the client's request; site.js injects nothing.
   What remains is that motion stops entirely under prefers-reduced-motion and
   whenever the figure is off screen. If a control is ever reinstated it needs
   both the button in site.js and a rule that reaches every animated element in
   the drawing — one blanket `animation-play-state: paused` descendant
   selector, not a list, because the list will fall behind the markup. */

/* --------------------------------------------------------------------------
   10. Code blocks & syntax highlighting
   -------------------------------------------------------------------------- */

.code-block {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--code-bg);
  overflow: hidden;
}

.code-block__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 0.9rem;
  border-bottom: 1px solid var(--border);
  background: var(--bg-inset);
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.code-block pre,
.code-block .highlight { margin: 0; border: 0; border-radius: 0; }

.copy-button {
  padding: 0.2rem 0.55rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--bg-raised);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.copy-button:hover { color: var(--text); border-color: var(--text-faint); }

pre {
  padding: 1rem 1.15rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--code-bg);
  color: var(--code-text);
  font-size: 0.875rem;
  line-height: 1.6;
  overflow-x: auto;
  tab-size: 4;
}

pre code {
  padding: 0;
  border: 0;
  background: none;
  font-size: inherit;
  color: inherit;
}

:not(pre) > code {
  padding: 0.15em 0.4em;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-inset);
  color: var(--code-text);
  font-size: 0.875em;
  word-break: break-word;
}

.highlight { background: var(--code-bg); }

.highlight .c, .highlight .c1, .highlight .cm, .highlight .cs,
.highlight .cp, .highlight .cd { color: var(--hl-comment); font-style: italic; }

.highlight .k, .highlight .kn, .highlight .kc, .highlight .kd,
.highlight .kp, .highlight .kr, .highlight .kt,
.highlight .ow { color: var(--hl-keyword); }

.highlight .s, .highlight .s1, .highlight .s2, .highlight .sb, .highlight .sc,
.highlight .sd, .highlight .se, .highlight .sh, .highlight .si, .highlight .sx,
.highlight .sr, .highlight .ss { color: var(--hl-string); }

.highlight .m, .highlight .mf, .highlight .mh, .highlight .mi,
.highlight .mo, .highlight .il { color: var(--hl-number); }

.highlight .n, .highlight .na, .highlight .nb, .highlight .nc, .highlight .nd,
.highlight .ne, .highlight .nf, .highlight .ni, .highlight .nl, .highlight .nn,
.highlight .no, .highlight .nt, .highlight .nv,
.highlight .bp { color: var(--hl-name); }

.highlight .o, .highlight .p { color: var(--hl-operator); }

.highlight .err { color: var(--hl-error); }

/* --------------------------------------------------------------------------
   11. Code bands, split features, tiles, dense lists
   -------------------------------------------------------------------------- */

/* A concept paired with the code that does it. Bands alternate sides so the
   page has a rhythm without needing alternating background colours.

   Because they alternate AND have unequal heights, consecutive bands visually
   interleave — the tall code block of one sits level with the next band's text.
   A full-width rule plus generous padding makes each boundary unambiguous. */
.code-bands { display: grid; gap: 0; }

.code-band + .code-band {
  margin-top: 4.5rem;
  padding-top: 4.5rem;
  border-top: 1px solid var(--border);
}

/* Top-aligned, not centred — the text column is much shorter than the code, and
   centring leaves it floating in dead space. */
.code-band {
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.28fr);
  align-items: start;
  gap: 3rem;
}

/* Slightly smaller than body code so realistic lines fit without a scrollbar. */
.code-band__code pre { font-size: 0.82rem; }

.code-band__text { min-width: 0; max-width: 46ch; }
.code-band__code { min-width: 0; }

.code-band__text h2 {
  margin-bottom: 0.85rem;
  font-size: clamp(1.5rem, 2.6vw, 1.9rem);
  letter-spacing: -0.022em;
}

.code-band__text p { color: var(--text-muted); }
.code-band__text p + p { margin-top: 0.85rem; }

/* Reads as a step marker in a sequence: "01 ———" */
.band-number {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  margin-bottom: 0.9rem;
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.1em;
}

.band-number::after {
  content: "";
  width: 32px;
  height: 1px;
  background: var(--accent-border);
}


/* Compact tiles — deliberately lighter than .card: a rule, not a box. */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 2rem 2.5rem;
}

.tile { padding-top: 1.1rem; border-top: 1px solid var(--border); }
.tile h3 { margin-bottom: 0.4rem; font-size: 1rem; font-weight: 640; }
.tile p { color: var(--text-muted); font-size: 0.925rem; line-height: 1.6; }

.two-col {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
  gap: 3.5rem;
}

/* Dense term/description list. Carries far more information per screen than a
   grid of cards, which is the point. */
.dl-list { margin-top: 1.25rem; }

.dl-list__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.15rem;
  padding-block: 1rem;
  border-top: 1px solid var(--border);
}

.dl-list__row:last-child { border-bottom: 1px solid var(--border); }

.dl-list dt { font-size: 0.95rem; font-weight: 640; }

.dl-list dd {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.925rem;
  line-height: 1.6;
}

/* Citation callout. */
.citation-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 2rem;
}

.citation-strip__text { max-width: 62ch; }
.citation-strip__title { margin-bottom: 0.4rem; font-size: 1.25rem; }
.citation-strip__authors { color: var(--text-muted); font-size: 0.95rem; }

.citation-strip__venue {
  margin-top: 0.2rem;
  color: var(--text-faint);
  font-size: 0.9rem;
  font-style: italic;
}

/* --------------------------------------------------------------------------
   12. Prose
   -------------------------------------------------------------------------- */

.page-header {
  padding-block: 3.5rem 2.5rem;
  border-bottom: 1px solid var(--border);
  background: var(--bg-subtle);
}

.page-header h1 {
  font-size: clamp(1.9rem, 4vw, 2.6rem);
  letter-spacing: -0.03em;
}

.page-header__lead {
  max-width: var(--measure);
  margin-top: 0.85rem;
  color: var(--text-muted);
  font-size: 1.075rem;
}

.page-header__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1rem;
  color: var(--text-faint);
  font-size: 0.875rem;
}

.back-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 1.25rem;
  color: var(--text-muted);
  font-size: 0.9rem;
  font-weight: 500;
}

.back-link:hover { color: var(--accent); text-decoration: none; }

.prose {
  max-width: var(--measure);
  padding-block: 3rem 4rem;
  font-size: 1.0625rem;
  line-height: 1.75;
}

.prose > * + * { margin-top: 1.25rem; }

.prose h2 {
  margin-top: 2.75rem;
  font-size: 1.5rem;
  letter-spacing: -0.02em;
}

.prose h3 {
  margin-top: 2.25rem;
  font-size: 1.2rem;
}

.prose h4 {
  margin-top: 1.75rem;
  font-size: 1.05rem;
}

.prose h2 + *, .prose h3 + *, .prose h4 + * { margin-top: 0.75rem; }

.prose ul, .prose ol { padding-left: 1.4rem; }
.prose li + li { margin-top: 0.4rem; }

.prose blockquote {
  padding-left: 1.15rem;
  border-left: 3px solid var(--accent-border);
  color: var(--text-muted);
  font-style: italic;
}

.prose hr {
  margin-block: 2.5rem;
  border: 0;
  border-top: 1px solid var(--border);
}

.prose img { border-radius: var(--radius); }

.prose figure figcaption {
  margin-top: 0.6rem;
  color: var(--text-faint);
  font-size: 0.875rem;
  text-align: center;
}

.post-footer {
  max-width: var(--measure);
  margin-bottom: 4rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 0.95rem;
}

/* --------------------------------------------------------------------------
   13. Tables
   -------------------------------------------------------------------------- */

.table-wrap { overflow-x: auto; }

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
}

th, td {
  padding: 0.7rem 0.9rem;
  border-bottom: 1px solid var(--border);
  text-align: left;
  vertical-align: top;
}

thead th {
  border-bottom: 1px solid var(--border-strong);
  color: var(--text-faint);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

tbody tr:last-child td { border-bottom: 0; }

.schedule-table td:first-child {
  width: 150px;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  white-space: nowrap;
}

.schedule-table tr.hands-on td:last-child { color: var(--accent); font-weight: 600; }
.schedule-table tr.break td { color: var(--text-faint); font-style: italic; }

/* --------------------------------------------------------------------------
   14. Publications & team
   -------------------------------------------------------------------------- */

.pub-list { list-style: none; padding: 0; display: grid; gap: 1rem; }

.pub {
  padding: 1.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-raised);
}

.pub__title { font-size: 1.05rem; font-weight: 640; margin-bottom: 0.35rem; }
.pub__authors { color: var(--text-muted); font-size: 0.925rem; }

.pub__venue {
  margin-top: 0.2rem;
  color: var(--text-faint);
  font-size: 0.875rem;
  font-style: italic;
}

.pub__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.9rem;
}

.pub__links a {
  padding: 0.25rem 0.65rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-size: 0.82rem;
  font-weight: 600;
}

.pub__links a:hover { border-color: var(--accent); text-decoration: none; }

/* --- News & blog index ---------------------------------------------------
   One featured post, then a dense dated list. See blog/index.html for why. */

.featured-post {
  display: block;
  position: relative;
  padding: 2.25rem 2.5rem 2rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-raised);
  box-shadow: var(--shadow-sm);
  color: inherit;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.featured-post:hover {
  border-color: var(--accent-border);
  box-shadow: var(--shadow-md);
  text-decoration: none;
}

.featured-post__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 1rem;
  color: var(--text-faint);
  font-size: 0.85rem;
}

.featured-post__title {
  max-width: 32ch;
  margin-bottom: 0.75rem;
  font-size: clamp(1.5rem, 3vw, 2rem);
  letter-spacing: -0.025em;
}

.featured-post:hover .featured-post__title { color: var(--accent); }

.featured-post__excerpt {
  max-width: 60ch;
  color: var(--text-muted);
  font-size: 1.05rem;
  line-height: 1.6;
}

.featured-post__more {
  display: inline-block;
  margin-top: 1.25rem;
  color: var(--accent);
  font-size: 0.95rem;
  font-weight: 600;
}

.post-list { margin-top: 3.5rem; }

.post-list__year {
  margin-bottom: 0.25rem;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.1em;
}

.post-list__year:not(:first-child) { margin-top: 2.5rem; }

.post-row {
  display: grid;
  position: relative;
  grid-template-columns: 74px 104px minmax(0, 1fr) 20px;
  align-items: baseline;
  gap: 1rem;
  padding-block: 1.1rem;
  border-top: 1px solid var(--border);
  color: inherit;
}

.post-row:last-of-type { border-bottom: 1px solid var(--border); }

.post-row:hover { background: var(--bg-subtle); text-decoration: none; }
.post-row:hover .post-row__title { color: var(--accent); }
.post-row:hover .post-row__arrow { color: var(--accent); transform: translateX(2px); }

.post-row__date {
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 0.82rem;
  white-space: nowrap;
}

.post-row__body { display: block; min-width: 0; }

.post-row__title {
  display: block;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.4;
}

.post-row__summary {
  display: block;
  margin-top: 0.2rem;
  color: var(--text-muted);
  font-size: 0.9rem;
  line-height: 1.5;
}

.post-row__arrow {
  color: var(--text-faint);
  font-size: 0.95rem;
  text-align: right;
  transition: color 0.15s ease, transform 0.15s ease;
}

.post-list__feed {
  margin-top: 2.5rem;
  color: var(--text-muted);
  font-size: 0.925rem;
}

.year-heading {
  font-size: 1.2rem;
  font-weight: 640;
  margin-bottom: 1.25rem;
}

.pub-list + .year-heading { margin-top: 3rem; }

.content-block { margin-top: 3.5rem; }
.content-block > * + * { margin-top: 1rem; }
.content-block > .year-heading { margin-bottom: 1.25rem; }

.team-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1.25rem;
}

.person {
  padding: 1.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-raised);
  text-align: center;
}

.person__avatar {
  width: 72px;
  height: 72px;
  margin: 0 auto 1rem;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--bg-inset);
  object-fit: cover;
}

/* Fallback while a photo is pending: the person's initial, not a dashed
   placeholder — these are real people, and the card should look finished. */
.person__avatar--initial {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  background: var(--accent-subtle);
  border-color: var(--accent-border);
  font-size: 1.5rem;
  font-weight: 640;
  line-height: 1;
}

.person__github {
  display: inline-block;
  margin-top: 0.6rem;
  font-size: 0.8rem;
  font-weight: 600;
}

.person__name { font-size: 1rem; font-weight: 640; }
.person__role { margin-top: 0.15rem; color: var(--accent); font-size: 0.85rem; font-weight: 500; }
.person__affiliation { margin-top: 0.15rem; color: var(--text-muted); font-size: 0.85rem; }

/* --------------------------------------------------------------------------
   15. Placeholder markers
   Every one of these is scaffolding awaiting real content. Grep for
   "placeholder" to find them all before launch.
   -------------------------------------------------------------------------- */

.placeholder-note {
  display: flex;
  gap: 0.75rem;
  padding: 1rem 1.25rem;
  margin-bottom: 2rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-inset);
  color: var(--text-muted);
  font-size: 0.9rem;
  line-height: 1.55;
}

.placeholder-note strong { color: var(--text); }

.placeholder-note::before {
  content: "\270E";
  flex-shrink: 0;
  font-size: 1rem;
  line-height: 1.55;
}

.is-placeholder { border-style: dashed; }

.is-placeholder .pub__title { color: var(--text-faint); }

/* --------------------------------------------------------------------------
   16. Footer
   -------------------------------------------------------------------------- */

.site-footer {
  flex-shrink: 0;
  padding-block: 3.5rem 2.5rem;
  border-top: 1px solid var(--border);
  background: var(--bg-subtle);
  font-size: 0.925rem;
}

/* auto-fit cannot resolve against a leading fr track — it collapses to a single
   repetition — so the columns are stated explicitly and stepped down below. */
.footer-grid {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1fr 1fr;
  gap: 2.5rem;
  padding-bottom: 2.5rem;
  border-bottom: 1px solid var(--border);
}

/* width:auto is required — the img carries width="500", which would otherwise
   stretch it against the fixed height. */
.footer-brand img { height: 32px; width: auto; margin-bottom: 1rem; }

.footer-brand p {
  max-width: 32ch;
  color: var(--text-muted);
  font-size: 0.9rem;
}

.footer-col__title {
  margin-bottom: 0.85rem;
  color: var(--text-faint);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.footer-col ul { list-style: none; padding: 0; }
.footer-col li + li { margin-top: 0.5rem; }

.footer-col a { color: var(--text-muted); }
.footer-col a:hover { color: var(--accent); }

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 1.75rem;
  color: var(--text-faint);
  font-size: 0.85rem;
}

.footer-bottom p { max-width: 60ch; }

/* --------------------------------------------------------------------------
   17. Responsive
   -------------------------------------------------------------------------- */

/* Layout stacks at 900px, but the nav links still fit — collapsing them here
   cost a 900px window its entire navigation. The nav has its own breakpoint
   further down. */
@media (max-width: 900px) {
  :root { --header-h: 58px; }

  .section { padding-block: 4rem; }
  .hero { padding-block: 4rem 3.5rem; }
  .hero--split { padding-block: 3rem 3rem; }

  /* Stack every two-column layout, and always put prose above its figure.
     Keep the measure — releasing it entirely gave ~95-character lines. */
  .hero__grid { grid-template-columns: 1fr; gap: 2.5rem; }

  /* No gap here: .code-band + .code-band already spaces symmetrically. */
  .code-band { grid-template-columns: 1fr; gap: 1.5rem; }
  .code-band__text { max-width: var(--measure); }

  .arch-split { grid-template-columns: 1fr; gap: 2.5rem; }
  /* 210px for a term plus a readable description does not fit here; stack. */
  .arch-split--single .dl-list__row { grid-template-columns: minmax(0, 1fr); gap: 0.15rem; }
  .arch-figure__media { padding: 1rem; }

  .dl-list--split { columns: 1; }

  .section-head--row { flex-direction: column; align-items: flex-start; }

  .footer-grid { grid-template-columns: repeat(3, 1fr); gap: 2rem; }
  .footer-brand { grid-column: 1 / -1; }
}

/* Navigation collapse. Gated on `.js` (set by the inline head script) so that
   with scripting blocked the menu stays in flow and reachable — hiding it in
   CSS that only JS can undo reintroduced the very bug this replaced. */
@media (max-width: 760px) {
  .site-header { position: sticky; }
  .site-nav { position: relative; flex-wrap: wrap; }

  .nav-menu {
    order: 3;
    flex: 0 0 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding-bottom: 0.75rem;
  }

  .nav-menu a {
    padding: 0.7rem 0.5rem;
    border-radius: var(--radius-sm);
    font-size: 1rem;
  }

  .js .nav-toggle { display: inline-flex; }

  .js .nav-menu {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    display: none;
    flex: initial;
    padding: 0.5rem 24px 1rem;
    border-bottom: 1px solid var(--border);
    background: var(--bg);
    box-shadow: var(--shadow-md);
  }

  .js .nav-menu.is-open { display: flex; }
}

@media (max-width: 700px) {
  /* Four columns will not fit: put the date and type on their own line and let
     the title and summary span the full width beneath them. */
  .post-row {
    grid-template-columns: auto auto minmax(0, 1fr);
    grid-template-areas:
      "date kind arrow"
      "body body body";
    align-items: center;
    row-gap: 0.45rem;
  }

  .post-row__date  { grid-area: date; }
  .post-row__kind  { grid-area: kind; }
  .post-row__body  { grid-area: body; }
  .post-row__arrow { grid-area: arrow; }

  .featured-post { padding: 1.75rem 1.5rem 1.5rem; }
  .featured-post__title { max-width: none; }

  /* SVG text scales with the drawing, so below this width the vision figure
     stops shrinking and scrolls sideways inside .vision__media instead. 630px
     of an 820-unit viewBox holds the smallest labels at 8.1px, which is what
     the previous, narrower drawing rendered at its worst — letting this one
     shrink to a 460px container instead would put them near 6px. The figure
     must not scroll at the breakpoint itself, so this sits under the container
     width a 700px viewport actually has once its scrollbar and padding are
     taken off (700 - 15 - 48 = 637). WCAG 1.4.10 exempts diagrams from
     reflow; the region is keyboard reachable for 2.1.1. */
  .vision__svg { min-width: 630px; }
}

@media (max-width: 560px) {
  .container { padding-inline: 18px; }
  .nav-menu { padding-inline: 18px; }
  .card, .step, .pub, .person { padding: 1.25rem; }
  .button-row .button { width: 100%; }
  .schedule-table td:first-child { width: auto; }
  .footer-grid { grid-template-columns: repeat(2, 1fr); }
}

/* --------------------------------------------------------------------------
   18. Motion preferences
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .card:hover,
  .button:hover { transform: none; }

  /* The blanket rule above already collapses these to 0.01ms, but say it
     outright for the vision figure, because what matters is *which* state it
     lands in. Every one of these animations is authored so that its base style
     is the informative one:

       - all five instrument bubbles present at equal, full opacity and
         unscaled — not one, frozen. The point of the group is that the
         instrument is plural, and freezing narrows it to a claim about one
         instrument;
       - all five tethers drawn full length, so the connections read as made
         rather than as caught mid-formation;
       - the whole wire network drawn, every mailbox on the rail and no letters
         in flight.

     THE MESSAGE LAYER IS THE EXCEPTION, AND IT GETS BETTER RATHER THAN WORSE.
     Queue depth is legible in a still frame in a way a moving letter never
     was, so freezing everything at its base style would throw away the one
     part of this figure that does not need motion. The base styles alone would
     land on four empty trays, four lowered flags and no halo — an idle
     exchange, which is a state the loop is only in for three seconds out of
     twenty-four and which teaches nothing.

     So the static frame is a real, chosen moment: t = 8s, the middle of the
     reasoning window. The cloud's mailbox holds three of its four, flag up;
     the cloud agent's halo is on, which is WHY they are waiting; the other
     three trays are empty with their flags down. That is the whole v8 idea in
     one frame — sense, queue, reason — and it is the only frame that states it
     without motion.

     Depth is carried by the count and by which sockets are filled, not by
     colour: three lit and one empty reads as three of four in monochrome.

     The order of the loop is in the text alternative. It is not drawn here and
     it is not drawn in motion either: the numbered markers went with the step
     list, at the client's request.

     site.js injects no pause control here — or anywhere.

     This is `animation: none`, not the blanket 0.01ms above it. Both land on
     the base style, but only this one says so, and the blanket rule depends on
     no animation in the figure ever using a fill mode. It is also a rule with
     declarations in it: the selector list that used to be here ended in a
     trailing comma with an empty body, so the whole thing was a parse error
     and nothing in this block applied at all.

     The one colour set below is the raised cloud flag's, and it is the same
     --wgd-accent the moving version raises it in — a state, not a repaint.
     Nothing here defines a colour of its own; the tokens have already handled
     the theme. */
  .vision .wgd-bub-orbit,
  .vision .wgd-bub-mark,
  .vision .wgd-bub-ring,
  .vision .wgd-ico,
  .vision .wgd-tether,
  .vision .wgd-letter,
  .vision .wgd-flutter,
  .vision .wgd-flag,
  .vision .wgd-pip,
  .vision .wgd-think,
  .vision .wgd-mflash {
    animation: none !important;
  }

  /* t = 8s: three messages waiting at the cloud, its flag up, its agent
     reasoning. Every one of these overrides a base style of the same element
     at one class more specificity, so nothing here needs !important. */
  .vision .wgd-pip--q1,
  .vision .wgd-pip--q2,
  .vision .wgd-pip--q3 { opacity: 1; }

  .vision .wgd-flag--cloud { transform: rotate(0deg); color: var(--wgd-accent); }

  .vision .wgd-think { opacity: 1; }
}

@media print {
  .site-header, .site-footer, .back-link, .copy-button { display: none; }
  body { color: #000; background: #fff; }
  .prose { max-width: none; }
}

/* --------------------------------------------------------------------------
   19. Components added after the multi-perspective review
   -------------------------------------------------------------------------- */

/* Whole-block click target. The parent must be position: relative. Used by
   .card--link, .featured-post and .post-row so the accessible name comes from
   the title alone rather than from every word in the block. */
.stretched::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.featured-post__title a,
.post-row__title a { color: inherit; }

.featured-post:hover .featured-post__title a,
.post-row:hover .post-row__title a { color: var(--accent); text-decoration: none; }

/* Events already past are muted; the row shows event_date, not the post date. */
.post-row--past .post-row__title { color: var(--text-muted); font-weight: 550; }
.post-row--past .tag { color: var(--text-faint); }

/* Licence / Python / version strip under the hero install command. */
.facts-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.85rem;
  color: var(--text-faint);
  font-size: 0.82rem;
}

/* The problem statement, quoted from the paper. */
.problem__text { max-width: 62ch; }
.problem__text .section-title { margin-bottom: 1rem; }
.problem__text p { color: var(--text-muted); font-size: 1.05rem; line-height: 1.7; }
.problem__text p + p { margin-top: 1rem; }

.band-next { margin-top: 3.5rem; }

/* Numbered step headings on /start/. Previously these reused .year-heading,
   a class named for the publications page, and came out smaller than the code
   blocks beneath them. */
.step-heading {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.85rem;
  font-size: 1.35rem;
  font-weight: 640;
  letter-spacing: -0.02em;
}

.step-heading__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border: 1px solid var(--accent-border);
  border-radius: 50%;
  background: var(--accent-subtle);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  font-weight: 700;
}

/* Replaces inline style="max-width: 68ch" repeated on individual paragraphs. */
.measured { max-width: var(--measure); color: var(--text-muted); }

.note {
  padding-left: 0.9rem;
  border-left: 2px solid var(--accent-border);
  font-size: 0.925rem;
}

.requirements { margin-bottom: 3.5rem; }
.requirements .dl-list__row { grid-template-columns: 160px minmax(0, 1fr); gap: 1rem; }
.requirements dt { color: var(--text-faint); font-weight: 600; }

/* Code blocks span the full container while their lines reach ~700px; cap them
   so they share a right edge with the prose. */
.content-block .code-block { max-width: 860px; }
.code-block--narrow { max-width: 520px; }
.code-block--bare:not(.code-block--inline) { position: relative; }
.code-block--bare:not(.code-block--inline) .copy-button {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
}

/* Two-column dense list. Uses columns rather than a grid so rows flow evenly
   instead of leaving one column dead-ending above the other. */
.dl-list--split { columns: 2; column-gap: 3.5rem; }

/* Every row closes itself: under `columns`, :last-child sits at the foot of the
   right column, leaving the left column trailing off unruled. */
.dl-list--split .dl-list__row {
  break-inside: avoid;
  border-top: 0;
  border-bottom: 1px solid var(--border);
}

@media (max-width: 900px) {
  .step-heading { font-size: 1.2rem; }
  .content-block .code-block { max-width: none; }
}

/* --------------------------------------------------------------------------
   20. Fixes from the second review round
   -------------------------------------------------------------------------- */

/* Links inside prose were distinguished by colour alone: --accent against
   --text-muted is 1.19:1 in light and 1.02:1 in dark, well under the 3:1 that
   WCAG 1.4.1 requires when colour is the only signal. Underline them. */
.prose a,
.tile p a,
.card p a,
.section-lead a,
.problem__text a,
.measured a,
.dl-list dd a,
.post-list__feed a,
.footer-bottom a { text-decoration: underline; text-underline-offset: 0.15em; }

/* Restyled after the footer column titles moved from <h2> to <p>. */
.footer-col__title {
  margin-bottom: 0.85rem;
  color: var(--text-faint);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* The stretched overlay swallowed mouse selection of real prose. Lift the
   blocks that carry sentences back above it; the rest of the card stays
   clickable. */
.featured-post__excerpt,
.post-row__summary,
.card p { position: relative; z-index: 1; }

/* Four-item grids resolved to three columns and orphaned the last tile. */
.tile-grid--four { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* Quoted source material in the problem statement. */
.problem {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 3rem;
  align-items: start;
}

.problem__quote {
  margin: 0;
  padding-left: 1.15rem;
  border-left: 3px solid var(--accent-border);
  font-size: 1.2rem;
  line-height: 1.55;
  color: var(--text);
}

.problem__quote cite {
  display: block;
  margin-top: 0.75rem;
  color: var(--text-faint);
  font-size: 0.85rem;
  font-style: normal;
}

/* The citation label outweighed the citation itself. */
.citation-strip { display: block; }
.citation-strip__text { max-width: 70ch; }

.citation-strip__title {
  margin-bottom: 0.75rem;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.citation-strip__paper { font-size: 1.25rem; font-weight: 640; line-height: 1.35; }
.citation-strip .button-row { margin-top: 1.5rem; }

/* Horizontally scrollable regions must be keyboard reachable. */
.table-wrap:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Evidence links on case-study tiles. */
.tile__source {
  display: block;
  margin-top: 0.75rem;
  font-size: 0.85rem;
  font-weight: 600;
}

.tile__meta {
  display: block;
  margin-bottom: 0.5rem;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.tile__result {
  display: block;
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px dashed var(--border);
  color: var(--text-muted);
  font-size: 0.875rem;
}

@media (max-width: 900px) {
  .problem { grid-template-columns: 1fr; gap: 2rem; }
  .tile-grid--four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 760px) {
  /* Without JS the menu sits in flow, so a sticky header eats ~40% of a small
     screen and no longer matches scroll-padding-top. */
  html:not(.js) .site-header { position: static; }
}

@media (max-width: 560px) {
  .tile-grid--four { grid-template-columns: 1fr; }
}

.tile__system { margin-bottom: 0.4rem; color: var(--text); font-size: 0.9rem; }

/* Case-study tiles carry more structure than the plain taxonomy tiles. */
.tag--stage {
  margin-left: 0.5rem;
  border-color: var(--accent-border);
  background: var(--accent-subtle);
  color: var(--accent);
}

.tile__scale {
  display: block;
  margin-top: 0.5rem;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 0.76rem;
}


/* --------------------------------------------------------------------------
   21. Case studies: figure carousel
   A scroll-snap track. Slides are uniform height with object-fit so the
   paper's wildly varying figure aspect ratios do not stagger the row.
   -------------------------------------------------------------------------- */

.carousel { margin-bottom: 3.5rem; }

.carousel__track {
  display: flex;
  gap: 1.25rem;
  margin: 0;
  padding: 0 0 1rem;
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
}

.carousel__track:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

.carousel__slide {
  flex: 0 0 min(540px, 82%);
  scroll-snap-align: start;
}

.carousel__slide figure { margin: 0; }

/* The diagrams are drawn on white; give them a light card in both themes
   rather than inverting them, since the colours carry meaning. */
.carousel__media {
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: #ffffff;
}

.carousel__media img {
  width: 100%;
  height: 240px;
  object-fit: contain;
}

.carousel__slide figcaption { padding-top: 0.9rem; }

.carousel__title {
  display: block;
  margin-bottom: 0.35rem;
  font-size: 1rem;
  font-weight: 640;
  color: var(--text);
}

.carousel__desc {
  display: block;
  color: var(--text-muted);
  font-size: 0.9rem;
  line-height: 1.55;
}

.carousel__controls {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
  margin-top: 0.5rem;
}

.carousel__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--bg-raised);
  color: var(--text-muted);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.carousel__btn:hover:not(:disabled) { color: var(--text); border-color: var(--text-faint); }
.carousel__btn:disabled { opacity: 0.4; cursor: default; }

@media (max-width: 560px) {
  .carousel__slide { flex-basis: 88%; }
  .carousel__media img { height: 180px; }
}
