/* ============================================================
   TANGOSARA V3 — warm editorial
   Palette: warm near-black, cream text, taupe-gold accent
   Fonts: Space Grotesk (display) / Manrope (body) / JetBrains Mono (labels)
============================================================ */

/* ---- VARIABLES ---- */
:root {
    --bg-0:        #111009;   /* page base — warm near-black */
    --bg-1:        #181610;   /* lifted panels */
    --bg-2:        #1f1c14;   /* hover surfaces */
    --text:        #e8e4de;   /* cream */
    --text-2:      #8a8578;   /* warm grey */
    --text-3:      #55503f;   /* muted */
    --accent:      #c4b5a0;   /* taupe-gold */
    --amber:       #e3b341;   /* SHIPPED only */
    --line:        #26241c;   /* hairlines */
    --line-2:      #3a3832;   /* stronger hairlines */
    --ease:        cubic-bezier(0.22, 1, 0.36, 1);
}

/* ---- RESET ---- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
    background: var(--bg-0);
    color: var(--text);
    font-family: 'Manrope', system-ui, sans-serif;
    font-weight: 400;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    overflow-x: hidden;
}
/* :where() keeps this reset at zero specificity — a bare `a:visited`
   selector (0,1,1) would beat classed colors like .nav-link (0,1,0),
   which is exactly the bug that made visited nav links stick white. */
:where(a, a:visited, a:active) { color: inherit; text-decoration: none; }
button {
    background: none;
    border: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
}
img, video { max-width: 100%; display: block; }
h1, h2, h3 { font-family: 'Space Grotesk', sans-serif; }

.mono {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    letter-spacing: 0.12em;
}
.dim { color: var(--text-3); }

::selection { background: var(--accent); color: var(--bg-0); }


/* ============================================================
   ATMOSPHERE — grain, cursor, chapter glow
============================================================ */

/* Film grain: SVG turbulence, fixed overlay */
.grain {
    position: fixed;
    inset: -50%;
    width: 200%;
    height: 200%;
    pointer-events: none;
    z-index: 9999;
    opacity: 0.05;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    animation: grain-shift 0.9s steps(4) infinite;
}
@keyframes grain-shift {
    0%   { transform: translate(0, 0); }
    25%  { transform: translate(-1.2%, 0.8%); }
    50%  { transform: translate(0.8%, -1.4%); }
    75%  { transform: translate(-0.6%, -0.8%); }
    100% { transform: translate(0, 0); }
}

/* Custom cursor — only on fine pointers; JS adds .cursor-on to body */
.cursor-dot, .cursor-ring {
    position: fixed;
    top: 0; left: 0;
    pointer-events: none;
    border-radius: 50%;
    z-index: 10000;
    opacity: 0;
}
body.cursor-on { cursor: none; }
body.cursor-on a,
body.cursor-on button { cursor: none; }
body.cursor-on .cursor-dot,
body.cursor-on .cursor-ring { opacity: 1; }
.cursor-dot {
    width: 7px; height: 7px;
    background: #fff;
    transform: translate(-50%, -50%);
    mix-blend-mode: difference;
}
.cursor-ring { mix-blend-mode: difference; }
.cursor-ring {
    width: 30px; height: 30px;
    border: 1.5px solid rgba(228, 220, 205, 0.9);
    transform: translate(-50%, -50%);
    transition: width 0.25s var(--ease), height 0.25s var(--ease),
                border-color 0.25s var(--ease), background 0.25s var(--ease);
}
body.cursor-hover .cursor-ring {
    width: 48px; height: 48px;
    border-color: #fff;
    background: rgba(228, 220, 205, 0.12);
}

/* Chapter glow — warm radial that breathes in on the active chapter */
.chapter { position: relative; }
.chapter::before {
    content: '';
    position: absolute;
    top: -10%; left: -20%;
    width: 70%;
    height: 80%;
    background: radial-gradient(ellipse at center,
        rgba(196, 181, 160, 0.055) 0%, transparent 65%);
    opacity: 0;
    transition: opacity 1.4s var(--ease);
    pointer-events: none;
}
.chapter.in-view::before { opacity: 1; }


/* ============================================================
   HERO ATMOSPHERE — light cones, beam-lit copy, embers
   ------------------------------------------------------------
   GEOMETRY CONTRACT: .hero-beams and .hero-lit MUST share an
   identical box (inset: -80px) so the cone gradient and the glow
   mask resolve to the same physical point. Changing one inset
   without the other silently decouples the glow from the light.
============================================================ */
.hero {
    /* one source of truth for the cones — reused as paint and as mask */
    --hero-ray: 227, 179, 65;
    --hero-cones: conic-gradient(from 0deg at 12% -10%,
        transparent 130deg,
        rgba(var(--hero-ray), 0.09) 134deg, rgba(var(--hero-ray), 0.28) 138deg, rgba(var(--hero-ray), 0.09) 142deg,
        transparent 146deg, transparent 150deg,
        rgba(var(--hero-ray), 0.11) 153deg, rgba(var(--hero-ray), 0.34) 158deg, rgba(var(--hero-ray), 0.11) 162deg,
        transparent 166deg, transparent 170deg,
        rgba(var(--hero-ray), 0.10) 173deg, rgba(var(--hero-ray), 0.30) 177deg, rgba(var(--hero-ray), 0.10) 181deg,
        transparent 185deg, transparent 190deg,
        rgba(var(--hero-ray), 0.07) 193deg, rgba(var(--hero-ray), 0.22) 197deg, rgba(var(--hero-ray), 0.07) 201deg,
        transparent 205deg);
    --hero-cones-mask: conic-gradient(from 0deg at 12% -10%,
        transparent 130deg,
        rgba(0,0,0,0.35) 134deg, #000 138deg, rgba(0,0,0,0.35) 142deg,
        transparent 146deg, transparent 150deg,
        rgba(0,0,0,0.40) 153deg, #000 158deg, rgba(0,0,0,0.40) 162deg,
        transparent 166deg, transparent 170deg,
        rgba(0,0,0,0.35) 173deg, #000 177deg, rgba(0,0,0,0.35) 181deg,
        transparent 185deg, transparent 190deg,
        rgba(0,0,0,0.25) 193deg, #000 197deg, rgba(0,0,0,0.25) 201deg,
        transparent 205deg);
}

/* ---- the four cones ---- */
.hero-beams {
    position: absolute;
    inset: -80px;                 /* see GEOMETRY CONTRACT */
    z-index: 1;
    pointer-events: none;
    mix-blend-mode: screen;
    transform-origin: 12% 0;
    animation: hero-sway 22s ease-in-out infinite alternate;
}
.hero-beams::before,
.hero-beams::after { content: ''; position: absolute; inset: 0; }
.hero-beams::before {
    background: var(--hero-cones);
    filter: blur(17px);
    -webkit-mask-image: linear-gradient(166deg, #000 2%, rgba(0,0,0,0.6) 46%, transparent 90%);
            mask-image: linear-gradient(166deg, #000 2%, rgba(0,0,0,0.6) 46%, transparent 90%);
}
.hero-beams::after {
    background: radial-gradient(26% 20% at 12% -8%, rgba(var(--hero-ray), 0.26), transparent 68%);
    filter: blur(24px);
}
@keyframes hero-sway     { 0% { transform: rotate(-1.1deg); } 100% { transform: rotate(1.1deg); } }
@keyframes hero-swayback { 0% { transform: rotate(1.1deg); }  100% { transform: rotate(-1.1deg); } }

/* ---- beam-lit clone of the hero copy ----
   .hero-lit carries the mask + sway; .hero-lit-rot carries the exact
   negation about the SAME origin on the SAME-size box, so the glyphs
   stay pinned to the base text while the mask sways over them.
   Its 80px padding pulls the content box back onto the true hero rect. */
.hero-lit {
    position: absolute;
    inset: -80px;                 /* see GEOMETRY CONTRACT */
    z-index: 3;
    pointer-events: none;
    opacity: 0;
    transition: opacity 1.4s var(--ease) 1.7s;
    -webkit-mask-image: var(--hero-cones-mask);
            mask-image: var(--hero-cones-mask);
    transform-origin: 12% 0;
    animation: hero-sway 22s ease-in-out infinite alternate;
}
.hero.in-view .hero-lit { opacity: 1; }
.hero-lit-rot {
    position: absolute;
    inset: 0;
    padding: 80px;
    display: flex;
    align-items: center;
    transform-origin: 12% 0;
    animation: hero-swayback 22s ease-in-out infinite alternate;
}
/* let the glow bleed past the line boxes */
.hero-lit .hero-name,
.hero-lit .hn-line,
.hero-lit .chapter-label { overflow: visible; }

.hero-lit .chapter-label { color: rgba(var(--hero-ray), 0.95); }
.hero-lit .hero-name {
    color: #fff6e2;
    text-shadow: 0 0 14px rgba(var(--hero-ray), 0.75),
                 0 0 42px rgba(var(--hero-ray), 0.45),
                 0 0 90px rgba(var(--hero-ray), 0.28);
}
.hero-lit .hero-rule i {
    background: linear-gradient(90deg, rgba(var(--hero-ray), 0.9), rgba(var(--hero-ray), 0));
    box-shadow: 0 0 12px rgba(var(--hero-ray), 0.55);
}
.hero-lit .hero-sub {
    color: #fff3e0;
    text-shadow: 0 0 16px rgba(var(--hero-ray), 0.5), 0 0 40px rgba(var(--hero-ray), 0.25);
}
.hero-lit .hero-meta,
.hero-lit .hero-now { color: rgba(var(--hero-ray), 0.9); text-shadow: 0 0 12px rgba(var(--hero-ray), 0.45); }
.hero-lit .cta {
    color: #ffeccb;
    border-bottom-color: rgba(var(--hero-ray), 0.9);
    text-shadow: 0 0 12px rgba(var(--hero-ray), 0.6);
}

/* ---- canvases ---- */
/* canvas is a replaced element: inset:0 alone will NOT stretch it,
   it would keep its intrinsic 300×150. Explicit size is required. */
.hero-embers {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 5;
    pointer-events: none;
}
.hero-fleet { width: 100%; height: 340px; display: block; }
.hero-fleet-cap {
    color: var(--text-3);
    margin-top: 0.6rem;
    text-align: right;
}
.hero-fleet-cap b { color: var(--accent); font-weight: 500; }


/* ============================================================
   REVEALS + HEADING MASKS
============================================================ */
.reveal {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 0.9s var(--ease) var(--rd, 0s),
                transform 0.9s var(--ease) var(--rd, 0s);
}
.reveal.visible {
    opacity: 1;
    transform: translateY(0);
}

/* Masked headings: rise in whenever their chapter enters view,
   sink away when it leaves (chapter .in-view toggles) */
.chapter-label, .hero-name, .work-title, .contact-title { overflow: hidden; }
.mask-in {
    display: inline-block;
    transform: translateY(118%);
    opacity: 0;
    transition: transform 1.3s var(--ease) 0.15s, opacity 0.95s ease 0.15s;
    will-change: transform;
}
.chapter.in-view .mask-in {
    transform: translateY(0);
    opacity: 1;
}

/* Ghost chapter numbers — outlined watermark per section */
.chapter { overflow: hidden; }
.chapter::after {
    content: attr(data-num);
    position: absolute;
    top: 3rem;
    right: 2rem;
    font-family: 'Space Grotesk', sans-serif;
    font-size: clamp(7rem, 17vw, 15rem);
    font-weight: 700;
    line-height: 1;
    color: transparent;
    -webkit-text-stroke: 1px rgba(196, 181, 160, 0.2);
    pointer-events: none;
    z-index: 0;
    opacity: 0;
    transform: translateY(24px);
    transition: opacity 1.2s var(--ease) 0.15s, transform 1.2s var(--ease) 0.15s;
}
.chapter.in-view::after {
    opacity: 1;
    transform: translateY(0);
}
/* hero's ghost number drops below the fixed style switch (top 4.3rem),
   which otherwise overlaps it on the landing view.
   Must stay after .chapter::after — same specificity, order decides. */
.hero::after { top: 8.5rem; }
.chapter-inner { position: relative; z-index: 1; }
.chapter-invert::after {
    -webkit-text-stroke: 1px rgba(28, 26, 23, 0.22);
}

/* Per-chapter atmospheres — base tone + texture + placed glow, all three
   distinct per chapter so sections read as different rooms */
#agentic {
    background-color: #131109;
    background-image: radial-gradient(rgba(196, 181, 160, 0.10) 1px, transparent 1.5px);
    background-size: 26px 26px;
}
#agentic::before {
    left: auto; right: -15%;
    background: radial-gradient(ellipse at center,
        rgba(196, 181, 160, 0.07) 0%, transparent 65%);
}
#mods {
    background-color: #16120a;
    background-image: radial-gradient(720px 480px at 88% 8%,
        rgba(227, 179, 65, 0.10), transparent 70%);
}
#mods::before {
    background: radial-gradient(ellipse at center,
        rgba(227, 179, 65, 0.05) 0%, transparent 65%);
}
#systems {
    background-color: #100f0c;
    background-image: repeating-linear-gradient(180deg,
        rgba(196, 181, 160, 0.06) 0px, rgba(196, 181, 160, 0.06) 1px,
        transparent 1px, transparent 64px);
}
#systems::before {
    top: auto; bottom: -10%;
}
#infra {
    background-color: #0d0c08;
    background-image:
        linear-gradient(rgba(196, 181, 160, 0.06) 1px, transparent 1px),
        linear-gradient(90deg, rgba(196, 181, 160, 0.06) 1px, transparent 1px);
    background-size: 64px 64px;
}

/* Reduced motion: everything visible and still */
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    .grain { animation: none; }
    .reveal { opacity: 1; transform: none; transition: none; }
    .chapter::before { transition: none; }
    .mask-in { transform: none; opacity: 1; transition: none; }
    .chapter::after { opacity: 1; transform: none; transition: none; }
    .cursor-dot, .cursor-ring { display: none; }
    body.cursor-on, body.cursor-on a, body.cursor-on button { cursor: auto; }

    /* hero atmosphere: keep the light, drop the motion.
       Embers are hidden outright — a frozen field of dots reads as
       noise rather than atmosphere. */
    .hero-beams, .hero-lit, .hero-lit-rot { animation: none; }
    .hero-lit { opacity: 1; transition: none; }
    .hero-rule i { transform: scaleX(1); transition: none; }
    .hero-embers { display: none; }

    /* step rail: no trace pulses, instant slot switches (fade kept short
       is fine, motion transforms are not).
       .sr-titlezone prefix beats the base rule's specificity — this media
       block sits EARLIER in the file than the step-rail section, so equal
       specificity would lose on source order. */
    .sr-titlezone .sr-traces path { animation: none; stroke-dasharray: none; opacity: 0.18; }
    .sr-shot { transition: opacity 0.15s ease; transform: translate(-50%,-50%); }
    .sr-text { transition: opacity 0.15s ease; transform: none; }
    .sr-bar i { transition: none; }
    /* numbers row: keep the static hairlines, drop the travelling pulse */
    .fnums .fnum-dv b { animation: none; opacity: 0; }

    /* infra floor: no pulses at all. The grid, the horizon, the pins and the
       payload chips are static CSS, so the resting frame loses nothing —
       same argument as the embers above. */
    .fpulse { display: none; }
    /* The spec-cell coupling STAYS: it is a state change, not motion, and it
       is how the chapter explains itself. Only the fade goes.
       #infra prefixes are load-bearing — this media block sits ~700 lines
       ABOVE where .fpin and .icell declare their transitions, so at equal
       specificity those later rules win on source order and this silently
       does nothing. Same trap as .sr-titlezone above. */
    #infra .fpin, #infra .icell, #infra .icell-rule { transition: none; }
}


/* ============================================================
   NAV
============================================================ */
.nav {
    position: fixed;
    top: 0; left: 0; right: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1.5rem 3rem;
    border-bottom: 1px solid transparent;
    transition: background 0.4s ease, border-color 0.4s ease, padding 0.4s ease;
}
.nav.scrolled {
    background: rgba(17, 16, 9, 0.85);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border-color: var(--line);
    padding: 1.1rem 3rem;
}
.nav-brand {
    color: var(--text);
    font-weight: 500;
    letter-spacing: 0.18em;
}
.nav-menu { display: flex; gap: 2.2rem; }
.nav-link {
    color: var(--text-2);
    transition: color 0.25s ease;
    position: relative;
}
.nav-link::after {
    content: '';
    position: absolute;
    left: 0; bottom: -4px;
    width: 100%;
    height: 1px;
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform 0.3s var(--ease);
}
.nav-link:hover { color: var(--text); }
.nav-link:hover::after { transform: scaleX(1); }
/* scrollspy: JS toggles .active on the link whose section is in view */
.nav-link.active { color: var(--text); }
.nav-link.active::after { transform: scaleX(1); }

/* mode switch — the physical door to the bold site.
   Fixed at identical coordinates on both pages so it never moves
   under the cursor across a warp. */
.mode-switch {
    position: fixed;
    top: 4.3rem; right: 3rem;
    z-index: 120;
    display: flex; align-items: center; gap: 0.7rem;
}
.ms-label { color: var(--text-3); font-size: 9px; }
.ms-track {
    position: relative;
    display: flex;
    align-items: center;
    border: 1px solid var(--line-2);
    border-radius: 999px;
    padding: 3px;
    background: var(--bg-1);
    transition: border-color 0.3s ease;
}
.mode-switch:hover .ms-track { border-color: var(--accent); }
.ms-opt {
    position: relative;
    z-index: 1;
    font-size: 9px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    padding: 0.3rem 0.7rem;
    color: var(--text-3);
    transition: color 0.35s ease;
}
.ms-opt-active { color: var(--bg-0); font-weight: 500; }
.ms-knob {
    position: absolute;
    top: 3px; bottom: 3px; left: 3px;
    width: calc(50% - 3px);
    background: var(--accent);
    border-radius: 999px;
    transition: left 0.35s var(--ease);
}
.ms-track.ms-right .ms-knob { left: 50%; }
/* hover: the knob leans toward the other side — "this moves" */
.mode-switch:hover .ms-track:not(.ms-right) .ms-knob { left: 9px; }
.mode-switch:hover .ms-track.ms-right .ms-knob { left: calc(50% - 6px); }


/* ============================================================
   CHAPTERS — shared shell
============================================================ */
.chapter {
    border-bottom: 1px solid var(--line);
}
.chapter-inner {
    max-width: 860px;
    margin: 0 auto;
    padding: 8rem 2.5rem;
}
.chapter-label {
    color: var(--accent);
    margin-bottom: 3.5rem;
}


/* ============================================================
   01 // IDENTITY
============================================================ */
.hero {
    min-height: 100vh;
    display: flex;
    align-items: center;
}
/* two columns: copy left, fleet constellation right.
   .hero-copy keeps the 860px measure the rest of the site reads at. */
.hero .chapter-inner {
    width: 100%;
    max-width: 1240px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 380px;
    gap: 3.5rem;
    align-items: center;
    padding-top: 10rem;
    padding-bottom: 6rem;
}
.hero-copy { max-width: 860px; }
.hero .chapter-label { margin-bottom: 2.5rem; }
/* font-family is stated here, not inherited from the `h1` element selector:
   the beam-lit clone renders this as a <div> so the page keeps one <h1>,
   and without this it silently falls back to Manrope and ghosts the base text. */
.hero-name {
    font-family: 'Space Grotesk', sans-serif;
    font-size: clamp(2.1rem, 6vw, 4.4rem);
    font-weight: 700;
    line-height: 1.08;
    letter-spacing: -0.025em;
    margin-bottom: 1.6rem;
}
.hn-line { display: block; overflow: hidden; }
/* divider between the two name lines — replaces the old trailing em-dash */
.hero-rule { display: block; margin: 0.34em 0 0.30em; line-height: 0; }
.hero-rule i {
    display: block;
    height: 2px;
    width: 72%;
    background: linear-gradient(90deg, rgba(196, 181, 160, 0.75), rgba(196, 181, 160, 0));
    transform: scaleX(0);
    transform-origin: left;
    transition: transform 1.1s var(--ease) 0.45s;
}
.chapter.in-view .hero-rule i { transform: scaleX(1); }
.hero-sub {
    color: var(--text-2);
    font-size: clamp(1rem, 1.8vw, 1.2rem);
    line-height: 1.55;
    max-width: 44ch;
    margin-bottom: 2.4rem;
}
.hero-meta {
    color: var(--text-2);
    margin-bottom: 1rem;
    /* slightly tighter than .mono (11px/.12em) so the languages list
       holds one line in the 724px copy column — "rust" was orphaning */
    font-size: 10.5px;
    letter-spacing: 0.09em;
}
.hero-now {
    color: var(--text-3);
    font-size: 10px;
    max-width: 70ch;
    line-height: 1.8;
    margin-bottom: 3.5rem;
}
.hero-ctas {
    display: flex;
    gap: 2.5rem;
    flex-wrap: wrap;
}
.cta {
    color: var(--accent);
    border-bottom: 1px solid var(--accent);
    padding-bottom: 4px;
    transition: opacity 0.25s ease;
}
.cta:hover { opacity: 0.65; }
.cta-dim {
    color: var(--text-2);
    border-bottom-color: var(--line-2);
}
.cta-dim:hover { color: var(--text); opacity: 1; border-bottom-color: var(--text-2); }


/* ============================================================
   TAGS
============================================================ */
.tag {
    font-size: 9px;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    padding: 0.2rem 0.6rem;
    border-radius: 2px;
    flex-shrink: 0;
}
.tag-shipped {
    color: var(--amber);
    border: 1px solid rgba(227, 179, 65, 0.35);
}
.tag-progress {
    color: var(--text-2);
    border: 1px solid var(--line-2);
}
.tag-live {
    color: var(--accent);
    border: 1px solid rgba(196, 181, 160, 0.4);
}


/* ============================================================
   02 // THE MODS — flagship works
============================================================ */
.work {
    padding: 3rem 0;
    border-top: 1px solid var(--line);
}
.work:last-child { padding-bottom: 0; }
.work-head {
    display: flex;
    align-items: center;
    gap: 1.2rem;
    margin-bottom: 1.2rem;
}
.work-title {
    font-size: clamp(1.5rem, 3vw, 2.1rem);
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1.1;
}
.work-desc {
    color: var(--text-2);
    font-size: 0.95rem;
    max-width: 62ch;
    margin-bottom: 1.4rem;
}
.work-caps {
    color: var(--text-3);
    font-size: 10px;
    margin-bottom: 1.8rem;
}
.work-links {
    display: flex;
    gap: 2rem;
    flex-wrap: wrap;
    align-items: baseline;
}
.work-link {
    color: var(--accent);
    border-bottom: 1px solid rgba(196, 181, 160, 0.4);
    padding-bottom: 3px;
    transition: border-color 0.25s ease, opacity 0.25s ease;
}
.work-link:hover { border-bottom-color: var(--accent); opacity: 0.8; }


/* ============================================================
   METRICS TABLES — flagship "by the numbers"
============================================================ */
.metrics {
    width: 100%;
    max-width: 620px;
    border-collapse: collapse;
    margin-bottom: 1.8rem;
}
.metrics td {
    padding: 0.6rem 0.2rem;
    border-top: 1px solid var(--line);
    vertical-align: baseline;
}
.metrics tr:last-child td { border-bottom: 1px solid var(--line); }
.metrics-key {
    color: var(--accent);
    font-size: 10px;
    width: 15ch;
    padding-right: 1.5rem;
    white-space: nowrap;
}
.metrics-val {
    color: var(--text-2);
    font-size: 0.85rem;
}

/* Stat chips on list rows */
.stat {
    color: var(--text-3);
    font-size: 9px;
    border-left: 1px solid var(--line-2);
    padding-left: 1.2rem;
    flex-shrink: 0;
}

/* Fleet stat strip (ch02) — no `transition` here: .reveal owns it */
.stat-strip {
    display: flex;
    flex-wrap: wrap;
    gap: 2.5rem 3.5rem;
    margin-bottom: 3.5rem;
    padding-bottom: 2.5rem;
    border-bottom: 1px solid var(--line);
}
.stat-cell {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}
.stat-num {
    font-family: 'Space Grotesk', sans-serif;
    font-size: clamp(2rem, 3.5vw, 2.75rem);
    font-weight: 600;
    line-height: 1.05;
    color: var(--amber);
}
.stat-label {
    font-size: 10px;
    letter-spacing: 0.08em;
    color: var(--text-2);
}


/* ============================================================
   FLEET GRAPH — interactive knowledge-graph canvas
============================================================ */
.graph-wrap { margin-top: 2rem; }
.graph-canvas {
    width: 100%;
    height: 380px;
    display: block;
    border: 1px solid var(--line);
    border-radius: 3px;
    background: radial-gradient(ellipse at 50% 40%,
        rgba(196, 181, 160, 0.04) 0%, transparent 70%), var(--bg-1);
    cursor: crosshair;
    touch-action: none;
}
body.cursor-on .graph-canvas { cursor: none; }
.graph-hint {
    margin-top: 0.8rem;
    color: var(--text-3);
    font-size: 9px;
}


/* ============================================================
   EXPANDABLE MEDIA — shared by works and rows
============================================================ */
.work-media {
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    transition: max-height 0.6s var(--ease), opacity 0.5s ease, margin 0.6s var(--ease);
}
.work-media.open {
    max-height: 620px;
    opacity: 1;
    margin-top: 2rem;
}
.media-pair {
    display: flex;
    gap: 1rem;
}
.media-pair img {
    width: calc(50% - 0.5rem);
    border: 1px solid var(--line-2);
    border-radius: 3px;
    object-fit: cover;
}
.media-video {
    width: 100%;
    border: 1px solid var(--line-2);
    border-radius: 3px;
}
.media-shot {
    max-height: 380px;
    width: auto;
    max-width: 100%;
    border: 1px solid var(--line-2);
    border-radius: 3px;
    margin: 0 auto;
}
.media-shot-tall { max-height: 420px; }
.media-watch {
    width: min(260px, 70%);
    aspect-ratio: 1;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid var(--accent);
    box-shadow: 0 0 32px rgba(196, 181, 160, 0.12);
    margin: 0 auto;
}


/* ============================================================
   03 / 04 — LIST ROWS
============================================================ */
.row {
    padding: 2.2rem 0;
    border-top: 1px solid var(--line);
    /* merged with .reveal's transition — a later bare `transition:` here
       would silently kill the reveal animation on rows */
    transition: background 0.3s ease,
                opacity 0.9s var(--ease) var(--rd, 0s),
                transform 0.9s var(--ease) var(--rd, 0s);
}
.row-line {
    display: flex;
    align-items: center;
    gap: 1.2rem;
    margin-bottom: 0.7rem;
}
.row-title {
    font-size: 1.15rem;
    font-weight: 500;
    letter-spacing: -0.01em;
}
.row-toggle {
    margin-left: auto;
    color: var(--text-3);
    transition: color 0.25s ease;
}
.row:hover .row-toggle { color: var(--accent); }
.row-desc {
    color: var(--text-2);
    font-size: 0.9rem;
    max-width: 62ch;
}


/* ============================================================
   06 // INFRASTRUCTURE
============================================================ */
.infra-lead {
    font-family: 'Space Grotesk', sans-serif;
    font-size: clamp(1.15rem, 2.2vw, 1.5rem);
    font-weight: 500;
    line-height: 1.45;
    letter-spacing: -0.01em;
    max-width: 44ch;
    margin-bottom: 3rem;
}
/* ---- the perspective floor ----
   Replaces the ASCII topology <pre>. The lab as a PLACE: a grid receding to a
   lit horizon with the machines standing on it as pins. No edges between pins —
   see the note in index.html.

   Everything is positioned in PERCENTAGES of .floor-stage, which has a definite
   height, so an absolutely-positioned child's percentage height actually
   resolves. (A percentage height against an auto-height parent silently
   collapses — the bug that clipped popup layout B.) That also lets the pulse
   keyframes address pin heads by the same numbers the markup uses. */
.floor {
    position: relative;
    /* 560, not 520: the services hub carries four chips and its stack ran 8px
       out of the top of the panel — measured, not guessed. The chip stack is a
       fixed pixel height while the pins are placed in %, so headroom has to be
       re-won at every breakpoint below. */
    height: 560px;
    /* safety net only — nothing should reach the edge, and a spill assertion
       guards that. This keeps a future content change from colliding with the
       stat above instead of just being clipped. */
    overflow: hidden;
    margin: 2.2rem 0 3rem;
    /* full-bleed out of the 860px text column, capped at 1240 like ch02–ch04 */
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    padding-left: max(1.4rem, calc(50vw - 620px));
    padding-right: max(1.4rem, calc(50vw - 620px));
}
.floor::before {
    content: '';
    position: absolute;
    inset: 0 max(1.4rem, calc(50vw - 620px));
    background: #0a0906;
    border: 1px solid var(--line);
    border-radius: 13px;
}
/* one clipping context for the plane, the horizon and every pin */
.floor-plane, .floor-horizon, .floor-stage {
    position: absolute;
    left: max(1.4rem, calc(50vw - 620px));
    right: max(1.4rem, calc(50vw - 620px));
}

/* The ground. A real 3D plane rather than faked gradients: rotateX about the
   top edge lays it down from the horizon toward the viewer, so both axes get
   correct perspective spacing for free. */
.floor-plane {
    top: 42%; bottom: 0;
    overflow: hidden;
    border-radius: 0 0 12px 12px;
    perspective: 420px;
    perspective-origin: 50% 0%;
}
/* 72deg not 76, and a longer plane: at 76 the near rows stretched so far apart
   that only two horizontal lines were left on screen and the floor stopped
   reading as a receding surface. */
.floor-plane i {
    position: absolute;
    left: -25%; right: -25%; top: 0;
    height: 1300px;
    transform: rotateX(72deg);
    transform-origin: 50% 0%;
    background-image:
        linear-gradient(rgba(196, 181, 160, 0.24) 1px, transparent 1px),
        linear-gradient(90deg, rgba(196, 181, 160, 0.24) 1px, transparent 1px);
    background-size: 48px 48px;
    /* dissolve the far field into the horizon glow, and the near edge into the
       panel, so the grid never ends on a hard line. The near fade starts early
       (44%) because the front rows are the widest-spaced and were loud enough
       to compete with the spec rows below the panel. */
    -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 14%, #000 44%, transparent 78%);
            mask-image: linear-gradient(180deg, transparent 0%, #000 14%, #000 44%, transparent 78%);
}

.floor-horizon {
    bottom: 58%; height: 1px;
    background: linear-gradient(90deg, transparent 4%,
        rgba(227, 179, 65, 0.42) 24%, rgba(227, 179, 65, 0.72) 50%,
        rgba(227, 179, 65, 0.42) 76%, transparent 96%);
}
.floor-horizon::after {
    content: '';
    position: absolute;
    left: 8%; right: 8%; bottom: -90px; height: 200px;
    background: radial-gradient(ellipse 52% 60% at 50% 50%,
        rgba(227, 179, 65, 0.16), transparent 72%);
    pointer-events: none;
}

.floor-stage { top: 0; bottom: 0; }

/* ---- the pins ----
   A pin is a zero-width column standing at (--x, --y) with a stem --h tall.
   Its head therefore sits at (--x, --y + --h) — the coordinates the pulses
   fly between. */
.fpin {
    position: absolute;
    left: var(--x); bottom: var(--y);
    width: 0; height: var(--h);
    transition: opacity 0.3s var(--ease);   /* for the spec-cell coupling */
}
.fpin-pool, .fpin-stem, .fpin-head, .fpin-label, .fpin-chips {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
}
.fpin-pool {
    bottom: 0;
    width: 78px; height: 17px;
    border-radius: 50%;
    background: radial-gradient(ellipse at center,
        rgba(227, 179, 65, 0.30), transparent 70%);
}
.fpin-stem {
    bottom: 0;
    width: 1px; height: 100%;
    background: linear-gradient(to top,
        rgba(227, 179, 65, 0.06), rgba(227, 179, 65, 0.62));
}
/* -3.5 on a 7px head puts the head's CENTRE exactly on the stem top, which is
   the point the pulse keyframes fly to. -3 left it half a pixel high and every
   pulse endpoint measured 1px off. */
.fpin-head {
    top: -3.5px;
    width: 7px; height: 7px;
    border-radius: 50%;
    background: #e3b341;
    box-shadow: 0 0 10px 2px rgba(227, 179, 65, 0.55);
}
.fpin-label {
    bottom: calc(100% + 12px);
    white-space: nowrap;
    text-align: center;
    line-height: 1.25;
}
.fpin-label span {
    display: block;
    font-family: 'Space Grotesk', sans-serif;
    font-size: 0.95rem;
    font-weight: 500;
    color: var(--text);
}
.fpin-label em {
    display: block;
    font-style: normal;
    font-size: 8.5px;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--amber);
}

/* Payload chips are shown AT REST, not only on hover — a visitor who never
   hovers still learns what runs where. Hover only lifts them. */
.fpin-chips {
    bottom: calc(100% + 52px);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.fpin-chips b {
    display: block;
    font-weight: 400;
    font-size: 8.5px;
    line-height: 1.2;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--text-2);
    background: rgba(227, 179, 65, 0.05);
    border: 1px solid rgba(196, 181, 160, 0.14);
    border-radius: 3px;
    padding: 1.5px 7px;
    transition: color 0.3s var(--ease), border-color 0.3s var(--ease),
                background 0.3s var(--ease);
}
.fsvr:hover .fpin-chips b {
    color: var(--text);
    background: rgba(227, 179, 65, 0.10);
    border-color: rgba(227, 179, 65, 0.34);
}

/* Devices read as lesser without needing a legend: smaller, taupe not amber. */
.fdev .fpin-pool {
    width: 46px; height: 11px;
    background: radial-gradient(ellipse at center,
        rgba(196, 181, 160, 0.20), transparent 70%);
}
.fdev .fpin-stem {
    background: linear-gradient(to top,
        rgba(196, 181, 160, 0.05), rgba(196, 181, 160, 0.42));
}
.fdev .fpin-head {
    width: 5px; height: 5px;
    top: -2.5px;
    background: var(--accent);
    box-shadow: 0 0 7px 1px rgba(196, 181, 160, 0.38);
}
.fdev .fpin-label { bottom: calc(100% + 8px); }
.fdev .fpin-label span {
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--text-2);
}

/* ---- the monitoring pulse ----
   Three legs per 12.6s cycle (4.2s each), so each light picks a different
   server origin and a different device destination as it goes. The point is
   "the fleet reports to whatever I'm holding" — NOT "everything goes to the
   watch", which is why no pulse repeats a destination within its cycle.
   Origins and destinations come in as custom properties per instance;
   keyframes resolve them, so one keyframe block drives all three and the
   mobile override only has to restate the destinations. */
.fpulse {
    position: absolute;
    width: 5px; height: 5px;
    margin: 0 0 -2.5px -2.5px;
    border-radius: 50%;
    background: #fff4d8;
    box-shadow: 0 0 9px 3px rgba(227, 179, 65, 0.6);
    opacity: 0;
    animation: fpulse 12.6s linear infinite;
}
@keyframes fpulse {
    0%   { left: var(--o1x); bottom: var(--o1y); opacity: 0;
           animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
    3%   { opacity: 1; }
    27%  { opacity: 1; }
    31%  { left: var(--d1x); bottom: var(--d1y); opacity: 0; }
    33%  { left: var(--o2x); bottom: var(--o2y); opacity: 0;
           animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
    36%  { opacity: 1; }
    60%  { opacity: 1; }
    64%  { left: var(--d2x); bottom: var(--d2y); opacity: 0; }
    66%  { left: var(--o3x); bottom: var(--o3y); opacity: 0;
           animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
    69%  { opacity: 1; }
    93%  { opacity: 1; }
    97%  { left: var(--d3x); bottom: var(--d3y); opacity: 0; }
    100% { left: var(--o1x); bottom: var(--o1y); opacity: 0; }
}
/* ---- the six spec cells ----
   These carry every claim ch06 makes — the floor above is decorative. They are
   full-bleed to the SAME 1240 cap as the floor: locked to the 860 text column
   the chapter visibly narrowed halfway down for no reason.

   A 3x2 hairline grid, deliberately the same construction as ch03's four
   points, so the site repeats a motif instead of inventing a third list. */
.infra-cells {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1px;
    background: rgba(196, 181, 160, 0.11);
    border: 1px solid rgba(196, 181, 160, 0.11);
    border-radius: 11px;
    overflow: hidden;
    /* Out to the viewport edge, then back in by exactly the floor's own inset,
       so this grid's left and right edges land on the floor panel's. Solving
       it in one margin beats width + auto margins, which fight each other. */
    margin-left: calc(50% - 50vw + max(1.4rem, 50vw - 620px));
    margin-right: calc(50% - 50vw + max(1.4rem, 50vw - 620px));
}
.icell {
    background: #131109;
    padding: 1.3rem 1.25rem 1.45rem;
    transition: background 0.3s var(--ease), opacity 0.3s var(--ease);
}
.icell:hover { background: #1a1610; }
.icell-n {
    display: block;
    font-size: 10.5px; letter-spacing: 0.14em;
    color: var(--text-3);
}
.icell-k {
    display: block;
    font-size: 12.5px; letter-spacing: 0.06em;
    color: var(--amber);
    margin: 0.5rem 0 0;
}
.icell-rule {
    display: block; height: 1px;
    margin: 0.7rem 0 0.8rem;
    background: rgba(196, 181, 160, 0.16);
    transition: background 0.3s var(--ease);
}
.icell:hover .icell-rule { background: rgba(227, 179, 65, 0.5); }
.icell-v {
    color: var(--text-2);
    font-size: 0.82rem; line-height: 1.62;
    margin: 0;
}

/* ---- the coupling ----
   Hover a cell and the pins it names light while the rest fall back; hover a
   pin and the cells naming it stay up while the rest fall back.

   PURE CSS. ch06 is the only chapter on this site with no JavaScript and this
   keeps it that way — :has() on the shared .chapter-inner reaches from one
   subtree to the other, so there is nothing to initialise and nothing to gate
   on an IntersectionObserver.

   The pin list per cell lives in index.html's data-pins. These selectors and
   that markup are one mapping in two places; the battery asserts both
   directions against the mapping table in the spec, never against this CSS,
   so a rule that drifts from the markup fails rather than agreeing with
   itself. */
#infra .chapter-inner:has(.icell:hover) .fpin { opacity: 0.26; }
#infra .chapter-inner:has(.icell[data-pins~="hyp"]:hover)    .fpin[data-pin="hyp"],
#infra .chapter-inner:has(.icell[data-pins~="hub"]:hover)    .fpin[data-pin="hub"],
#infra .chapter-inner:has(.icell[data-pins~="iron"]:hover)   .fpin[data-pin="iron"],
#infra .chapter-inner:has(.icell[data-pins~="pc"]:hover)     .fpin[data-pin="pc"],
#infra .chapter-inner:has(.icell[data-pins~="laptop"]:hover) .fpin[data-pin="laptop"],
#infra .chapter-inner:has(.icell[data-pins~="phone"]:hover)  .fpin[data-pin="phone"],
#infra .chapter-inner:has(.icell[data-pins~="watch"]:hover)  .fpin[data-pin="watch"] {
    opacity: 1;
}
#infra .chapter-inner:has(.fpin[data-pin="hyp"]:hover)    .icell:not([data-pins~="hyp"]),
#infra .chapter-inner:has(.fpin[data-pin="hub"]:hover)    .icell:not([data-pins~="hub"]),
#infra .chapter-inner:has(.fpin[data-pin="iron"]:hover)   .icell:not([data-pins~="iron"]),
#infra .chapter-inner:has(.fpin[data-pin="pc"]:hover)     .icell:not([data-pins~="pc"]),
#infra .chapter-inner:has(.fpin[data-pin="laptop"]:hover) .icell:not([data-pins~="laptop"]),
#infra .chapter-inner:has(.fpin[data-pin="phone"]:hover)  .icell:not([data-pins~="phone"]),
#infra .chapter-inner:has(.fpin[data-pin="watch"]:hover)  .icell:not([data-pins~="watch"]) {
    opacity: 0.3;
}


/* ============================================================
   07 // CONTACT
============================================================ */
.contact-title {
    font-size: clamp(1.8rem, 4vw, 2.8rem);
    font-weight: 700;
    letter-spacing: -0.02em;
    margin-bottom: 3rem;
}
.contact-list {
    display: flex;
    flex-direction: column;
}
.contact-row {
    display: flex;
    align-items: baseline;
    gap: 2rem;
    width: 100%;
    padding: 1.1rem 0.2rem;
    border-top: 1px solid var(--line);
    text-align: left;
    transition: background 0.25s ease, padding 0.25s var(--ease);
}
.contact-row:last-child { border-bottom: 1px solid var(--line); }
.contact-row:hover { background: var(--bg-1); padding-left: 0.8rem; }
.contact-key {
    color: var(--text-3);
    width: 10ch;
    flex-shrink: 0;
}
.contact-val { color: var(--text); }
.contact-hint {
    margin-left: auto;
    color: var(--text-3);
    font-size: 9px;
    opacity: 0;
    transition: opacity 0.25s ease;
}
.contact-row:hover .contact-hint { opacity: 1; }
.contact-row.copied .contact-hint { opacity: 1; color: var(--amber); }
.contact-row-static:hover { background: none; padding-left: 0.2rem; }

.footer {
    margin-top: 6rem;
    color: var(--text-3);
}

/* ---- THE INVERT: contact flips to cream paper ---- */
.chapter-invert {
    background: var(--text);          /* cream #e8e4de */
    border-bottom: none;
}
.chapter-invert .chapter-label { color: #6f6046; }        /* espresso-taupe */
.chapter-invert .dim { color: rgba(28, 26, 23, 0.35); }
.chapter-invert .contact-title { color: #1c1a17; }
.chapter-invert .contact-row {
    border-color: rgba(28, 26, 23, 0.18);
    color: #1c1a17;
}
.chapter-invert .contact-row:hover { background: rgba(28, 26, 23, 0.05); }
.chapter-invert .contact-row-static:hover { background: none; }
.chapter-invert .contact-key { color: rgba(28, 26, 23, 0.45); }
.chapter-invert .contact-val { color: #1c1a17; }
.chapter-invert .contact-val.dim { color: rgba(28, 26, 23, 0.4); }
.chapter-invert .contact-hint { color: rgba(28, 26, 23, 0.45); }
.chapter-invert .contact-row.copied .contact-hint { color: #8a6d1f; }
.chapter-invert .footer { color: rgba(28, 26, 23, 0.4); }
.chapter-invert::before {
    background: radial-gradient(ellipse at center,
        rgba(28, 26, 23, 0.045) 0%, transparent 65%);
}
.chapter-invert ::selection { background: #1c1a17; color: var(--text); }


/* ============================================================
   RESPONSIVE
============================================================ */
/* ============================================================
   04 // SYSTEMS + 05 // EXPERIMENTS — TWO-LANE CARD RAIL
   ------------------------------------------------------------

   ch05 stopped being its own page: the top lane is chapter 04, the BOTTOM
   LANE IS CHAPTER 05. That is why infra is still 06 — nothing renumbered.

   Layer order inside a card (back to front):
     traces (0) -> scrim (1) -> ghost icon (2) -> copy (3) -> rim glow (4)
   Trace visibility is layer-opacity x stroke-alpha — the two MULTIPLY.
   Keep traces < icon < copy or the card stops being readable.
============================================================ */
/* full-bleed out of the 860px text column — a rail needs to run edge to
   edge, and cards should bleed off both sides. */
.xrail-wrap {
    position: relative;
    margin-top: 2.6rem;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
}

.xlane { position: relative; }
.xlane + .xlane { margin-top: 0.4rem; }

/* the hairline each lane hangs from: SOLID for systems, DASHED for
   experiments, so the two zones stay distinguishable once the labels have
   scrolled out of shot. */
.xlane-line {
    position: absolute; left: 0; right: 0; top: 2.1rem; height: 1px; z-index: 1;
    background: rgba(196, 181, 160, 0.15);
    pointer-events: none;
}
.xlane-exp .xlane-line {
    height: 0; background: none;
    border-top: 1px dashed rgba(196, 181, 160, 0.20);
}

.xlane-scroll {
    position: relative; z-index: 2;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-behavior: auto;           /* scrollLeft writes must land instantly */
    scrollbar-width: none;
    padding: 2.4rem 0 1.1rem;
    cursor: grab;
}
.xlane-scroll::-webkit-scrollbar { display: none; }
.xlane-scroll.dragging { cursor: grabbing; }
.xlane-scroll:focus-visible { outline: 1px solid var(--accent); outline-offset: 4px; }

/* NO horizontal padding on the track: the recycler's arithmetic is pure
   cardWidth + gap, and end padding would fold into it and jolt every wrap.
   Breathing room lives on .xrail-wrap instead. */
.xlane-track {
    display: flex;
    gap: 1rem;
    width: max-content;
    align-items: flex-start;
}

/* ---- label layer: siblings of the scrollers, so cards drift beneath ---- */
.xlane-chip, .xlane-num {
    position: absolute; z-index: 6; pointer-events: none;
    top: 0;                          /* real value measured by app.js */
}
/* Both lanes wear the same amber: they are two zones of ONE chapter, and a
   taupe pill above an amber one read as two different systems. */
.xlane-chip {
    left: max(1.4rem, calc(50vw - 390px));
    font-size: 9px; letter-spacing: 0.2em;
    color: var(--amber);
    background: #141208;
    border: 1px solid rgba(227, 179, 65, 0.4);
    border-radius: 99px;
    padding: 4px 12px;
    white-space: nowrap;
}
.xlane-chip .dim { color: rgba(227, 179, 65, 0.5); }

.xlane-num {
    right: max(1.4rem, calc(50vw - 390px));
    font-family: 'Space Grotesk', sans-serif; font-weight: 700;
    font-size: 30px; line-height: 1; letter-spacing: -0.5px;
    color: transparent;
    -webkit-text-stroke: 1px rgba(227, 179, 65, 0.36);
}

/* TEST (Owen, 2026-07-27): a full-size ghost 05 sitting in the gap between
   the two lanes. Deliberately matches .chapter::after's clamp() exactly, so
   the question being answered is purely "is it readable down there", not
   "is it the right size". Centred on the seam via translateY(-50%). */
.xlane-ghost {
    position: absolute;
    /* aligned with .chapter::after above it — which does put
       its right edge back under the 4rem edge fade. Accepted: this numeral is
       background texture by design, with the small pill numeral beside it
       doing the actual naming. */
    right: 2rem;
    z-index: 0;                 /* behind the cards, like the chapter numeral */
    transform: translateY(-50%);
    font-family: 'Space Grotesk', sans-serif;
    font-size: clamp(7rem, 17vw, 15rem);
    font-weight: 700;
    line-height: 1;
    color: transparent;
    -webkit-text-stroke: 1px rgba(196, 181, 160, 0.2);
    pointer-events: none;
}

/* edge fades hint that the lanes continue */
.xrail-wrap::before,
.xrail-wrap::after {
    content: '';
    position: absolute;
    top: 0; bottom: 0;
    width: 4rem;
    z-index: 5;
    pointer-events: none;
}
.xrail-wrap::before { left: 0;  background: linear-gradient(90deg,  #141208, transparent); }
.xrail-wrap::after  { right: 0; background: linear-gradient(270deg, #141208, transparent); }

/* ---- the card ---- */
.xcard {
    --c: 240, 197, 116;                 /* family tint, set per data-fam */
    position: relative;
    flex: 0 0 auto;
    width: 540px;
    min-height: 340px;
    padding: 1.4rem 1.6rem 1.3rem;
    background: linear-gradient(145deg, #1a1812 0%, #161410 55%, #14120c 100%);
    border: 1px solid var(--line);
    border-radius: 16px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    transition: transform 0.35s var(--ease), border-color 0.35s var(--ease),
                box-shadow 0.35s var(--ease);
}
.xcard[data-fam="lilac"] { --c: 207, 184, 247; }
.xcard[data-fam="ice"]   { --c: 168, 208, 240; }
.xcard[data-fam="amber"] { --c: 240, 197, 116; }
.xcard:hover {
    transform: translateY(-5px);
    border-color: rgba(var(--c), 0.55);
    box-shadow: 0 18px 50px rgba(0,0,0,0.5), 0 0 34px rgba(var(--c), 0.14);
}

/* injected decoration */
.xcard .xtraces,
.xcard .xtlit { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.xcard .xtraces { z-index: 0; opacity: 0.55; transition: opacity 0.35s; }
.xcard:hover .xtraces { opacity: 0.8; }
.xcard .xtraces path { fill: none; stroke: rgba(196,181,160,0.20); stroke-width: 1; }
.xcard .xtraces rect, .xcard .xtraces circle { fill: rgba(196,181,160,0.18); stroke: none; }
/* bright copy of the circuitry, revealed only inside the cursor's light */
.xcard .xtlit {
    z-index: 2; opacity: 0; transition: opacity 0.25s;
    filter: drop-shadow(0 0 5px rgba(var(--c), 0.55));
    -webkit-mask-image: radial-gradient(250px circle at var(--mx, 50%) var(--my, 50%), #000, transparent 72%);
            mask-image: radial-gradient(250px circle at var(--mx, 50%) var(--my, 50%), #000, transparent 72%);
}
.xcard .xtlit path { fill: none; stroke: rgba(var(--c), 0.75); stroke-width: 1.2; }
.xcard .xtlit rect, .xcard .xtlit circle { fill: rgba(var(--c), 0.8); stroke: none; }
.xcard:hover .xtlit { opacity: 1; }

/* readability scrim — strongest under the copy, gone by the right third */
.xcard::before {
    content: '';
    position: absolute; inset: 0; z-index: 1; pointer-events: none;
    background: linear-gradient(100deg,
        rgba(20,18,12,0.62) 0%, rgba(20,18,12,0.40) 40%,
        rgba(20,18,12,0.10) 64%, rgba(20,18,12,0) 80%);
}
.xcard .xicon {
    position: absolute; right: -30px; bottom: -36px;
    width: 240px; height: 240px; z-index: 2; pointer-events: none;
    opacity: 0.24;
    transition: opacity 0.4s var(--ease), transform 0.4s var(--ease);
}
.xcard .xicon * {
    fill: none; stroke: #d8d0bf; stroke-width: 1.6;
    stroke-linecap: round; stroke-linejoin: round;
}
.xcard:hover .xicon { opacity: 0.44; transform: scale(1.04) rotate(-2deg); }
.xcard:hover .xicon * { stroke: rgb(var(--c)); }

/* cursor-following light + rim */
.xcard .xglow {
    position: absolute; inset: 0; z-index: 1; pointer-events: none;
    opacity: 0; transition: opacity 0.3s var(--ease);
    background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 50%),
        rgba(var(--c), 0.14), rgba(var(--c), 0.05) 40%, transparent 68%);
}
.xcard .xrim {
    position: absolute; inset: 0; z-index: 4; pointer-events: none;
    opacity: 0; transition: opacity 0.3s var(--ease);
    border-radius: 16px; padding: 1px;
    background: radial-gradient(300px circle at var(--mx, 50%) var(--my, 50%),
        rgba(var(--c), 0.8), rgba(var(--c), 0.12) 55%, transparent 75%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
}
.xcard:hover .xglow, .xcard:hover .xrim { opacity: 1; }

/* copy */
.xcard-badge {
    position: absolute; top: 1.2rem; right: 1.4rem; z-index: 3;
    font-size: 9.5px; letter-spacing: 0.15em; color: var(--accent);
    border: 1px solid rgba(196,181,160,0.28); border-radius: 20px;
    padding: 0.36rem 0.8rem; background: rgba(17,16,9,0.55);
    backdrop-filter: blur(3px);
}
.xcard-badge .st { color: rgb(var(--c)); }
.xcard-title {
    position: relative; z-index: 3;
    font-size: 1.7rem; font-weight: 600; letter-spacing: -0.015em;
    margin: 2.2rem 0 0.5rem;
}
/* NB: no `flex: 1` here. A stretched flex item is taller than the clamp,
   and -webkit-line-clamp then draws its ellipsis at line 6 while lines 7+
   still paint below it. The button carries `margin-top: auto` instead. */
.xcard-desc {
    position: relative; z-index: 3;
    color: var(--text-2); font-size: 0.9rem; line-height: 1.6;
    max-width: 42ch;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 6;
    overflow: hidden;
}
/* Cards no longer expand in place. These two blocks stay in the markup as the
   FALLBACK content the popup reads for any card without a data file yet, so
   they are hidden unconditionally rather than toggled. */
.xcard-media,
.xcard-extra { display: none; }
.xcard-extra .work-caps { margin-bottom: 0.9rem; }
.xcard-extra .metrics { width: 100%; }
.xcard-extra .metrics td { padding: 0.5rem 0; font-size: 0.78rem; }
.xcard-extra .metrics-key { width: 34%; vertical-align: top; }
.xcard-media video { width: 100%; border-radius: 8px; border: 1px solid var(--line); }
.xcard-media img { border-radius: 8px; border: 1px solid var(--line); }
.xcard-media .media-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
.xcard-more {
    position: relative; z-index: 3; align-self: flex-start; margin-top: auto;
    display: inline-flex; align-items: center; gap: 0.5rem;
    font-size: 10.5px; letter-spacing: 0.14em; color: var(--text);
    border: 1px solid #35301f; border-radius: 24px;
    padding: 0.58rem 1.15rem; background: rgba(24,22,16,0.85);
    transition: all 0.25s var(--ease);
}
.xcard-more .arr { color: rgb(var(--c)); transition: transform 0.25s var(--ease); }
.xcard-more:hover { border-color: rgba(var(--c), 0.55); background: #241f14; }
.xcard-more:hover .arr { transform: translateX(3px); }

/* mote canvas sits above the cards, below the grain */
.xmotes {
    position: absolute; inset: 0; width: 100%; height: 100%;
    z-index: 6; pointer-events: none;
}

/* ============================================================
   02 // AGENTIC — STEP RAIL
   ------------------------------------------------------------
   1:1 with the locked pattern: the title zone and rail are FULL-BLEED
   out of the 860px chapter column (the pattern owns the viewport,
   the page does not squeeze the pattern), panel track is 1180px,
   panel is 400px tall, patches/traces/timings use the locked
   coordinates verbatim. Click-only: works are read, not watched.
============================================================ */
/* ch02 runs tighter than the default chapter rhythm: the step rail is a
   tall composition on its own, so the usual 8rem of chapter padding left
   ~196px of dead air above the label and ~115px below the deck. */
#agentic .chapter-inner { padding-top: 2.5rem; padding-bottom: 4.5rem; }
/* ghost 02 pushed down so it sits across the title and numbers rather than
   floating above them */
#agentic::after { top: 5rem; }

.sr-titlezone {
    position: relative;
    text-align: center;
    padding: 1.4rem 0 0;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    overflow: hidden;             /* clips px-coord traces at the viewport */
}
.sr-titlezone .chapter-label,
.sr-titlezone .sr-display,
.sr-titlezone .sr-sub { position: relative; z-index: 2; }
.sr-titlezone .chapter-label { margin-bottom: 1.4rem; }
.sr-display {
    font-family: 'Space Grotesk', sans-serif; font-weight: 700;
    font-size: clamp(1.9rem, 4vw, 3.2rem); line-height: 1.1;
    letter-spacing: -0.02em;
}
.sr-display .hl { color: var(--amber); text-shadow: 0 0 26px rgba(227,179,65,0.45); }
.sr-sub { color: var(--text-2); font-size: 1rem; margin-top: 0.6rem; }
.sr-subline { display: inline-block; position: relative; }
.sr-subline::after {
    content: ''; position: absolute; left: 50%; transform: translateX(-50%);
    bottom: -10px; width: 120px; height: 2px;
    background: linear-gradient(90deg, transparent, rgba(227,179,65,0.9), transparent);
    box-shadow: 0 0 10px rgba(227,179,65,0.6);
}
.sr-gpatch {
    position: absolute; pointer-events: none; opacity: 0.5; z-index: 0;
    background-image:
        linear-gradient(rgba(196,181,160,0.09) 1px, transparent 1px),
        linear-gradient(90deg, rgba(196,181,160,0.09) 1px, transparent 1px);
    background-size: 44px 44px;
    -webkit-mask-image: radial-gradient(closest-side, #000, transparent);
            mask-image: radial-gradient(closest-side, #000, transparent);
}
/* locked coordinates — % of the full viewport, not the text column */
.sr-gp1 { left: 24%; top: 0;     width: 340px; height: 190px; }
.sr-gp2 { right: 18%; top: 36px; width: 300px; height: 170px; }
.sr-gp3 { left: 4%;  top: 70px;  width: 260px; height: 160px; opacity: 0.38; }
.sr-gp4 { right: 3%; top: 0;     width: 240px; height: 150px; opacity: 0.42; }
.sr-gp5 { left: 42%; top: 120px; width: 300px; height: 130px; opacity: 0.30; }
.sr-gp6 { right: 26%; top: 120px; width: 200px; height: 120px; opacity: 0.34; }
.sr-traces {
    position: absolute; inset: 0; width: 100%; height: 230px;
    pointer-events: none; z-index: 1; overflow: visible;
}
/* dash period 860 on ~300px runs → each lit ~40% of its cycle;
   6 staggered paths ≈ 2–3 pulses visible at once (locked tuning) */
.sr-traces path {
    fill: none; stroke: rgba(227,179,65,0.5); stroke-width: 1.2;
    filter: drop-shadow(0 0 5px rgba(227,179,65,0.6));
    stroke-dasharray: 60 800;
    animation: sr-dash 8.5s linear infinite;
}
.sr-traces path:nth-of-type(2) { animation-delay: 1.4s; animation-duration: 9.6s; }
.sr-traces path:nth-of-type(3) { animation-delay: 2.9s; animation-duration: 7.7s; }
.sr-traces path:nth-of-type(4) { animation-delay: 4.2s; animation-duration: 9.1s; }
.sr-traces path:nth-of-type(5) { animation-delay: 5.7s; animation-duration: 8.0s; }
.sr-traces path:nth-of-type(6) { animation-delay: 7.1s; animation-duration: 10.2s; }
@keyframes sr-dash { to { stroke-dashoffset: -860; } }

/* ---- the three compounding numbers, as an open row ----
   Dividers carry the same trace pulse as the title zone. THE END FADE IS A
   MASK, not overflow: clipping made a pulse arriving at a tip look sliced
   off. The mask fades the static hairline AND the moving pulse identically,
   so light eases in and out at both ends. overflow:hidden stays only as a
   hard backstop well outside the visible fade. */
.fnums { margin-top: 2.1rem; }
.fnums-inner { display: flex; align-items: baseline; justify-content: center; }
.fnum {
    position: relative;
    text-align: center;
    padding: 0 clamp(1.6rem, 5vw, 4rem);
}
.fnum-n {
    display: block;
    font-family: 'Space Grotesk', sans-serif; font-weight: 700;
    font-size: clamp(1.9rem, 3.2vw, 2.6rem); line-height: 1;
    letter-spacing: -0.02em;
    transition: text-shadow 0.3s var(--ease);
}
.fnum:hover .fnum-n { text-shadow: 0 0 26px rgba(227, 179, 65, 0.45); }
.fnum-l { display: block; color: var(--text-2); font-size: 0.78rem; margin-top: 0.55rem; }
.fnum-dv {
    position: absolute; left: 0; top: 0.35rem; bottom: 0.1rem;
    width: 1px; overflow: hidden;
    background: rgba(196, 181, 160, 0.22);
    -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 78%, transparent 100%);
            mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 78%, transparent 100%);
}
.fnum-dv b {
    position: absolute; left: 0; width: 1px; height: 34%;
    background: linear-gradient(180deg, transparent, rgba(227, 179, 65, 0.95), transparent);
    filter: drop-shadow(0 0 4px rgba(227, 179, 65, 0.7));
    top: -40%; opacity: 0;
    animation: fnum-run var(--fd, 5.5s) linear infinite var(--fdel, 0s);
}
/* travel spans 60% of the cycle: at 5.5s that's the same 3.3s glide the
   11s/30% timing gave, but it comes round twice as often */
@keyframes fnum-run {
    0%   { top: -40%; opacity: 0; }
    6%   { opacity: 1; }
    52%  { opacity: 1; }
    60%  { top: 104%; opacity: 0; }
    100% { top: 104%; opacity: 0; }
}

@media (max-width: 760px) {
    .fnums-inner { flex-wrap: wrap; gap: 1.6rem 0; }
    .fnum { padding: 0 1.1rem; flex: 1 1 40%; }
    .fnum-dv { display: none; }   /* wrapped rows make vertical rules nonsense */
}


/* full-bleed rail; children ride the locked 1180px track (1116px content
   after the 2rem side padding the mockup carried) */
.srail {
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    padding: 0 2rem;
}
.sr-panel,
.sr-slots,
.sr-texts { max-width: 1116px; margin-left: auto; margin-right: auto; }
.sr-panel {
    position: relative; height: 400px;
    margin-top: 2.6rem;
    border: 1px solid #292418; border-radius: 18px;
    background: #141208; overflow: hidden;
}
.sr-silk { position: absolute; inset: 0; width: 100%; height: 100%; }
/* pause/play for the auto-advance — floats over the stage, top-right */
.sr-pause {
    position: absolute; top: 0.9rem; right: 0.9rem; z-index: 5;
    width: 34px; height: 34px; border-radius: 9px;
    border: 1px solid #35301f; background: rgba(17,15,10,0.7);
    backdrop-filter: blur(3px);
    display: flex; align-items: center; justify-content: center;
    transition: border-color 0.25s var(--ease), background 0.25s var(--ease);
}
.sr-pause:hover { border-color: rgba(227,179,65,0.55); background: #241f14; }
.sr-pause svg { width: 12px; height: 12px; }
.sr-pause svg rect, .sr-pause svg path { fill: var(--accent); }
.sr-pause .ic-play { display: none; }
.sr-pause[aria-pressed="true"] .ic-pause { display: none; }
.sr-pause[aria-pressed="true"] .ic-play { display: block; }
.sr-pause:focus-visible { outline: 1px solid var(--accent); outline-offset: 3px; }
.sr-shot {
    position: absolute; top: 50%; left: 50%;
    width: 88%; height: 78%;
    background: rgba(15,13,9,0.82);
    border: 1px solid #322c1d; border-radius: 14px;
    padding: 1.5rem 2rem;
    box-shadow: 0 30px 80px rgba(0,0,0,0.55);
    backdrop-filter: blur(4px);
    display: flex; flex-direction: column; justify-content: center;
    opacity: 0; transform: translate(-50%,-50%) translateY(14px) scale(0.985);
    transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
    pointer-events: none;          /* only the active shot is interactive */
}
.sr-shot.on { opacity: 1; transform: translate(-50%,-50%); pointer-events: auto; }
/* The graph shot is a BARE frame: zero padding so the canvas fills it
   edge to edge. With padding, a node clamped hard against the canvas
   edge still sat ~40px inside the rounded border people actually see,
   which reads as "it won't drag any further". overflow:hidden lets the
   frame's radius clip the square canvas corners. */
.sr-shot-bare { padding: 0; overflow: hidden; }
.sr-shot .graph-wrap {
    margin-top: 0; height: 100%;
    display: flex; flex-direction: column;
}
.sr-shot .graph-canvas {
    flex: 1; min-height: 0; height: auto;
    border: none; border-radius: 0;
    background: radial-gradient(ellipse at 50% 40%,
        rgba(196,181,160,0.04) 0%, transparent 70%), transparent;
}
.sr-illu { width: 180px; height: 180px; margin: 0 auto; opacity: 0.5; }
.sr-illu * {
    fill: none; stroke: #d8d0bf; stroke-width: 1.4;
    stroke-linecap: round; stroke-linejoin: round;
}

/* bottom deck: 3 slot columns + the mini grid on the right.
   display:contents on the tablist keeps tab semantics while the buttons
   and texts share one grid, so each text sits exactly under its own bar. */
/* four equal slot columns across the full rail track — the fleet-numbers
   grid that used to occupy a 5th column is being re-placed (see the
   ch02-numbers brainstorm); its CSS is kept for reuse. */
.sr-bottom {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    column-gap: 2rem;
    max-width: 1116px;
    margin: 1.6rem auto 0;
}
.sr-slots { display: contents; }
.sr-slot { cursor: pointer; text-align: left; padding: 0; grid-row: 1; }
.sr-bar {
    display: block; position: relative; height: 1px;
    background: #2c2820; margin-bottom: 0.85rem;
}
.sr-bar i {
    position: absolute; left: 0; top: -0.5px; height: 2px; width: 0%;
    background: linear-gradient(90deg, rgba(227,179,65,0.95), rgba(227,179,65,0.35));
    box-shadow: 0 0 8px rgba(227,179,65,0.4);
}
/* done slots keep a dim full bar, like the locked pattern */
.sr-slot.done .sr-bar i { width: 100%; opacity: 0.35; box-shadow: none; }
.sr-slabel {
    font-size: 11px; letter-spacing: 0.22em;
    color: var(--text-3); transition: color 0.3s;
}
.sr-slot.active .sr-slabel { color: var(--text); }
.sr-slot:hover .sr-slabel { color: var(--text-2); }
.sr-slot.active:hover .sr-slabel { color: var(--text); }
.sr-br { opacity: 0; color: var(--amber); }
.sr-slot.active .sr-br { opacity: 1; }
.sr-slot:focus-visible { outline: 1px solid var(--accent); outline-offset: 4px; }
/* status text beside the ordinal — only on the active slot, so the row
   reads as ordinals until you arrive at a section */
.sr-tag {
    font-size: 9px; letter-spacing: 0.16em; margin-left: 0.45rem;
    opacity: 0; transition: opacity 0.35s var(--ease);
}
.sr-slot.active .sr-tag { opacity: 1; }
.sr-tag-live { color: #9dbd8a; }
.sr-tag-progress { color: var(--text-2); }
.sr-tag-shipped { color: var(--amber); }

/* texts: ONE column each, directly under their own bar */
.sr-text {
    grid-row: 2; margin-top: 1.4rem;
    opacity: 0; transform: translateY(8px);
    transition: opacity 0.45s var(--ease), transform 0.45s var(--ease);
    pointer-events: none; visibility: hidden;
}
.sr-text.on { opacity: 1; transform: none; pointer-events: auto; visibility: visible; }
.sr-text[data-slot="0"] { grid-column: 1; }
.sr-text[data-slot="1"] { grid-column: 2; }
.sr-text[data-slot="2"] { grid-column: 3; }
.sr-text[data-slot="3"] { grid-column: 4; }
.sr-text .work-title { font-size: 1.15rem; margin-bottom: 0.55rem; }
.sr-text .work-desc { font-size: 0.84rem; line-height: 1.6; max-width: none; }

@media (max-width: 900px) {
    .sr-bottom { grid-template-columns: repeat(4, 1fr); column-gap: 1rem; }
    .sr-slabel { letter-spacing: 0.16em; }
    .sr-text[data-slot="0"],
    .sr-text[data-slot="1"],
    .sr-text[data-slot="2"],
    .sr-text[data-slot="3"] { grid-column: 1 / -1; }
}
@media (max-width: 700px) {
    .sr-panel { height: 340px; }
    .sr-shot { width: 92%; height: 84%; padding: 0.9rem 1rem; }
    .sr-bottom { grid-template-columns: 1fr; row-gap: 0.4rem; }
    .sr-slot { grid-row: auto; grid-column: 1 !important; }
    .sr-text { grid-row: auto; }
}

@media (max-width: 1100px) {
    .xcard { width: 460px; }
}
@media (max-width: 860px) {
    .xcard { width: min(86vw, 420px); min-height: 300px; padding: 1.2rem 1.3rem; }
    .xcard-title { font-size: 1.4rem; }
    /* The two lanes must NOT collapse into one here: they are semantic now
       (systems vs experiments), not the visual stagger the old .xrow pair
       was. A proper mobile pass is scheduled after all chapters land. */
    .xlane-chip { font-size: 8px; letter-spacing: 0.16em; padding: 3px 9px; }
    .xlane-num { font-size: 22px; }
}

/* below 1100px the constellation column shrinks; below 860px it goes entirely
   and the hero returns to a single column */
@media (max-width: 1100px) {
    .hero .chapter-inner { grid-template-columns: minmax(0, 1fr) 300px; gap: 2.5rem; }
    .hero-fleet { height: 280px; }
}
@media (max-width: 860px) {
    .hero .chapter-inner { display: block; max-width: 860px; }
    .hero-aside { display: none; }
    /* two strongest cones only — the pair that crosses the headline */
    .hero {
        --hero-cones: conic-gradient(from 0deg at 12% -10%,
            transparent 150deg,
            rgba(var(--hero-ray), 0.11) 153deg, rgba(var(--hero-ray), 0.34) 158deg, rgba(var(--hero-ray), 0.11) 162deg,
            transparent 166deg, transparent 170deg,
            rgba(var(--hero-ray), 0.10) 173deg, rgba(var(--hero-ray), 0.30) 177deg, rgba(var(--hero-ray), 0.10) 181deg,
            transparent 185deg);
        --hero-cones-mask: conic-gradient(from 0deg at 12% -10%,
            transparent 150deg,
            rgba(0,0,0,0.40) 153deg, #000 158deg, rgba(0,0,0,0.40) 162deg,
            transparent 166deg, transparent 170deg,
            rgba(0,0,0,0.35) 173deg, #000 177deg, rgba(0,0,0,0.35) 181deg,
            transparent 185deg);
    }
}

/* infra floor: the three servers survive every width; the two middle devices
   go first because their labels collide with the server pins above them.
   The pulses' destinations are restated so nothing flies at a hidden pin —
   the origins still differ, so the three paths stay distinct. */
@media (max-width: 760px) {
    /* 470 buys the four-chip stack the headroom it needs once the pins drop
       with the shorter floor; 430 clipped it by 32px */
    .floor { height: 470px; }
    .fdev-mid { display: none; }
    .fpulse {
        --d1x: 79%;  --d1y: 27%;
        --d2x: 21%;  --d2y: 27%;
        --d3x: 79%;  --d3y: 27%;
    }
}
@media (max-width: 520px) {
    /* chips would overlap between neighbouring pins at this width */
    .floor { height: 400px; }
    .fpin-chips { display: none; }
    .fpin-label span { font-size: 0.8rem; }
}
/* the spec cells step down independently of the floor: three columns need the
   full 1240, two survive well below it */
@media (max-width: 900px) {
    .infra-cells { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
    .infra-cells { grid-template-columns: 1fr; }
}

@media (max-width: 700px) {
    .nav { padding: 1.1rem 1.4rem; }
    .nav.scrolled { padding: 0.9rem 1.4rem; }
    .nav-menu { gap: 1.2rem; }
    .nav-link { font-size: 10px; }
    .ms-label { display: none; }
    .ms-opt { padding: 0.25rem 0.5rem; }
    .mode-switch { top: 3.6rem; right: 1.4rem; }

    .chapter-inner { padding: 5rem 1.4rem; }
    .hero .chapter-inner { padding-top: 8rem; padding-bottom: 4rem; }
    .hero-name { font-size: clamp(1.9rem, 9vw, 2.6rem); }
    .hero-ctas { gap: 1.6rem; }


    .metrics td { display: block; }
    .metrics-key { width: auto; padding: 0.6rem 0.2rem 0; border-top: 1px solid var(--line); }
    .metrics .metrics-val { border-top: none; padding-top: 0.15rem; }
    .metrics tr:last-child .metrics-key { border-bottom: none; }

    .stat { display: none; }
    .graph-canvas { height: 300px; }

    .work { padding: 2.4rem 0; }
    .work-links { gap: 1.4rem; }
    .media-pair { flex-direction: column; }
    .media-pair img { width: 100%; }

    .row-line { flex-wrap: wrap; gap: 0.8rem; }
    .row-toggle { margin-left: 0; flex-basis: 100%; }

    .contact-row { gap: 1rem; }
    .contact-key { width: 8ch; }
    .contact-hint { display: none; }
    .contact-row.copied .contact-hint { display: inline; }
}

/* ============================================================
   CH02 · STAGE SLOT 03 — VOICE-TO-AGENT RELAY
   A closed relay circuit read as an instrument: wrist -> tunnel -> relay ->
   live session -> back to the wrist. One amber packet completes one lap per
   cycle. All motion hangs off `.sr-shot.on`, so it restarts every time the
   slot is shown and runs nowhere else.
   Timing: the lap is 6.6s against the rail's 7s dwell, so a viewer always
   sees the whole round trip, press to reply, before the crossfade.
============================================================ */
.vr {
    flex: 1 1 auto; min-height: 0; width: 100%;
    display: flex; align-items: center; justify-content: center;
}
.vr-svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* ---- structure: non-scaling strokes so the instrument stays crisp when the
        whole band is scaled down on a narrow viewport ---- */
.vr-loop, .vr-gate, .vr-gate-lit, .vr-port, .vr-pin, .vr-bus, .vr-mini,
.vr-log, .vr-log-hit, .vr-sess-hdr, .vr-sess-hdr-lit, .vr-sess-gut,
.vr-lead, .vr-dim-l, .vr-mod-body, .vr-sess-body,
.vr-ring-o, .vr-ring-i, .vr-ring-press, .vr-ring-recv {
    fill: none;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
}
.vr-zone   { fill: rgba(196,181,160,0.022); }
.vr-grat-v { stroke: rgba(196,181,160,0.075); stroke-width: 1; stroke-dasharray: 1 6; }
.vr-axis   { stroke: rgba(196,181,160,0.06);  stroke-width: 1; stroke-dasharray: 1 8; }

.vr-loop { stroke: rgba(196,181,160,0.34); stroke-width: 1.5; }

.vr-gate     { stroke: rgba(196,181,160,0.28); stroke-width: 1.2; stroke-dasharray: 4 5; }
.vr-port     { stroke: rgba(196,181,160,0.36); stroke-width: 1.2; }
.vr-gate-lit { stroke: #e3b341; stroke-width: 1.4; stroke-dasharray: 4 5; opacity: 0.1; }

/* The two legs must not read as the same signal: outbound is SAMPLED (discrete
   bars, bursty, with real pauses), the reply is GENERATED (one continuous
   filled envelope). That asymmetry is what shows direction in the still frame.
   These strokes deliberately scale, so bar density stays honest when shrunk. */
.vr-wave-out {
    fill: none; stroke: rgba(216,208,191,0.46); stroke-width: 1.8;
    stroke-linecap: round;
}
.vr-wave-in {
    fill: none; stroke: rgba(216,208,191,0.36); stroke-width: 1.3;
    stroke-linejoin: round;
}

.vr-mod-body { stroke: rgba(196,181,160,0.28); stroke-width: 1.2; fill: rgba(20,18,8,0.55);   /* #141208, the panel bg */ }
.vr-sess-body { stroke: rgba(196,181,160,0.28); stroke-width: 1.4; }
.vr-pin  { stroke: rgba(196,181,160,0.32); stroke-width: 1.2; }
.vr-bus  { stroke: rgba(196,181,160,0.16); stroke-width: 1.1; stroke-dasharray: 3 4; }
.vr-mini { stroke: rgba(216,208,191,0.40); stroke-width: 1.2; }
.vr-tok  { fill: rgba(196,181,160,0.30); }
.vr-relay-core { fill: #e3b341; opacity: 0.75; }

.vr-sess-hdr     { stroke: rgba(196,181,160,0.14); stroke-width: 1; }
.vr-sess-hdr-lit { stroke: rgba(196,181,160,0.34); stroke-width: 1.4; }
.vr-log      { stroke: rgba(196,181,160,0.17); stroke-width: 1.4; }
.vr-log-hit  { stroke: #e3b341; stroke-width: 1.6; opacity: 0.3; }
.vr-sess-gut { stroke: rgba(196,181,160,0.14); stroke-width: 1.2; }

.vr-ring-o { stroke: rgba(196,181,160,0.36); stroke-width: 1.3; }
.vr-ring-i { stroke: rgba(216,208,191,0.42); stroke-width: 1.2; }
.vr-core   { fill: #e3b341; opacity: 0.7; }
/* both ripples are invisible at rest, so a faded-out shot or a reduced-motion
   frame never shows a ring frozen mid-flight */
.vr-ring-press, .vr-ring-recv {
    stroke: #f0c574; stroke-width: 1.4; opacity: 0;
    transform-box: fill-box; transform-origin: 50% 50%;
}

.vr-dim-l { stroke: rgba(196,181,160,0.18); stroke-width: 1.1; }
.vr-st, .vr-an {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    text-anchor: middle;
}
.vr-st   { font-size: 9px;   letter-spacing: 2.2px; fill: rgba(232,228,222,0.44); }
.vr-an   { font-size: 8.5px; letter-spacing: 2px;   fill: rgba(196,181,160,0.32); }
.vr-an-l { text-anchor: start; }
.vr-an-r { text-anchor: end; }
.vr-arrow { fill: rgba(196,181,160,0.4); }

/* ---- the packet ----
   dasharray = one 46-long head plus two ticks placed at the END of the
   pattern, which is what puts them BEHIND the head rather than ahead of it.
   Measured arc length of the loop is 1622.9; the sweep overruns to
   -1667 so the wake fully clears the tail, and that overrun is why the
   travel phase ends at 78.0% rather than 100%. */
.vr-pulse {
    fill: none; stroke: #f0c574; stroke-width: 2.1; stroke-linecap: round;
    vector-effect: non-scaling-stroke;
    /* same glow idiom as .sr-traces, so no SVG <defs> and no id to collide */
    filter: drop-shadow(0 0 5px rgba(227,179,65,0.6));
    stroke-dasharray: 46 3960 13 7 6 8;
    stroke-dashoffset: 66;            /* parked off the path */
}

/* ---- motion, only while this slot is the active shot ---- */
.sr-shot.on .vr-pulse      { animation: vr-run   6.6s linear infinite; }
.sr-shot.on .vr-gate-lit   { animation: vr-gate  6.6s linear infinite; }
.sr-shot.on .vr-relay-core { animation: vr-relay 6.6s linear infinite; }
.sr-shot.on .vr-log-hit    { animation: vr-hit   6.6s linear infinite; }
.sr-shot.on .vr-sess-body  { animation: vr-sess  6.6s linear infinite; }
.sr-shot.on .vr-core       { animation: vr-core  6.6s linear infinite; }
.sr-shot.on .vr-ring-press { animation: vr-press 6.6s linear infinite; }
.sr-shot.on .vr-ring-recv  { animation: vr-recv  6.6s linear infinite; }

/* the stage's own pause button freezes the relay too */
.sr-panel:has(.sr-pause[aria-pressed="true"]) .vr-svg * {
    animation-play-state: paused;
}

/* Where the packet's head is, as a share of the cycle — every flash below is
   pinned to one of these, all derived from the measured arc length:
     0.9% leaves the wrist · 14.8% crosses the bulkhead ·
     24.2% relay · 31.3% enters the session ·
     43.6% leaves it · 50.6% relay · 60.0% crosses back ·
     73.9% lands on the wrist ring · 78.0% wake clear, then a rest beat. */
@keyframes vr-run {
    0%                 { stroke-dashoffset: 66; }
    78.0%    { stroke-dashoffset: -1667; }
    100%               { stroke-dashoffset: -1667; }
}
@keyframes vr-gate {
    0%, 11.6%              { opacity: 0.1; }
    14.8%                    { opacity: 0.85; }
    19.3%, 56.8% { opacity: 0.1; }
    60.0%                    { opacity: 0.85; }
    64.5%, 100%            { opacity: 0.1; }
}
@keyframes vr-relay {
    0%, 21.0%                 { opacity: 0.75; }
    24.2%                       { opacity: 1; }
    28.7%, 47.4%   { opacity: 0.75; }
    50.6%                       { opacity: 1; }
    55.1%, 100%               { opacity: 0.75; }
}
/* the injected line lands, holds while the session works, then falls back to
   its resting brightness — which is also the reduced-motion state */
@keyframes vr-hit {
    0%, 28.1%        { opacity: 0.3; }
    31.3%              { opacity: 1; }
    43.6%             { opacity: 1; }
    49.6%, 100%     { opacity: 0.3; }
}
@keyframes vr-sess {
    0%, 28.1%          { stroke: rgba(196,181,160,0.28); }
    31.3%, 43.6% { stroke: rgba(227,179,65,0.5); }
    49.6%, 100%       { stroke: rgba(196,181,160,0.28); }
}
/* press on departure, then a second beat when the reply lands */
@keyframes vr-core {
    0%                     { opacity: 1; }
    8%, 70.7%      { opacity: 0.7; }
    73.9%            { opacity: 1; }
    83.9%, 100%    { opacity: 0.7; }
}
@keyframes vr-press {          /* expanding outward = leaving the wrist */
    0%   { opacity: 0.85; transform: scale(0.45); }
    9%   { opacity: 0;    transform: scale(1.75); }
    100% { opacity: 0;    transform: scale(1.75); }
}
@keyframes vr-recv {           /* collapsing inward = arriving at the wrist */
    0%, 70.7%   { opacity: 0;   transform: scale(1.8); }
    73.9%         { opacity: 0.8; transform: scale(1.66); }
    85.9%       { opacity: 0;   transform: scale(1); }
    100%                { opacity: 0;   transform: scale(1); }
}

/* ---- reduced motion: no animation at all. The circuit is instead shown
        energised end to end and the injection left landed, so the static frame
        reads as a completed closed round trip, not an empty diagram. ---- */
@media (prefers-reduced-motion: reduce) {
    .vr-svg *, .sr-shot.on .vr-svg * { animation: none !important; }
    .vr-pulse {
        stroke: rgba(227,179,65,0.30); stroke-width: 1.6;
        stroke-dasharray: none; stroke-dashoffset: 0; filter: none;
    }
    .vr-gate-lit { opacity: 0.4; }
    .vr-log-hit  { opacity: 0.8; }
}

/* ---- small viewports: the band scales down as a whole, so drop the type and
        the graticule before they turn into sub-pixel mush and lift the
        waveforms so they still read. Nothing reflows, nothing can overflow. */
@media (max-width: 700px) {
    .vr-txt, .vr-dim, .vr-grat { display: none; }
    /* 3.48:1 art in a ~1.2:1 box letterboxes by definition; height:auto makes
       the element match the band so it centres cleanly instead of floating. */
    .vr-svg { height: auto; }
    .vr-wave-out { stroke: rgba(216,208,191,0.62); }
    .vr-wave-in  { stroke: rgba(216,208,191,0.46); fill: rgba(196,181,160,0.14); }
}

/* ============================================================
   CH02 · STAGE SLOT 02 — AGENTIC AI IN PRACTICE (TRIPTYCH)
   Three hairline-divided bays, one working instrument per technique
   studied: durable skills / spec-driven planning / encoded self-correction.

   Motion principle, same as slot 03: the STRUCTURE is always fully drawn
   and only an emphasis moves. That is what lets a shot fade in mid-cycle
   without ever showing a half-built diagram, and it makes the
   reduced-motion frame identical to the resting frame rather than a
   special case. The emphasis runs bay 1 -> bay 2 -> bay 3 so the eye
   reads the three proofs in the order the paragraph names them.
   Cycle is 6.6s against the rail's 7s dwell, so all three land before
   the crossfade.
============================================================ */
.tq {
    flex: 1 1 auto; min-height: 0; width: 100%;
    display: flex; align-items: center; justify-content: center;
}
.tq-svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* ---- structure ---- */
.tq-ln {
    fill: none; stroke-linecap: round; stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
}
.tq-f0 { stroke: rgba(196,181,160,0.16); }
.tq-f1 { stroke: rgba(196,181,160,0.30); }
.tq-f2 { stroke: rgba(196,181,160,0.46); }
.tq-am { stroke: #e3b341; }
.tq-w1 { stroke-width: 1; } .tq-w12 { stroke-width: 1.2; }
.tq-w14 { stroke-width: 1.4; } .tq-w16 { stroke-width: 1.6; }
.tq-w2 { stroke-width: 2; }
.tq-dash { stroke-dasharray: 4 5; }
.tq-dot  { stroke-dasharray: 1 5; }
.tq-plate { fill: rgba(20,18,8,0.55); }        /* #141208, the panel bg */
.tq-glow { filter: drop-shadow(0 0 5px rgba(227,179,65,0.55)); }

/* the built profile carries a faint amber wash so it reads as realised
   against the plan's hollow dashed outline directly above it */
.tq-built {
    fill: rgba(227,179,65,0.07);
    stroke: #e3b341; stroke-width: 1.4;
    stroke-linejoin: round; vector-effect: non-scaling-stroke;
}
/* the same profile in grey, sitting under the amber one: the shape the head
   has not reached yet. Hollow, so the amber wash above reads as the fill
   arriving rather than changing colour. */
.tq-built-ghost {
    fill: none;
    stroke: rgba(196,181,160,0.30); stroke-width: 1.4;
    stroke-linejoin: round; vector-effect: non-scaling-stroke;
}
/* OPEN at rest. The animation closes it at 0% and re-opens it across the
   sweep, so a paused frame — and the reduced-motion frame, where the blanket
   `animation: none` below drops back to exactly this rule — shows the profile
   fully built, which is the bay's whole claim. */
.tq-wipe { transform: translateX(220px); }
.tq-seal { fill: #e3b341; }

.tq-hd, .tq-sub, .tq-l {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
}
.tq-hd  { font-size: 9px;   letter-spacing: 2.2px; fill: rgba(232,228,222,0.46); text-anchor: middle; }
.tq-sub { font-size: 8px;   letter-spacing: 1.7px; fill: rgba(196,181,160,0.32); text-anchor: middle; }
.tq-sub-s { font-size: 7.5px; text-anchor: start; }
.tq-l   { font-size: 8.5px; letter-spacing: 1.8px; fill: rgba(196,181,160,0.34); text-anchor: start; }
.tq-l-am { fill: rgba(227,179,65,0.62); }

/* ---- resting state of the three moving parts ----
   All three are invisible at rest, so a faded-out shot and the
   reduced-motion frame never catch a head parked mid-sweep. */
.tq-scan, .tq-glint { opacity: 0; }
/* the reuses rest slightly down so the sweep has somewhere to brighten TO.
   0.7 x the 0.46 stroke still reads as fully drawn at rest — this is a
   dimmer, not a hide, so the reduced-motion frame loses nothing. */
.tq-inst { opacity: 0.7; }

/* ---- motion, only while this slot is the active shot ---- */
.sr-shot.on .tq-i1    { animation: tq-inst 6.6s linear infinite 0.30s; }
.sr-shot.on .tq-i2    { animation: tq-inst 6.6s linear infinite 0.62s; }
.sr-shot.on .tq-i3    { animation: tq-inst 6.6s linear infinite 0.94s; }
.sr-shot.on .tq-i4    { animation: tq-inst 6.6s linear infinite 1.26s; }
.sr-shot.on .tq-now   { animation: tq-now   6.6s ease-in-out infinite; }
.sr-shot.on .tq-scan  { animation: tq-scan  6.6s linear infinite; }
.sr-shot.on .tq-wipe  { animation: tq-wipe  6.6s linear infinite; }
.sr-shot.on .tq-glint { animation: tq-glint 6.6s linear infinite; }
.sr-shot.on .tq-seal  { animation: tq-seal  6.6s linear infinite; }

/* the stage's own pause button freezes the triptych too */
.sr-panel:has(.sr-pause[aria-pressed="true"]) .tq-svg * {
    animation-play-state: paused;
}

/* bay 1: each reuse brightens in turn, then settles back — the plate is
   the same every time, which IS the point, so only the light changes */
@keyframes tq-inst {
    0%, 3%   { opacity: 0.7; filter: none; }
    6%       { opacity: 1;   filter: drop-shadow(0 0 4px rgba(227,179,65,0.55)); }
    14%,100% { opacity: 0.7; filter: none; }
}
/* the current run acknowledges the sweep reaching it */
@keyframes tq-now {
    0%, 22%   { opacity: 0.75; }
    28%       { opacity: 1; }
    38%, 100% { opacity: 0.75; }
}
/* bay 2: the build head lays the profile down against the plan above.
   The fades are tight (2%) so the head is lit for essentially the whole
   travel — at 4% it was invisible for the first stretch of the sweep while
   the profile was already turning amber behind nothing. */
@keyframes tq-scan {
    0%, 30%   { opacity: 0;   transform: translateX(0); }
    32%       { opacity: 0.9; }
    56%       { opacity: 0.9; }
    58%       { opacity: 0;   transform: translateX(220px); }
    100%      { opacity: 0;   transform: translateX(220px); }
}
/* The colour edge. Identical timing to tq-scan's transform on purpose: these
   two ARE the same edge, and drifting them apart is the bug this replaced. */
@keyframes tq-wipe {
    0%, 30%   { transform: translateX(0); }
    58%       { transform: translateX(220px); }
    100%      { transform: translateX(220px); }
}
/* bay 3: run 02 is read end to end and passes clean through the seal */
@keyframes tq-glint {
    0%, 60%   { opacity: 0;   transform: translateX(0); }
    64%       { opacity: 0.9; }
    84%       { opacity: 0.9; }
    88%       { opacity: 0;   transform: translateX(198px); }
    100%      { opacity: 0;   transform: translateX(198px); }
}
/* the seal answers as the head crosses it (x=752 is ~38% of that travel) */
@keyframes tq-seal {
    0%, 68%   { opacity: 1; }
    72%       { opacity: 1; filter: drop-shadow(0 0 6px rgba(227,179,65,0.85)); }
    80%, 100% { opacity: 1; }
}

/* ---- reduced motion: nothing moves. The resting frame is already the
        complete instrument — all three bays fully drawn — so there is no
        special static composition to build here. ---- */
@media (prefers-reduced-motion: reduce) {
    .tq-svg *, .sr-shot.on .tq-svg * { animation: none !important; }
    .tq-scan, .tq-glint { opacity: 0; }
}

/* ---- small viewports: the band scales as a whole, so the 8-9px mono
        would land at ~3px. Drop every label and the outer frame and let
        the three mechanisms carry it, exactly as slot 03 does. ---- */
@media (max-width: 700px) {
    .tq-txt, .tq-frame { display: none; }
    /* 3.48:1 art in a ~1.2:1 box letterboxes by definition; height:auto
       makes the element match the band so it centres instead of floating. */
    .tq-svg { height: auto; }
}

/* ============================================================
   CH02 · STAGE SLOT 04 — THE COMPOUND LEDGER
   Cumulative mass: x = date, y = seals sealed so far. Every mark in here is
   one real seal and every arc one real link — the markup is GENERATED by a
   script that patches index.html between the ch02:slot04 markers. Re-run it
   after sealing and the art is true again.

   The first four months are nearly flat. That emptiness is deliberate: it is
   what makes July's near-vertical rise mean anything, and Owen signed it off
   explicitly rather than have the axis rescaled to hide it.

   Motion is the same principle as slots 02 and 03: the structure is ALWAYS
   fully drawn and only an emphasis moves — here a glint that reads the record
   end to end, arriving at the total. 6.6s against the rail's 7s dwell.
============================================================ */
.cl {
    flex: 1 1 auto; min-height: 0; width: 100%;
    display: flex; align-items: center; justify-content: center;
}
.cl-svg { display: block; width: 100%; height: 100%; overflow: visible; }

.cl-mass { fill: rgba(227,179,65,0.075); stroke: none; }

/* 137 arcs have to sit UNDER the curve in visual weight or the piece turns
   into a cat's cradle. Height already encodes reach, so alpha stays flat. */
.cl-links {
    fill: none; stroke: rgba(196,181,160,0.155); stroke-width: 1;
    vector-effect: non-scaling-stroke;
}

/* zero-length round-capped subpaths: dots that survive any viewBox scale */
.cl-dots {
    fill: none; stroke: rgba(216,208,191,0.55);
    stroke-width: 2.6; stroke-linecap: round;
    vector-effect: non-scaling-stroke;
}
.cl-origin {
    fill: none; stroke: rgba(227,179,65,0.8);
    stroke-width: 5; stroke-linecap: round;
    vector-effect: non-scaling-stroke;
}

.cl-axis, .cl-tick {
    fill: none; stroke: rgba(196,181,160,0.30); stroke-width: 1;
    stroke-linecap: round; vector-effect: non-scaling-stroke;
}
.cl-curve {
    fill: none; stroke: #e3b341; stroke-width: 1.6;
    stroke-linejoin: round; stroke-linecap: round;
    vector-effect: non-scaling-stroke;
    filter: drop-shadow(0 0 5px rgba(227,179,65,0.5));
}

/* the read head. pathLength="1000" on the element normalises the geometry,
   so these numbers stay correct however the ledger grows. */
.cl-glint {
    fill: none; stroke: #f0c574; stroke-width: 2.6;
    stroke-linecap: round; vector-effect: non-scaling-stroke;
    filter: drop-shadow(0 0 6px rgba(240,197,116,0.75));
    stroke-dasharray: 26 974;
    stroke-dashoffset: 26;            /* parked off the path */
    opacity: 0;
}

.cl-hd, .cl-sub, .cl-n {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
}
.cl-hd  { font-size: 9px;   letter-spacing: 2.2px; fill: rgba(232,228,222,0.46); text-anchor: start; }
.cl-sub { font-size: 8px;   letter-spacing: 1.7px; fill: rgba(196,181,160,0.32); text-anchor: middle; }
.cl-sub-l { text-anchor: start; }
.cl-sub-r { text-anchor: end; }
.cl-n   { font-size: 13px;  letter-spacing: 1px;   fill: rgba(227,179,65,0.75); text-anchor: start; }

/* ---- motion, only while this slot is the active shot ---- */
.sr-shot.on .cl-glint { animation: cl-run  6.6s linear infinite; }
.sr-shot.on .cl-n     { animation: cl-tot  6.6s ease-out infinite; }
.sr-shot.on .cl-origin{ animation: cl-orig 6.6s ease-out infinite; }

/* the stage's own pause button freezes the ledger too */
.sr-panel:has(.sr-pause[aria-pressed="true"]) .cl-svg * {
    animation-play-state: paused;
}

/* travel ends at 82% — the overrun clears the head off the end of the path
   before the rest beat, so it never sits parked on top of the total */
@keyframes cl-run {
    0%       { stroke-dashoffset: 26;    opacity: 0; }
    5%       { opacity: 0.95; }
    76%      { opacity: 0.95; }
    82%      { stroke-dashoffset: -1010; opacity: 0; }
    100%     { stroke-dashoffset: -1010; opacity: 0; }
}
/* the total answers as the head arrives */
@keyframes cl-tot {
    0%, 72%   { opacity: 0.72; }
    79%       { opacity: 1; }
    92%, 100% { opacity: 0.72; }
}
/* and the first seal marks where the whole record starts */
@keyframes cl-orig {
    0%       { opacity: 0.55; }
    6%       { opacity: 1; }
    20%,100% { opacity: 0.55; }
}

/* ---- reduced motion: nothing moves. The resting frame is already the whole
        record — curve, mass, every seal and every link — so there is no
        special static composition to build. ---- */
@media (prefers-reduced-motion: reduce) {
    .cl-svg *, .sr-shot.on .cl-svg * { animation: none !important; }
    .cl-glint { opacity: 0; }
    .cl-origin { opacity: 0.9; }
}

/* ---- small viewports: the 8-9px mono would land near 3px once the band is
        scaled down, so drop the type, exactly as slots 02 and 03 do. ---- */
@media (max-width: 700px) {
    .cl-txt { display: none; }
    /* 3.48:1 art in a ~1.2:1 box letterboxes by definition; height:auto makes
       the element match the band so it centres instead of floating. */
    .cl-svg { height: auto; }
    .cl-dots { stroke-width: 2.1; }
}

/* ============================================================
   CH04 · CARD POPUP — one morph, three fill-in-able layouts
   ------------------------------------------------------------

   The panel animates REAL GEOMETRY (top/left/width/height), never
   transform: scale(). A scale transform smears text and hands a video a
   distorted backing store; layout-driven morphing is "slower" in the
   abstract and is what makes the contents land pixel-correct.
============================================================ */
.xpop { position: fixed; inset: 0; z-index: 120; }
.xpop[hidden] { display: none; }

/* backdrop-filter IGNORES the element's opacity — fading a blurred scrim by
   animating opacity gives a hard blur snap on the first frame. Transition the
   blur() value and the tint instead. */
.xpop-scrim {
    position: absolute; inset: 0;
    background: rgba(9, 8, 5, 0);
    backdrop-filter: blur(0px);
    -webkit-backdrop-filter: blur(0px);
    transition: background 0.4s ease, backdrop-filter 0.4s ease;
}
.xpop.open .xpop-scrim {
    background: rgba(9, 8, 5, 0.86);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
}

.xpop-morph {
    --c: 240, 197, 116;
    position: fixed; top: 3vh; left: 3vw; width: 94vw; height: 94vh;
    background: #141208;
    border: 1px solid #322c1d;
    border-radius: 16px;
    overflow: hidden;
    display: flex; flex-direction: column;
    box-shadow: 0 40px 120px rgba(0, 0, 0, 0.6);
    transition: top 0.45s var(--ease), left 0.45s var(--ease),
                width 0.45s var(--ease), height 0.45s var(--ease),
                border-radius 0.45s var(--ease);
}
.xpop-morph::after {
    content: ''; position: absolute; inset: 0; pointer-events: none;
    background: radial-gradient(80% 60% at 12% 0%, rgba(var(--c), 0.09), transparent 60%);
}
.xpop-inner {
    position: relative; z-index: 2; flex: 1; min-height: 0;
    display: flex; flex-direction: column;
    opacity: 0; transition: opacity 0.2s ease;
}
.xpop.open .xpop-inner { opacity: 1; transition: opacity 0.35s ease 0.18s; }

.xpop-close {
    position: absolute; top: 16px; right: 18px; z-index: 6;
    width: 34px; height: 34px; font-size: 12px;
    color: var(--text-3); background: rgba(20, 18, 8, 0.8);
    border: 1px solid var(--line); border-radius: 99px; cursor: pointer;
    transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.xpop-close:hover { color: var(--cream); border-color: #4a412c; }
.xpop-close:focus-visible { outline: 1px solid var(--accent); outline-offset: 3px; }

/* ---- shared pieces ---- */
.xp-badge {
    display: inline-block; align-self: flex-start;   /* a flex column would stretch it */
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 9.5px; letter-spacing: 0.18em;
    color: rgba(var(--c), 0.9);
    border: 1px solid rgba(var(--c), 0.38);
    border-radius: 99px; padding: 5px 12px;
}
.xp-title {
    font-family: 'Space Grotesk', sans-serif; font-weight: 600;
    letter-spacing: -0.02em; line-height: 1.02; margin: 14px 0 0;
}
.xp-body { color: #cfc9bd; line-height: 1.72; margin: 16px 0 0; }
.xp-body p { margin: 0; }
.xp-body p + p { margin-top: 1em; }
.xp-caps {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 10.5px; letter-spacing: 0.14em; color: var(--text-3);
    margin-top: 18px; line-height: 2;
}
.xp-lab {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 9.5px; letter-spacing: 0.22em;
    color: rgba(232, 228, 222, 0.4); margin-bottom: 12px;
}
.xp-metrics { width: 100%; border-collapse: collapse; }
.xp-metrics td {
    padding: 0.62rem 0; font-size: 0.84rem; vertical-align: top;
    border-bottom: 1px solid rgba(196, 181, 160, 0.09);
}
.xp-metrics .k {
    width: 32%; padding-right: 16px; color: var(--text-3);
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 10px; letter-spacing: 0.13em;
}
.xp-metrics .v { color: #d8d0bf; }
.xp-media { display: block; width: 100%; border-radius: 10px; border: 1px solid var(--line); }
.xp-shot + .xp-shot { margin-top: 14px; }
.xp-cap {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 9.5px; letter-spacing: 0.14em; color: var(--text-3); margin-top: 10px;
}
/* the ghost icon blown up as line art — this is what carries the five cards
   that have no extra content yet. Injected raw, so fill/stroke MUST be set
   here or the SVG defaults to fill:black and paints a solid silhouette. */
.xp-ghost { pointer-events: none; display: flex; align-items: center; justify-content: center; }
.xp-ghost svg {
    width: 100%; height: 100%;
    fill: none; stroke: rgba(var(--c), 0.42); stroke-width: 1.05;
    stroke-linecap: round; stroke-linejoin: round;
    filter: drop-shadow(0 0 18px rgba(var(--c), 0.18));
}

/* ---- layout A · editorial split ---- */
.xp-a { display: grid; grid-template-columns: 1fr 1fr; height: 100%; }
.xp-a .xp-l { padding: 64px 48px 48px 56px; overflow: auto; display: flex; flex-direction: column; }
.xp-a .xp-r {
    position: relative; border-left: 1px solid var(--line);
    padding: 64px 56px 48px 48px; overflow: auto; background: rgba(0, 0, 0, 0.16);
}
.xp-a .xp-title { font-size: clamp(2.2rem, 3.4vw, 3.4rem); }
.xp-a .xp-body { font-size: 1.02rem; max-width: 52ch; }
.xp-a .xp-ghost { height: 100%; min-height: 260px; }
.xp-a .xp-ghost > div { width: 62%; aspect-ratio: 1; }

/* ---- layout B · hero band ---- */
.xp-b { display: flex; flex-direction: column; height: 100%; }
.xp-b .xp-hero {
    position: relative; flex: 0 0 52%; min-height: 0; overflow: hidden;
    border-bottom: 1px solid var(--line); background: #100e08;
    display: flex; align-items: center; justify-content: center;
}
/* max-height:100% would not resolve against this percentage-height hero, so a
   portrait asset kept its intrinsic height and got clipped. Fill the hero
   absolutely and let object-fit letterbox inside it — the README says B wants a
   wide asset, but picking wrong should look plain, not broken. */
.xp-b .xp-heromedia { position: absolute; inset: 0; }
.xp-b .xp-hero .xp-media {
    width: 100%; height: 100%;
    object-fit: contain; border: 0; border-radius: 0;
}
.xp-b .xp-heroband .xp-cap { margin-top: 8px; }
.xp-b .xp-heroband {
    position: absolute; left: 0; right: 0; bottom: 0; padding: 40px 56px 26px;
    display: flex; flex-direction: column;
    background: linear-gradient(0deg, rgba(20,18,8,0.96), rgba(20,18,8,0.6) 55%, transparent);
}
.xp-b .xp-title { font-size: clamp(2rem, 3vw, 3rem); margin: 10px 0 0; }
.xp-b .xp-deck {
    flex: 1; min-height: 0; overflow: auto; padding: 30px 56px 44px;
    display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 48px;
}
.xp-b .xp-body { font-size: 1rem; margin: 0; }
.xp-b .xp-hero .xp-ghost { width: 100%; height: 100%; }
.xp-b .xp-hero .xp-ghost > div { width: 26%; aspect-ratio: 1; }

/* ---- layout C · instrument panel ---- */
.xp-c { display: flex; flex-direction: column; height: 100%; }
.xp-c .xp-top {
    display: flex; align-items: center; gap: 18px;
    padding: 22px 56px; border-bottom: 1px solid var(--line);
}
.xp-c .xp-title { font-size: 1.5rem; margin: 0; }
.xp-c .xp-stage {
    position: relative; flex: 1; min-height: 0; margin: 26px 56px 0;
    border: 1px solid #322c1d; border-radius: 14px; background: rgba(15, 13, 9, 0.82);
    overflow: auto; padding: 26px 32px;
}
/* only MEDIA is centred. Centring a metrics table floated it in the middle of
   a large empty box and overlapped its own label. */
.xp-c .xp-stage.has-media {
    display: flex; align-items: center; justify-content: center;
    overflow: hidden; padding: 22px;
}
.xp-c .xp-stage .xp-media { max-height: 100%; width: auto; max-width: 100%; object-fit: contain; }
.xp-c .xp-stage .xp-ghost { height: 100%; min-height: 240px; }
.xp-c .xp-stage .xp-ghost > div { width: 46%; aspect-ratio: 1; }
.xp-c .xp-deck {
    padding: 22px 56px 34px; display: grid;
    grid-template-columns: 1.4fr 1fr; gap: 44px; align-items: start;
}
.xp-c .xp-body { font-size: 0.95rem; margin: 0; }
.xp-c .xp-chips {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 1px;
    background: rgba(196, 181, 160, 0.10);
    border: 1px solid rgba(196, 181, 160, 0.10);
    border-radius: 10px; overflow: hidden;
}
.xp-c .xp-chip {
    background: #141208; padding: 13px 14px; display: flex; align-items: center;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 8.5px; letter-spacing: 0.13em; line-height: 1.5; color: var(--text-3);
}

/* ---- reduced motion: no morph. The panel simply appears at rest. ---- */
@media (prefers-reduced-motion: reduce) {
    .xpop-morph, .xpop-scrim, .xpop-inner { transition: none !important; }
}

/* ---- narrow: the split and the two-column decks collapse ---- */
@media (max-width: 900px) {
    .xpop-morph { top: 2vh; left: 2vw; width: 96vw; height: 96vh; }
    .xp-a { grid-template-columns: 1fr; }
    .xp-a .xp-l { padding: 52px 26px 20px; }
    .xp-a .xp-r { border-left: 0; border-top: 1px solid var(--line); padding: 26px; }
    .xp-b .xp-deck, .xp-c .xp-deck { grid-template-columns: 1fr; gap: 26px; padding: 24px 26px 34px; }
    .xp-b .xp-heroband { padding: 30px 26px 18px; }
    .xp-c .xp-top { padding: 18px 26px; }
    .xp-c .xp-stage { margin: 20px 26px 0; }
}

/* ============================================================
   CH03 · THE MODS — platform-led
   ------------------------------------------------------------
   The chapter's subject is Project Tango, the business — not the two
   products. A full-bleed cover band carries the platform copy; the two
   mod marks ride top-right and are the only place the products are
   named; four points sit beneath.
============================================================ */
/* Full-bleed out of the 860px text column, like ch02's rail and ch04's lanes.
   Locked to the column the four points were 194px each and every heading wrapped
   to two lines; this caps the content at 1240 so they land at ~300. */
.mods {
    margin-top: 2.2rem;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    padding-left: max(1.4rem, calc(50vw - 620px));
    padding-right: max(1.4rem, calc(50vw - 620px));
}

/* ---- the hero band ---- */
.mods-hero {
    position: relative; overflow: hidden;
    /* Sized from the measured stack, not guessed: the copy is bottom-anchored
       and ~285px tall in this 780px column, and the marks occupy the top ~95px.
       340 left them overlapping by 24px. Narrowing the copy to dodge them made
       it WORSE — fewer characters per line means more lines means a taller
       block reaching further up. Height is the only lever that helps. */
    min-height: 400px; display: flex; align-items: flex-end;
    border: 1px solid var(--line); border-radius: 13px;
}
.mods-art { position: absolute; inset: 0; }
/* The cover is 16:9 PROMO art carrying its own wordmark. At this band's
   aspect object-fit barely crops vertically, so the wordmark stayed in
   frame and fought the title. Zoom into the car line-up instead. */
.mods-art img {
    width: 100%; height: 100%; object-fit: cover;
    transform: scale(1.85); transform-origin: 50% 86%;
    filter: saturate(0.85) brightness(0.6);
    transition: filter 0.5s var(--ease), transform 0.7s var(--ease);
}
.mods-hero:hover .mods-art img { filter: saturate(1.05) brightness(0.74); transform: scale(1.92); }
.mods-scrim {
    position: absolute; inset: 0; pointer-events: none;
    background: linear-gradient(0deg, rgba(18,15,9,0.97) 12%,
                rgba(18,15,9,0.6) 55%, rgba(18,15,9,0.15) 100%);
}

/* the two product marks — the only place either mod is named */
.mods-marks {
    position: absolute; right: 1.4rem; top: 1.25rem; z-index: 3;
    display: flex; gap: 0.65rem;
}
.mods-marks div {
    width: 132px; aspect-ratio: 16 / 9; overflow: hidden;
    border: 1px solid rgba(196, 181, 160, 0.35); border-radius: 6px;
}
.mods-marks img {
    width: 100%; height: 100%; object-fit: cover;
    filter: saturate(0.95) brightness(0.82);
}

.mods-copy { position: relative; padding: 1.8rem 2rem 1.7rem; max-width: 62ch; }
.mods-eyebrow {
    font-size: 9.5px; letter-spacing: 0.22em; color: var(--amber);
    margin: 0 0 0.6rem;
}
.mods-copy .work-title { font-size: 2rem; line-height: 1.06; margin: 0; }
.mods-desc {
    color: #cfc9bd; font-size: 0.9rem; line-height: 1.68;
    margin: 0.7rem 0 0; max-width: 58ch;
}
.mods-links { display: flex; gap: 1.2rem; margin-top: 1rem; }

/* ---- the four points ---- */
.mods-points {
    display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px;
    margin-top: 1.4rem;
    background: rgba(196, 181, 160, 0.11);
    border: 1px solid rgba(196, 181, 160, 0.11);
    border-radius: 11px; overflow: hidden;
}
.mpt {
    background: #16120a; padding: 1.15rem 1.15rem 1.3rem;
    transition: background 0.3s var(--ease);
}
.mpt:hover { background: #191509; }
.mpt-n {
    display: block; font-size: 9.5px; letter-spacing: 0.2em;
    color: var(--amber); margin-bottom: 0.6rem;
}
.mpt-t {
    font-family: 'Space Grotesk', sans-serif; font-weight: 600;
    font-size: 1rem; letter-spacing: -0.01em; margin: 0 0 0.45rem;
}
.mpt-d { color: var(--text-2); font-size: 0.8rem; line-height: 1.62; margin: 0; }

@media (max-width: 1100px) {
    .mods-points { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 760px) {
    .mods-hero { min-height: 300px; }
    .mods-marks { right: 1rem; top: 1rem; }
    .mods-marks div { width: 96px; }
    .mods-copy { padding: 1.4rem 1.3rem 1.3rem; }
    .mods-copy .work-title { font-size: 1.6rem; }
    .mods-points { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
    /* the marks would sit on top of the title at this width */
    .mods-marks { display: none; }
}
