/* ═══════════════════════════════════════════════════════════════════════════
   SAFEHOUSE LAB — the public design system.
   TERMINAL MEETS MAGAZINE.

   ⛔ NOT in the updater manifest. admin.css ships to every site on the channel;
   this one never does, so a core update can never repaint the front end.

   THE BRIEF (2026-10-17): fluid magazine, beautiful typography, brick wall,
   Watch-Dogs neon — purple, green, acid yellow — a hacker lab that reads like
   a print magazine. The studio where it all comes from, and a gaming hub.

   ⛔ THREE NEONS, EACH WITH EXACTLY ONE MEANING. The house rule is one accent
   per meaning, not one accent total. Break this and it becomes decoration:
       ACID GREEN  → live, active, running, terminal
       MAGENTA     → the machine half. AI, agents, models
       AMBER       → the human half. craft, hands, the studio
   A fourth colour needs a fourth meaning or it does not go in.

   ⛔ EVERY REVEAL STARTS HIDDEN, so all of it is gated on `html.js` (set inline
   before paint) AND cancelled under prefers-reduced-motion. JS off, reduced
   motion, or no IntersectionObserver must all leave the page fully readable.
   tools/verify-motion-safety.php asserts that and has been proven to go red.
   ═══════════════════════════════════════════════════════════════════════════ */

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

:root {
    /* ── Voice. Anton is the poster. Instrument Serif is the magazine.
          JetBrains Mono is the terminal. Inter carries the reading. ──────── */
    --poster:  'Anton', 'Arial Narrow', system-ui, sans-serif;
    --serif:   'Instrument Serif', Georgia, serif;
    --body:    'Inter', system-ui, sans-serif;
    --mono:    'JetBrains Mono', ui-monospace, monospace;

    /* ── Ground. Near-black with a faint violet cast, so neon sits in it
          rather than on it. Brick is warm; the ground is cool; that tension
          is the whole look. ─────────────────────────────────────────────── */
    --void:    #0a0810;
    --ink:     #0e0b15;
    --panel:   #13101d;
    --panel-2: #191427;

    /* ── The three. One meaning each. ─────────────────────────────────── */
    --acid:    #9dff3c;
    --magenta: #c04bff;
    --amber:   #ffd84d;

    --text:    #ece9f5;
    --dim:     #8b86a0;
    --mute:    #514c63;

    --hair:      rgba(255,255,255,.08);
    --hair-acid: rgba(157,255,60,.28);
    --hair-mag:  rgba(192,75,255,.28);
    /* ⚠️ Added because I USED IT BEFORE IT EXISTED. An undefined custom
       property makes the whole declaration invalid, so the numerals'
       "brighten on arrival" silently did nothing — a value that looks like a
       decision and has no effect. Same family as the dead object-position. */
    --line-mag:  rgba(192,75,255,.62);
    /* ⛔ BOTH OF THESE WERE USED AND NEVER DEFINED, AND BOTH FAILED SILENTLY.
       `--line` is the brighter hairline three hover states ask for — every
       one of those hovers was doing nothing. `--nav-h` sits inside a calc()
       for the takeover's top padding, and ONE undefined token invalidates
       the WHOLE declaration, so the menu has had no top padding at all.
       Found by auditing every var() against every definition, which is a
       check I only wrote because I had just invented --line-mag the same
       way. Nothing errors; the rule simply is not there. */
    --line:      rgba(255,255,255,.20);
    --nav-h:     76px;   /* chrome: 32px inset + the 42px dot button + 2 */

    /* ═════ THE SPACING SCALE IS FLUID ═════
       Every one of these was a fixed pixel number, which meant the big
       section gap was 96px on a 27-inch monitor and 96px on a phone. That
       is most of what read as stiff: the type had some flow in it and the
       space around the type had none at all, so the page kept its phone
       rhythm on a desktop and its desktop rhythm on a phone.

       Each step is now a straight line through viewport width:
           0.75x at 390px   —   1.00x at ~1440px   —   1.25x at 2200px
       ⛔ HOLDING ~1440 IS THE POINT. The site was designed at that width
          and he likes how it looks there, so the value barely moves (+4%,
          measured, not estimated). A phone gets tighter, a large monitor
          gets real air, and the width he reviews at stays put. A scale
          that jumped at 1440 would be a redesign wearing the word fluid.
       ⚠️ The floor and ceiling are hard stops — below 390 and above 2200
          nothing keeps shrinking or growing. Unbounded vw is how a
          headline ends up three words tall on a television. */
    --s2:  clamp(6px,   calc(5.14px   + 0.2210vw), 10px);
    --s3:  clamp(9px,   calc(7.71px   + 0.3315vw), 15px);
    --s4:  clamp(12px,  calc(10.28px  + 0.4420vw), 20px);
    --s5:  clamp(18px,  calc(15.41px  + 0.6630vw), 30px);
    --s6:  clamp(24px,  calc(20.55px  + 0.8840vw), 40px);
    --s7:  clamp(36px,  calc(30.83px  + 1.3260vw), 60px);
    --s8:  clamp(48px,  calc(41.10px  + 1.7680vw), 80px);
    --s9:  clamp(72px,  calc(61.66px  + 2.6519vw), 120px);
    --s10: clamp(112px, calc(95.62px  + 4.1989vw), 188px);
    --s11: clamp(165px, calc(141.30px + 6.0773vw), 275px);

    --swift:  180ms cubic-bezier(.4,0,.2,1);
    --glide:  620ms cubic-bezier(.22,.61,.36,1);
    --liquid: 1400ms cubic-bezier(.16,1,.3,1);

    /* The strip above a spread's heading that the section tag sits in. Used
       by BOTH columns of .spread-row so their first lines align. */
    --spread-tag: 2rem;

    /* ⛔ THE PAGE GROWS, THE TEXT COLUMN DOES NOT. Above 1340px nothing
       on this site changed at all, so a 2560 monitor showed a 1340 island
       in a dark field. The wrap now opens to 1560.
       ⚠️ THAT IS ONLY SAFE BECAUSE THE MEASURE IS CAPPED SEPARATELY.
          `.lede` is held at 56ch and `ch` scales with the font, so the
          reading column stays around 70 characters no matter how wide the
          window gets — measured at 390, 820, 1100, 1440, 1920 and 2560,
          all 70. Widen a page without that cap and you get a line a reader
          loses their place on, which looks modern and reads worse. The
          extra width goes to the pictures and the grid, where it belongs. */
    --max: clamp(1340px, calc(923.16px + 28.9474vw), 1560px);
    --wall: none;
}

html { background: var(--void); color-scheme: dark; scroll-behavior: smooth; }
body {
    font-family: var(--body);
    background: var(--void);
    color: var(--text);
    -webkit-font-smoothing: antialiased;
    overflow-x: hidden;
}
img { max-width: 100%; height: auto; display: block; }
[hidden] { display: none !important; }
a { color: inherit; text-decoration: none; }
::selection { background: var(--acid); color: var(--void); }

/* ── GRAIN + SCANLINES. Drawn, never downloaded. The grain stops the page
   looking printed by a machine; the scanlines are the terminal. Both are
   decoration and both stop dead under reduced motion. ───────────────────── */
body::after {
    content: "";
    position: fixed; inset: -120px; z-index: 900;
    pointer-events: none; opacity: .032;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    animation: drift 9s steps(6) infinite;
}
body::before {
    content: "";
    position: fixed; inset: 0; z-index: 899;
    /* ⚠️ Was opacity .5 with multiply over 2px bands — it striped the poster
       type hard enough to read as a broken render rather than a CRT. Scanlines
       are atmosphere: you should feel them, never count them. */
    pointer-events: none; opacity: .22;
    background: repeating-linear-gradient(
        to bottom, transparent 0 3px, rgba(0,0,0,.14) 3px 4px);
}
@keyframes drift {
    0%,100% { transform: translate(0,0); }
    20% { transform: translate(-30px,18px); }
    40% { transform: translate(22px,-26px); }
    60% { transform: translate(-18px,-14px); }
    80% { transform: translate(26px,22px); }
}

/* ── THE MARK + THE DOT. No top bar. He asked for this and kept it. ─────── */
.mark, .dot-btn { position: fixed; z-index: 820; top: var(--s6); }
.mark {
    left: var(--s6);
    font-family: var(--mono);
    font-size: .6rem; letter-spacing: .26em; text-transform: uppercase;
    color: var(--text);
    display: flex; align-items: center; gap: var(--s2);
    mix-blend-mode: difference;
}
/* ⚠️ `.mark i` is GONE with the element it styled. The acid dot used to live
   between the logo and the wordmark and split a three-part lockup into two
   halves; its heartbeat moved onto the mark itself, so the signal survives
   and the lockup reads as one thing.
   ⛔ `@keyframes beat` STAYS — the stream page's live lamp still runs on it.
      Deleting a keyframe because its first user left is how a second,
      unrelated feature stops animating with nothing to show for it. */
@keyframes beat { 0%,100%{opacity:1} 50%{opacity:.35} }

.dot-btn { right: var(--s6); width: 42px; height: 42px; background: none; border: 0; cursor: pointer; display: grid; place-items: center; }
.dot-btn span {
    position: absolute; width: 24px; height: 2px; background: var(--text);
    transition: transform var(--glide), width var(--glide), background var(--swift);
}
.dot-btn span:nth-child(1) { transform: translateY(-6px); }
.dot-btn span:nth-child(2) { transform: translateY(6px); width: 14px; }
.dot-btn:hover span { background: var(--acid); }
.dot-btn:hover span:nth-child(2) { width: 24px; }
.nav-open .dot-btn span:nth-child(1) { transform: rotate(45deg); background: var(--acid); }
.nav-open .dot-btn span:nth-child(2) { transform: rotate(-45deg); width: 24px; background: var(--acid); }

/* ═══════════════════════════════════════════════════════════════════════
   THE TAKEOVER — a magazine CONTENTS SPREAD that boots like a terminal.
   Opens from the dot (he liked that; it stays). Left is the contents, right
   is a live preview that answers whichever section the cursor is on.
   ═══════════════════════════════════════════════════════════════════════ */
.takeover {
    position: fixed; inset: 0; z-index: 810;
    background: var(--ink);
    display: grid;
    grid-template-rows: 1fr auto;
    padding: calc(var(--nav-h) + var(--s8)) var(--s6) var(--s6);
    clip-path: circle(0% at calc(100% - 44px) 44px);
    transition: clip-path var(--liquid);
    visibility: hidden;
    overflow: hidden;
}
.nav-open .takeover { clip-path: circle(150% at calc(100% - 44px) 44px); visibility: visible; }
.takeover::before {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    background-image: var(--wall); background-size: cover; background-position: center;
    opacity: .1; filter: grayscale(.5);
}
/* ⚠️ STILL TWO COLUMNS, AND THE SECOND ONE IS DELIBERATELY EMPTY. It held
   the preview card; now it holds nothing and the artwork shows through it.
   That is what keeps the list inside the left 55% where the wash is heavy
   and the collages are blank. Collapse it to one column and seven large
   words move out across the brightest part of every picture. */
.takeover-grid {
    position: relative;
    max-width: var(--max); width: 100%; margin: 0 auto;
    display: grid; grid-template-columns: minmax(0,1.25fr) minmax(0,1fr);
    gap: var(--s8); align-items: center;
}

/* ── The contents list ─────────────────────────────────────────────────── */
.takeover-list { position: relative; }
.takeover-list a {
    position: relative;
    /* ⚠️ FOUR columns. The blurb is a fourth child, and with three tracks it
       wrapped onto an implicit second ROW in the 3.2rem number column —
       "notes from the machine room" rendered one word per line down the far
       left. A grid does not warn you; it just wraps. */
    display: grid; grid-template-columns: 3.2rem auto auto 1fr; align-items: center;
    gap: var(--s5);
    padding: .22em 0;
    font-family: var(--poster);
    font-size: clamp(2rem, calc(16.55px + 3.9619vw), 6.210rem);
    letter-spacing: .005em; line-height: 1.02;
    text-transform: uppercase; color: var(--text);
    opacity: .46;
    transition: opacity var(--swift), color var(--swift), transform var(--glide);
}
.takeover-list a:hover,
.takeover-list a:focus-visible { opacity: 1; color: var(--acid); transform: translateX(14px); }
.takeover-list a small {
    font-family: var(--mono); font-size: .58rem; letter-spacing: .2em;
    color: var(--magenta); transition: color var(--swift);
}
.takeover-list a:hover small { color: var(--acid); }
/* You are here. A menu that cannot say where you are is half a menu. */
.takeover-list a.is-here { opacity: 1; color: var(--amber); }
.takeover-list a.is-here small { color: var(--amber); }
.takeover-list a.is-here .nav-ico { opacity: 1; }
.js.nav-open .takeover-list a.is-here { opacity: 1; }
/* the rule that draws under the hovered row */
.takeover-list a::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: .06em;
    height: 1px; background: var(--acid);
    transform: scaleX(0); transform-origin: left;
    transition: transform var(--glide);
}
.takeover-list a:hover::after { transform: scaleX(1); }

/* ── The drawn section glyph. No icon font, nothing downloaded. ────────── */
.nav-ico { width: 1em; height: 1em; display: block; overflow: visible; }
/* ⛔ NO `vector-effect: non-scaling-stroke` HERE. It pins the stroke to 7 real
   screen pixels at every size — correct against a 48px desktop glyph, and on a
   phone, where the same icon is about 20px, it fills the shape in and every
   row shows an unreadable blob. A stroke on an icon has to scale WITH it:
   7/100 of the box, which is ~3.4px on desktop and ~1.4px on a phone. */
.nav-ico * { fill: none; stroke: currentColor; stroke-width: 7; stroke-linecap: round; stroke-linejoin: round; }
.takeover-list a .nav-ico { opacity: .4; transition: opacity var(--swift), transform var(--glide); }
.takeover-list a:hover .nav-ico { opacity: 1; transform: rotate(90deg); }
.nav-ico .spin { transform-origin: 50% 50%; }
.takeover-list a:hover .nav-ico .spin { animation: icospin 2.4s linear infinite; }
@keyframes icospin { to { transform: rotate(360deg); } }

/* ── The art layer ─────────────────────────────────────────────────────
   ⛔ FULL BLEED, NOT A CARD. The menu rests on brick; hovering a row brings
      that page's hero up behind the whole takeover. It only works because
      every collage leaves its left third empty — the exact column the
      contents list sits in. A picture composed differently would need the
      scrim turned up and would lose most of itself to it.
   ⚠️ The list keeps its own stacking context above this (z-index on
      .takeover-grid), so the art can never swallow a link. */
.takeover-art { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
.takeover-art img {
    /* ⛔ FULL WIDTH, CENTRED — NOT `cover`. Cover scales to whichever side
       needs more, and on a tall window that is the HEIGHT: at 1440x1200 it
       blew the collage up 1.275x and threw away a third of its width — the
       empty third this entire idea rests on. Measured, the top and bottom
       rows then sat on faces at 1.64:1. Keeping the width means the
       composition is identical at every viewport height, and the brick
       simply shows above and below it. */
    position: absolute; left: 0; top: 50%; transform: translateY(-50%) scale(1.04);
    width: 100%; height: auto;
    opacity: 0;
    /* ⛔ No grayscale — the acid and violet are the palette. */
    /* Measured: at .78 the brightest pixel on the open right side was 139 of
       255 — present, but reading as a dark smudge rather than a picture. */
    filter: contrast(1.03) brightness(.94);
    transition: opacity var(--glide), transform var(--liquid);
}
.takeover-art img.on { opacity: 1; transform: translateY(-50%) scale(1); }
/* The wash. Heaviest down the left where seven large words live, and it
   LIFTS when nothing is hovered so the brick is not needlessly darkened. */
.takeover-art::after {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    background:
        linear-gradient(94deg,
            rgba(10,8,16,.97) 0%,
            rgba(10,8,16,.95) 46%,
            rgba(10,8,16,.88) 58%,
            rgba(10,8,16,.46) 72%,
            rgba(10,8,16,.16) 100%),
        /* Top and bottom fade the band's hard edges into the brick.
           ⚠️ This was briefly much deeper, tuned against a measurement that
           turned out to be fiction — the harness could not snap a transition
           on an ::after, so every reading was of UNWASHED artwork. Reverted
           to the simple pair and re-verified green at 820, 1000 and 1200. */
        linear-gradient(to top, rgba(10,8,16,.92) 0%, transparent 26%),
        linear-gradient(to bottom, rgba(10,8,16,.92) 0%, transparent 18%);
    opacity: 0; transition: opacity var(--glide);
}
.nav-art-on .takeover-art::after { opacity: 1; }

/* ── Terminal status bar at the foot of the menu ────────────────────────── */
.takeover-foot {
    position: relative;
    max-width: var(--max); width: 100%; margin: var(--s7) auto 0;
    padding-top: var(--s4); border-top: 1px solid var(--hair);
    display: flex; justify-content: space-between; gap: var(--s5); flex-wrap: wrap;
    font-family: var(--mono); font-size: .54rem;
    letter-spacing: .16em; text-transform: uppercase; color: var(--mute);
}
.takeover-foot a { color: var(--dim); }
.takeover-foot a:hover { color: var(--acid); }
.takeover-foot b { color: var(--acid); font-weight: 400; }

/* Stagger the rows in once the circle is open.
   ⛔ `.js.nav-open`, NOT `.js .nav-open`. BOTH CLASSES LIVE ON <html>, so the
      descendant form demands two separate elements and can never match. The
      first rule below always applied, the second never did — the menu opened
      onto six links at opacity 0. Nothing errored, the markup was correct,
      and "I cannot navigate through the website" is exactly what that looks
      like from the outside. A space between two classes that share an element
      is a silent, total failure. */
.js .takeover-list a { opacity: 0; transform: translateY(26px); }
.js.nav-open .takeover-list a { opacity: .46; transform: none; transition: opacity var(--glide) var(--d,0ms), transform var(--glide) var(--d,0ms); }
.js.nav-open .takeover-list a:hover,
.js.nav-open .takeover-list a:focus-visible { opacity: 1; transform: translateX(14px); }

@media (max-width: 920px) {
    .takeover-grid { grid-template-columns: 1fr; gap: var(--s6); }
    /* ⛔ The art layer is hidden on a phone, not merely unhovered. There is
       no hover, so it could only ever show brick — and a layer that can
       never paint is still bytes the browser decodes. */
    .takeover-art { display: none; }
    /* ⚠️ Three tracks here and FOUR children, so the blurb fell into an
       implicit second row in the 2.4rem number column and rendered one word
       per line down the side. It is placed explicitly instead: its own line
       under the word, starting at the glyph. */
    .takeover-list a { grid-template-columns: 2.4rem auto 1fr; gap: var(--s4) var(--s4); }
    .nav-blurb { grid-column: 2 / -1; margin-left: 0; max-width: 38ch; }
}

/* ── Layout ──────────────────────────────────────────────────────────── */
.wrap    { width: 100%; max-width: var(--max); margin: 0 auto; padding: 0 var(--s6); }
.section { padding: var(--s10) 0; position: relative; }

/* ── TYPE. The poster is enormous on purpose. Magazines are not timid. ──── */
.poster {
    font-family: var(--poster);
    text-transform: uppercase;
    letter-spacing: .004em;
    line-height: .86;
    font-weight: 400;
}
h1.poster { font-size: clamp(3.6rem, calc(-22.03px + 20.4190vw), 22.950rem); }
h2.poster { font-size: clamp(2.4rem, calc(14.63px + 6.0952vw), 8.640rem); }
h3 { font-family: var(--poster); text-transform: uppercase; font-size: clamp(1.3rem, calc(16.05px + 1.2190vw), 2.679rem); letter-spacing: .01em; line-height: 1; }

.serif  { font-family: var(--serif); font-weight: 400; }
.quote  { font-family: var(--serif); font-style: italic; font-size: clamp(1.3rem, calc(14.86px + 1.5238vw), 3.024rem); line-height: 1.32; color: var(--text); }
.lede   { font-size: clamp(1rem, calc(15.05px + 0.2438vw), 1.276rem); line-height: 1.78; color: var(--dim); max-width: 56ch; }
.lede b { color: var(--text); font-weight: 500; }
.small  { font-size: .88rem; line-height: 1.7; color: var(--dim); }

/* The terminal label. Every section is numbered like a magazine spread. */
.tag {
    font-family: var(--mono);
    font-size: .58rem; letter-spacing: .26em; text-transform: uppercase;
    color: var(--acid);
    display: inline-flex; align-items: center; gap: var(--s3);
}
.tag::before { content: "▚"; color: var(--magenta); }
.tag--mag { color: var(--magenta); }
.tag--mag::before { color: var(--acid); }
.tag--amber { color: var(--amber); }
.tag--amber::before { color: var(--magenta); }

/* ── THE HERO. The TYPE is the hero. No photograph behind the headline —
   his note was that the words were small in the corner of a picture. Now the
   words ARE the picture, and the brick is only the room they stand in. ──── */
.hero {
    position: relative; min-height: 100svh;
    display: flex; flex-direction: column; justify-content: center;
    overflow: hidden; isolation: isolate;
    padding: var(--s11) 0 var(--s9);
}
.hero-wall {
    position: absolute; inset: 0; z-index: 0;
    background-image: var(--wall); background-size: cover; background-position: center;
    opacity: .17; filter: grayscale(.55) contrast(1.1);
}
/* ── The hero photograph ──────────────────────────────────────────────────
   ⛔ THE TYPE IS STILL THE HERO. The picture sits UNDER it and is graded so
      the headline keeps its contrast. A photograph that wins over the
      headline has taken the page.
   ⛔ THE PICTURE HAS TO BE COMPOSED FOR THIS LAYOUT, AND NOW IT IS. The
      headline owns the left 57% of the viewport; the third frame we tried
      groups all three on the RIGHT and leaves the loft quiet on the left,
      so nobody is behind the type and nothing has to be cropped or flipped.
      Two earlier frames needed a mirror or lost Glyph's orb entirely —
      that rule is GONE, and it must not come back with a picture that does
      not need it, because it flips whatever is in the slot.
   ⚠️ Contrast is MEASURED, never judged: render the backdrop with
      .hero-body hidden and sample the worst pixel under each line. Change
      the grade, either gradient, or the picture, and measure again. */
.hero-photo { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
.hero-photo img {
    /* ⛔ FULL WIDTH, NATURAL HEIGHT — NOT `object-fit: cover`.
       The hero box is taller than 16:9, so cover can only ever show about
       78% of the picture's width and the rest is thrown away. Uncropped, the
       frame sits at the top at its own ratio and dissolves into the page. */
    position: absolute; top: 0; left: 0;
    width: 100%; height: auto;
    /* ⛔ NO GRAYSCALE. The homepage hero is a collage now, and its acid and
       violet ARE this site's palette — the photo grade would grey out the one
       thing that makes it belong here. Only a gentle lift stays, because the
       type sits on a left third the artwork leaves empty on purpose. */
    filter: contrast(1.03) brightness(.92);
}
/* The scrim. It is a GRADIENT, not a flat wash, so the left column where the
   headline lives is nearly solid and the right stays open enough to read as a
   room with people in it. */
.hero-photo::after {
    content: ""; position: absolute; inset: 0;
    background:
        linear-gradient(96deg,
            rgba(10,8,16,.74) 0%,
            rgba(10,8,16,.66) 26%,
            rgba(10,8,16,.56) 52%,
            rgba(10,8,16,.44) 76%,
            rgba(10,8,16,.58) 100%),
        linear-gradient(to top,
            rgba(10,8,16,1) 0%,
            rgba(10,8,16,1) 18%,
            rgba(10,8,16,.72) 40%,
            rgba(10,8,16,.20) 66%,
            transparent 82%);
}
@media (max-width: 900px) {
    /* ⚠️ No object-position here any more. It was left behind from the cover
       version and did nothing at all once the image stopped being cropped —
       a dead declaration reads as a decision somebody made and checked.
       In portrait the picture is a shallow band at the top, so the headline
       sits BELOW it rather than on it, and it can carry more light. */
    .hero-photo img { filter: grayscale(.34) contrast(1.04) brightness(.72); }
    .hero-photo::after {
        background: linear-gradient(to bottom,
            rgba(10,8,16,.42) 0%,
            rgba(10,8,16,.30) 26%,
            rgba(10,8,16,.86) 48%,
            rgba(10,8,16,1)   64%);
    }
}

.hero-glow { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
.hero-glow::before, .hero-glow::after {
    content: ""; position: absolute; border-radius: 50%; filter: blur(30px);
}
.hero-glow::before {
    width: 62vmax; height: 62vmax; top: -24vmax; right: -16vmax;
    background: radial-gradient(circle, rgba(192,75,255,.17), transparent 62%);
    animation: liquid 28s ease-in-out infinite;
}
.hero-glow::after {
    width: 52vmax; height: 52vmax; bottom: -22vmax; left: -14vmax;
    background: radial-gradient(circle, rgba(157,255,60,.12), transparent 62%);
    animation: liquid 34s ease-in-out infinite reverse;
}
@keyframes liquid {
    0%,100% { transform: translate(0,0) scale(1); }
    33% { transform: translate(-6vmax,5vmax) scale(1.12); }
    66% { transform: translate(5vmax,-4vmax) scale(.92); }
}
.hero-vig {
    position: absolute; inset: 0; z-index: 2; pointer-events: none;
    background: radial-gradient(ellipse 80% 70% at 50% 50%, transparent 40%, rgba(10,8,16,.86) 100%);
}
.hero-body { position: relative; z-index: 3; width: 100%; }

/* HUMAN × AI — the human half amber, the machine half magenta, the cross
   acid because it is the live connection between them. */
.lockup { display: block; }
.lockup .l1, .lockup .l2 { display: block; white-space: nowrap; }
.lockup .human { color: var(--text); }
.lockup .ai    { color: var(--magenta); text-shadow: 0 0 60px rgba(192,75,255,.45); }
.lockup .x {
    display: inline-block;
    color: var(--acid);
    text-shadow: 0 0 44px rgba(157,255,60,.6);
    /* ⚠️ Anton carries no multiplication sign, so this character falls back
       to another family whose metrics sit lower. Pinned explicitly rather
       than left to whatever the fallback decides. */
    font-size: .74em;
    line-height: 1;
    transform: translateY(-.30em);
    padding: 0 .1em 0 0;
}
.js .lockup .x { opacity: 0; transform: translateY(-.30em) scale(.3) rotate(-90deg); }
.js .ready .lockup .x {
    opacity: 1; transform: translateY(-.30em) scale(1) rotate(0deg);
    transition: opacity 700ms cubic-bezier(.16,1,.3,1) 640ms,
                transform 900ms cubic-bezier(.16,1,.3,1) 640ms;
}

.hero-rail {
    display: flex; justify-content: space-between; align-items: flex-end;
    gap: var(--s6); flex-wrap: wrap;
    margin-top: var(--s8);
    padding-top: var(--s5);
    border-top: 1px solid var(--hair);
}
.hero-rail .lede { max-width: 46ch; }
.ticker-line {
    font-family: var(--mono); font-size: .56rem;
    letter-spacing: .2em; text-transform: uppercase; color: var(--mute);
}
.ticker-line b { color: var(--acid); font-weight: 400; }

/* ── MASKED LINE REVEAL ─────────────────────────────────────────────────── */
.reveal { display: block; overflow: hidden; }
.js .reveal > span { display: block; transform: translateY(104%); }
.js .ready .reveal > span,
.js .in .reveal > span,
.js .reveal.in > span { transform: translateY(0); transition: transform 1000ms cubic-bezier(.16,1,.3,1) var(--d,0ms); }

.js .rise { opacity: 0; transform: translateY(34px); }
/* ⛔ ALL THREE FORMS. This rule has now hidden content TWICE in one day.
     .rise.in      — the element carries data-in itself
     .in .rise     — data-in sits on a PARENT so a group reveals together
     .ready .rise  — inside the HERO, which is revealed by data-arrive/.ready
   Round one cost the Dark Zone origin block. Round two cost the hero's own
   lede and status line, which sat at opacity 0 under a visible hairline —
   he saw an empty gap and said "something is not loading right".
   ⛔ The lesson both times: a reveal has TWO halves, the trigger and the
   rule that answers it, and a guard that only checks one half is blind. */
.js .rise.in,
.js .in .rise,
.js .ready .rise { opacity: 1; transform: none; transition: opacity var(--glide) var(--d,0ms), transform var(--glide) var(--d,0ms); }

/* ── THE SPREAD. A magazine grid: an oversized number, a rule, and copy
   that hangs off it. Asymmetric on purpose — a 12-col even grid is what a
   business page does. ───────────────────────────────────────────────────── */
.spread { display: grid; grid-template-columns: 1fr; gap: var(--s6); }
/* ⛔ THIS WAS AN ADJACENCY RULE AND IT BROKE THE MOMENT SOMETHING WAS PUT
   BETWEEN TWO SPREADS. Inserting the EchoBorn feature made the band after it
   `.feature + .spread`, which matched nothing — so section 03 lost its
   margin, its padding AND its rule all at once and sat straight against the
   cards above it. He spotted it; no test could, because every declaration
   here is correct and the selector simply stops applying.
   Third time in one session that a rule keyed to the old container was
   silently lost when a component moved: the numeral flood, the paragraph
   gap, and now the separator itself. The pairs are listed explicitly — a
   new band type must be added here deliberately. */
/* ⛔ ONE SEPARATOR, NOT TWO. This carried `border-top: 1px solid
   var(--hair)` AND `.spread-row::before` drew an acid rule half a gap
   below it, so every join between two rows was a pair of parallel lines
   about 50px apart — five times down the page. Nobody reads that as two
   deliberate rules; it reads as the page being noisy, and it is a real
   part of why he said his eyes were getting lost in this section.
   The acid rule stays, because it is the brand gesture and it animates.
   The plain one goes. The spacing is unchanged: the padding and margin
   did the distance, the border was only ever drawing on top of it. */
.spread + .spread,
.feature + .spread { margin-top: var(--s9); padding-top: var(--s9); }
/* ⚠️ The feature draws its OWN border and its own acid top rule, so it
   takes the gap only — a padding and a second border here would thicken its
   frame. Doubled so the rhythm matches the spread-to-spread distance. */
.spread + .feature { margin-top: calc(var(--s9) * 2); }
/* ⛔ THE HEADING STARTED 35px BELOW THE COPY IT TITLES. The tag sits above
   the h3 inside the head column, so the copy column's first line aligned
   with the TAG and the heading floated under it against nothing. Both
   columns now reserve the same strip for the tag and the tag is lifted out
   of flow into it - one number, used twice, so the two first lines cannot
   drift apart again.
   ⚠️ The head column was also 509px wide for a heading that needs about
   250, which is where the sea of empty space came from. .85fr / 1.25fr
   gives the reading column the width instead. */
.spread-row { display: grid; grid-template-columns: 7rem minmax(0,.85fr) minmax(0,1.25fr); gap: var(--s7); align-items: start; }
.spread-head, .spread-copy { padding-top: var(--spread-tag); }
.spread-head { position: relative; }
.spread-head .tag { position: absolute; top: 0; left: 0; margin-bottom: 0; }
/* ── The index numerals ────────────────────────────────────────────────
   An outlined numeral that FLOODS as its row scrolls in. The outline is the
   resting state and it is always there; the flood is the arrival.
   ⛔ THE OUTLINE IS NEVER HIDDEN. Only the filled copy is clipped, and only
      behind `.js` — so with JavaScript off, or an observer that never fires,
      the numerals read exactly as they did before. A flourish may never hide
      the product, and a number nobody can see is a broken list. */
/* ═════ THE INDEX ═════
   His read: "a little bit 2010". He is right and it is worth naming WHAT
   was dated, because "make it modern" is not a brief. It was two effects
   stacked: a HOLLOW numeral drawn in outline, with a second solid copy
   laid over it behind a GRADIENT MASK that faded out toward the top.
   Outlined display type and a gloss fade are both of that decade, and
   together they made a glowing blob where an index should be.
   What it is now: one flat numeral in the mono face, tabular, sitting on
   a hairline. Precision rather than spectacle — the same register as the
   rest of the site's terminal language, and it stops competing with the
   heading it is supposed to be numbering.
   ⚠️ The arrival is NOT lost with the flood. `.spread-no` already carries
      `.rise`, so it fades and lifts in with everything else; the flood was
      a second mechanism on top of a reveal that was already there. */
.spread-no {
    position: relative;
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    /* ⛔ MEASURED, NOT JUDGED: at 1440 this was 70.4px against a 33.6px
       heading - the INDEX was 2.1x the size of the TITLE it indexes, so the
       eye landed on the number and the section name read as a caption. He
       said the numerals were "a little bit too much" and that is the number
       behind it. They stay big and they still flood; they just stop
       outranking the words. */
    font-size: clamp(1.15rem, calc(15.11px + 0.8562vw), 2.1rem);
    line-height: 1;
    letter-spacing: .08em;
    color: var(--magenta);
    padding-bottom: var(--s3);
}

/* The hairline the index sits on. It used to be a second copy of the
   numeral flooding up behind a gradient; it is a rule now, which is what
   an index in a magazine actually has under it. It draws itself from the
   left once, with the row. */
.spread-no::after {
    content: "";
    position: absolute; left: 0; right: 0; bottom: 0;
    height: 1px; background: var(--line-mag);
    transform-origin: left;
}
.js .spread-no::after { transform: scaleX(0); }
.js .spread.in .spread-no::after {
    transform: scaleX(1);
    transition: clip-path 1100ms cubic-bezier(.16,1,.3,1) var(--d,0ms);
}

/* The scan line: a hairline that draws across the row as it arrives, the
   way an instrument sweeps before it settles. */
.spread-row::before {
    content: ""; position: absolute; left: 0; right: 0; top: calc(var(--s9) * -.5);
    height: 1px; background: linear-gradient(to right, var(--acid), transparent 70%);
    transform-origin: left;
}
.js .spread-row::before { transform: scaleX(0); }
.js .spread.in .spread-row::before {
    transform: scaleX(1);
    transition: transform 1400ms cubic-bezier(.16,1,.3,1) var(--d,0ms);
}
.spread-row { position: relative; }
/* The first spread has no rule above it to sweep along. */
.spread:first-of-type .spread-row::before { display: none; }

@media (prefers-reduced-motion: reduce) {
    /* ⛔ The flood ARRIVES rather than animating — the numeral must still be
       filled, because the fill is how the list reads as a sequence. Only the
       sweep goes, because a line drawing itself is pure motion. */
    .js .spread-no::after { clip-path: inset(0 0 0 0) !important; transition: none !important; }
    .js .spread-row::before { transform: none !important; transition: none !important; }
}
/* The section title is the biggest thing in its row again. Raised only
   inside a spread, so every other h3 on the site is untouched. */
.spread-head h3 { margin-bottom: var(--s4); font-size: clamp(1.7rem, calc(20.07px + 1.8286vw), 3.769rem); }
.spread-head .tag { margin-bottom: var(--s4); }
.spread-copy p + p { margin-top: var(--s4); }
.spread-meta { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s5); }
.pill {
    font-family: var(--mono); font-size: .52rem;
    letter-spacing: .14em; text-transform: uppercase;
    color: var(--dim);
    border: 1px solid var(--hair); padding: .28rem .7rem;
}
.pill--acid { color: var(--acid); border-color: var(--hair-acid); }
.pill--mag  { color: var(--magenta); border-color: var(--hair-mag); }

/* ── THE TICKER — a terminal feed, not a list of cards ──────────────────── */
.ticker { position: relative; overflow: hidden; border-block: 1px solid var(--hair); padding: var(--s6) 0; }
.ticker-track { display: flex; gap: var(--s9); width: max-content; animation: slide 52s linear infinite; }
.ticker:hover .ticker-track { animation-play-state: paused; }
@keyframes slide { to { transform: translateX(-50%); } }
.ticker-item { display: flex; align-items: baseline; gap: var(--s4); white-space: nowrap; }
.ticker-item b { font-family: var(--poster); text-transform: uppercase; font-size: clamp(1.4rem, calc(16.46px + 1.5238vw), 3.124rem); letter-spacing: .01em; color: var(--text); }
.ticker-item span { font-family: var(--mono); font-size: .54rem; letter-spacing: .2em; text-transform: uppercase; color: var(--acid); }
.ticker::before, .ticker::after { content: ""; position: absolute; top: 0; bottom: 0; width: 16%; z-index: 2; pointer-events: none; }
.ticker::before { left: 0;  background: linear-gradient(to right, var(--void), transparent); }
.ticker::after  { right: 0; background: linear-gradient(to left,  var(--void), transparent); }

/* ── Buttons ────────────────────────────────────────────────────────────── */
.btn {
    display: inline-flex; align-items: center; gap: var(--s3);
    font-family: var(--mono); font-size: .58rem;
    letter-spacing: .18em; text-transform: uppercase;
    padding: 1rem 1.7rem;
    border: 1px solid var(--acid); color: var(--acid);
    background: transparent; cursor: pointer;
    transition: background var(--swift), color var(--swift), transform var(--swift), box-shadow var(--swift);
    will-change: transform;
}
.btn:hover { background: var(--acid); color: var(--void); box-shadow: 0 0 36px rgba(157,255,60,.3); }
.btn::after { content: "→"; transition: transform var(--swift); }
.btn:hover::after { transform: translateX(4px); }
.btn--mag { border-color: var(--magenta); color: var(--magenta); }
.btn--mag:hover { background: var(--magenta); color: var(--void); box-shadow: 0 0 36px rgba(192,75,255,.3); }
/* ⛔ A CONTROL NEEDS AN EDGE YOU CAN SEE. This bordered at
   rgba(255,255,255,.08), which measures 1.18:1 against the ground — the
   box was effectively invisible, so the thing read as a line of grey text
   rather than something to click. His words: "not really visible enough
   to click there".
   ⚠️ THE LABEL WAS NEVER THE PROBLEM. --dim on the ground is 5.70:1 and
      passes comfortably; it is the only part a text-contrast check looks
      at, which is exactly why ten green guards had nothing to say about a
      button nobody could find. A non-text control wants 3:1 for its own
      boundary, and that is a different measurement entirely.
   It now has a visible edge, a surface to sit on, and its label in full
   strength. Hover fills it the way the acid button does. */
.btn--ghost {
    border-color: rgba(255,255,255,.38);        /* 3.49:1 — measured */
    color: var(--text);
    background: var(--panel-2);
}
.btn--ghost:hover {
    background: var(--text); border-color: var(--text); color: var(--void);
    box-shadow: none;
}

/* ═════ THE LEAD STORY ═════
   The contents page set every entry at the same weight, so the piece worth
   reading first was indistinguishable from the fourth one down. A magazine
   never does that. This is the one block on the page allowed to be loud.

   ⚠️ IT IS A LINK WRAPPING BLOCK CONTENT, so every child is inline by
      default and the grid has to be declared on the anchor itself. The
      children are spans rather than divs for the same reason — a <div>
      inside an <a> is legal in HTML5 but the span/grid pairing keeps it
      obvious that this is one target, not a card with things in it.

   ⛔ THE EXCERPT KEEPS THE ARTICLE MEASURE. It is the only body copy in
      the block, and at 1560px wide an uncapped line here would run past
      120 characters — the exact thing the fluid pass was careful about. */
.lead {
    display: grid; grid-template-columns: 7rem minmax(0, 1fr); gap: var(--s7);
    align-items: start;
    padding: var(--s7) 0 var(--s8);
    margin-bottom: var(--s8);
    border-top: 2px solid var(--line-mag);
    border-bottom: 1px solid var(--hair);
    text-decoration: none; color: inherit;
    transition: border-color var(--swift);
}
.lead:hover { border-bottom-color: var(--line); }

.lead-mark { padding-top: .35rem; }
.lead-mark .tag { margin: 0; }

.lead-title {
    font-size: clamp(2rem, calc(26.09px + 1.9810vw), 4.413rem);
    line-height: .96;
    margin: var(--s4) 0 0;
    max-width: 20ch;
    transition: color var(--swift);
}
.lead:hover .lead-title { color: var(--magenta); }

.lead-excerpt {
    display: block; max-width: 52ch; margin-top: var(--s5);
    font-size: clamp(1rem, calc(15.05px + 0.2438vw), 1.276rem);
    line-height: 1.7; color: var(--dim);
}

.lead-meta {
    display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5);
    margin-top: var(--s6);
    font-family: var(--mono); font-size: .54rem; letter-spacing: .18em;
    text-transform: uppercase; color: var(--dim);
}
.lead-meta span + span { position: relative; padding-left: var(--s5); }
.lead-meta span + span::before {
    content: ""; position: absolute; left: 0; top: 50%;
    width: var(--s3); height: 1px; background: var(--line);
}

/* ⚠️ A control's own edge, not just its label — the rule the ghost
   button failed. This one is acid on the ground, well past 3:1. */
.lead-cta {
    display: inline-flex; align-items: center; gap: var(--s3);
    margin-top: var(--s6);
    font-family: var(--mono); font-size: .58rem;
    letter-spacing: .18em; text-transform: uppercase;
    color: var(--acid);
}
.lead-cta::after { content: "\2192"; transition: transform var(--swift); }
.lead:hover .lead-cta::after { transform: translateX(5px); }

@media (max-width: 760px) {
    /* The index column costs more than it gives on a phone: it pushes the
       headline into a column too narrow to be a headline. */
    .lead { grid-template-columns: 1fr; gap: var(--s4); }
    .lead-mark { padding-top: 0; }
}

/* ═════ A JOURNAL ENTRY ═════
   Long-form is the one surface on this site that is READ rather than
   scanned, and it wants different rules from everything around it.

   ⛔ THE MEASURE IS CAPPED AND IT IS NOT OPTIONAL. The page runs to
      1560px on a large monitor; an uncapped paragraph in that would be
      about 150 characters a line, which is roughly double what anyone can
      track on the return sweep. The body sits at 58ch, and `ch` scales
      with the font, so the line stays the same LENGTH in characters while
      the type grows. verify-fluid-scale measures this surface now — a
      reading column nobody measures is a reading column that drifts.

   ⚠️ The subheads use the serif. It is in the design system, it was
      barely used anywhere, and a serif subhead over a sans body is the
      oldest magazine signal there is. It also puts real distance between
      a subhead and the poster face in the title, which stops the page
      reading as one long shout. */
.post-title { max-width: 18ch; }

.post-meta {
    display: flex; flex-wrap: wrap; gap: var(--s3) var(--s5);
    margin-top: var(--s6);
    font-family: var(--mono); font-size: .58rem; letter-spacing: .18em;
    text-transform: uppercase; color: var(--dim);
}
.post-meta span + span { position: relative; padding-left: var(--s5); }
.post-meta span + span::before {
    content: ""; position: absolute; left: 0; top: 50%;
    width: var(--s3); height: 1px; background: var(--line);
}

.post-body { max-width: 58ch; }
.post-body p {
    font-size: clamp(1rem, calc(15.05px + 0.2438vw), 1.276rem);
    line-height: 1.78; color: var(--dim); margin: 0;
}
.post-body p + p { margin-top: var(--s5); }
.post-body strong, .post-body b { color: var(--text); font-weight: 500; }
.post-body em { font-style: italic; }

.post-body h2 {
    font-family: var(--serif); font-weight: 400;
    font-size: clamp(1.5rem, calc(19.41px + 0.9048vw), 2.4rem);
    line-height: 1.15; color: var(--text);
    margin: var(--s8) 0 var(--s5);
}
.post-body h3 {
    font-family: var(--mono); font-size: .62rem; letter-spacing: .2em;
    text-transform: uppercase; color: var(--acid);
    margin: var(--s7) 0 var(--s4);
}
.post-body h2:first-child, .post-body h3:first-child { margin-top: 0; }

/* A quote is the one place the reading column is allowed to be interrupted.
   The rule is magenta because that is the index colour — same accent doing
   the same job, marking something as set apart. */
.post-body blockquote {
    margin: var(--s7) 0; padding-left: var(--s5);
    border-left: 2px solid var(--line-mag);
}
.post-body blockquote p { color: var(--text); font-style: italic; }

.post-body ul, .post-body ol { margin: var(--s5) 0; padding-left: var(--s5); }
.post-body li {
    font-size: clamp(1rem, calc(15.05px + 0.2438vw), 1.276rem);
    line-height: 1.7; color: var(--dim); margin-bottom: var(--s3);
}
.post-body li::marker { color: var(--magenta); }

.post-body a { color: var(--acid); text-decoration: underline; text-underline-offset: .25em; }
.post-body a:hover { color: var(--text); }

.post-body code {
    font-family: var(--mono); font-size: .86em;
    background: var(--panel-2); padding: .15em .45em; color: var(--text);
}
.post-body hr { border: 0; height: 1px; background: var(--hair); margin: var(--s8) 0; }

/* The way out of an article. A reader who finished one wants the next. */
.post-nav {
    display: grid; grid-template-columns: 1fr 1fr; gap: var(--s5);
    margin-top: var(--s9); padding-top: var(--s7);
    border-top: 1px solid var(--hair);
}
.post-nav-link {
    display: grid; gap: var(--s2); padding: var(--s5);
    border: 1px solid var(--hair); background: var(--panel);
    color: var(--text); text-decoration: none;
    transition: border-color var(--swift), background var(--swift), transform var(--glide);
}
.post-nav-link:hover { border-color: var(--line); background: var(--panel-2); transform: translateY(-3px); }
.post-nav-link .mono-s { color: var(--dim); }
.post-nav-link b { font-weight: 500; line-height: 1.3; }
.post-nav-link--end { text-align: right; }

@media (max-width: 620px) {
    .post-nav { grid-template-columns: 1fr; }
    .post-nav-link--end { text-align: left; }
}

/* ── Image plates — magazine figures, wiped in ──────────────────────────── */
.plate { position: relative; overflow: hidden; display: block; background: var(--panel); }
.plate figure { position: relative; overflow: hidden; aspect-ratio: 16/10; }
.plate img {
    width: 100%; height: 100%; object-fit: cover;
    filter: grayscale(.72) contrast(1.06) brightness(.74);
    transform: scale(1.02);
    transition: filter var(--liquid), transform var(--liquid);
}
.plate:hover img { filter: grayscale(0) contrast(1) brightness(.95); transform: scale(1.055); }
/* ⚠️ The house grade is grayscale(.72) brightness(.74), and it only lifts on
   HOVER — which a phone never has. The default state is therefore the state
   most people see, and for a warm portrait that carries a section it is too
   far down. This modifier sits between the two so the picture reads without
   a pointer, and still resolves to full colour for anyone who has one. */
.plate--warm img { filter: grayscale(.34) contrast(1.02) brightness(.88); }
.js .plate figure { clip-path: inset(0 0 100% 0); }
.js .plate.in figure { clip-path: inset(0 0 0 0); transition: clip-path 1100ms cubic-bezier(.16,1,.3,1) var(--d,0ms); }
.plate-flag {
    position: absolute; top: var(--s5); left: var(--s5); z-index: 2;
    font-family: var(--mono); font-size: .5rem;
    letter-spacing: .18em; text-transform: uppercase;
    color: var(--void); background: var(--acid); padding: .26rem .6rem;
}
.plate-flag--mag { background: var(--magenta); color: #fff; }
.plate-flag--amber { background: var(--amber); }

/* ── PAGE HERO — the same room, two thirds the volume. The front door is the
   only thing allowed to fill a screen with type. ───────────────────────── */
.phero {
    position: relative; overflow: hidden; isolation: isolate;
    padding: calc(var(--s11) - 40px) 0 var(--s9);
    border-bottom: 1px solid var(--hair);
}
.phero .hero-wall { opacity: .14; }
.phero-body { position: relative; z-index: 3; }
.phero h1.poster { font-size: clamp(2.8rem, calc(7.95px + 9.4476vw), 12.150rem); }
.phero .lede { margin-top: var(--s6); }

/* ── TEAM — a roster, not a gallery of circles ──────────────────────────── */
/* ⚠️ The old .roster rules are GONE with the markup they styled — six
   dossier cards replaced the plain name-and-role grid. Seven dead rules
   that still parsed and could never apply again. */

/* ── TESTIMONIAL — one voice, given room ────────────────────────────────── */
.voice { display: grid; grid-template-columns: 92px minmax(0,1fr); gap: var(--s6); align-items: start; }
.voice img { width: 92px; height: 92px; object-fit: cover; filter: grayscale(.3); border: 1px solid var(--hair-mag); }
.voice-by { margin-top: var(--s5); font-family: var(--mono); font-size: .56rem; letter-spacing: .16em; text-transform: uppercase; color: var(--dim); }
.voice-by b { color: var(--amber); font-weight: 400; display: block; margin-bottom: 4px; }

/* ── STREAM — the embed, and an honest offline card ─────────────────────
   ⛔ A cross-origin player cannot be styled, only not loaded. Off air, Twitch
   fills the frame with other people's channels, so the page decides on the
   SERVER not to embed it and draws our own card instead. */
.screen { position: relative; aspect-ratio: 16/9; background: var(--ink); border: 1px solid var(--hair); overflow: hidden; }
.screen iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.screen-off { position: absolute; inset: 0; display: grid; place-content: center; justify-items: center; gap: var(--s4); text-align: center; padding: var(--s6); }
.screen-off .lamp { width: 10px; height: 10px; background: var(--mute); border-radius: 50%; }
.screen-off.is-live .lamp { background: var(--acid); box-shadow: 0 0 18px var(--acid); animation: beat 1.4s ease-in-out infinite; }
.screen-scan { position: absolute; inset: 0; pointer-events: none; background: repeating-linear-gradient(to bottom, transparent 0 3px, rgba(157,255,60,.03) 3px 4px); }

/* ── SUPPORT — tiers as terminal rows ───────────────────────────────────── */
/* ⚠️ .tiers / .tier were the first version of this and are GONE, not left
   behind - no markup on the site uses them any more. Dead CSS is drift: the
   next person reads it as a component that exists and styles against it. */

/* ── JOURNAL — an index, set like a contents page ───────────────────────── */
.index-row { display: grid; grid-template-columns: 7rem minmax(0,1fr) auto; gap: var(--s6); align-items: baseline; padding: var(--s6) 0; border-bottom: 1px solid var(--hair); transition: background var(--swift); }
a.index-row:hover { background: rgba(157,255,60,.035); }
a.index-row:hover h3 { color: var(--acid); }
.index-row h3 { transition: color var(--swift); }
.index-row .when { font-family: var(--mono); font-size: .54rem; letter-spacing: .16em; text-transform: uppercase; color: var(--mute); }
.index-row .excerpt { font-size: .86rem; color: var(--dim); margin-top: var(--s2); max-width: 62ch; }

/* ── An honest empty state. Quiet, never a fault. ───────────────────────── */
.empty { border: 1px dashed var(--hair); padding: var(--s8); text-align: center; color: var(--mute); font-family: var(--mono); font-size: .6rem; letter-spacing: .16em; text-transform: uppercase; }

@media (max-width: 760px) {
    .voice { grid-template-columns: 1fr; }
    .index-row { grid-template-columns: 1fr; gap: var(--s2); }
}

/* ── Footer ─────────────────────────────────────────────────────────────── */
.foot { border-top: 1px solid var(--hair); padding: var(--s8) 0 var(--s7); }
.foot-row { display: flex; justify-content: space-between; gap: var(--s6); flex-wrap: wrap; align-items: center; }
.foot a { color: var(--dim); font-size: .84rem; }
.foot a:hover { color: var(--acid); }
.foot-links { display: flex; gap: var(--s6); flex-wrap: wrap; }
.mono-s { font-family: var(--mono); font-size: .54rem; letter-spacing: .18em; text-transform: uppercase; color: var(--mute); }

/* ── Responsive ─────────────────────────────────────────────────────────── */
@media (max-width: 1000px) {
    .spread-row { grid-template-columns: 1fr; gap: var(--s5); }
    .spread-no { font-size: 2.6rem; }
}
@media (max-width: 900px) {
    .section { padding: var(--s9) 0; }
    .mark, .dot-btn { top: var(--s5); }
    .mark { left: var(--s5); }
    .dot-btn { right: var(--s5); }
    .takeover { padding: var(--s9) var(--s5); }
    .hero { padding-top: var(--s10); }
    .hero-rail { gap: var(--s5); }
}

/* ⛔ THE SAFETY GATE. Without JS the `.js` class never lands, so nothing above
   that hides anything can apply. With reduced motion every hidden state is
   cancelled outright and the decoration stops. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
        scroll-behavior: auto !important;
    }
    .js .reveal > span,
    .js .rise,
    .js .lockup .x,
    .js .takeover-list a { opacity: 1 !important; transform: none !important; }
    .js .plate figure { clip-path: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE LAB PAGE — the travelling studio
   ═══════════════════════════════════════════════════════════════════════ */

/* Copy beside a picture. ⚠️ `align-items:start` on purpose: without it the
   text column stretches to the plate's height and its first line stops
   sitting on the plate's top edge, which is the only alignment anyone reads. */
.lab-split {
    display: grid; grid-template-columns: 1fr 1fr;
    gap: var(--s7); align-items: start;
}
.lab-split--flip .plate { order: -1; }
/* ⚠️ `p + p` spacing exists ONLY inside .spread-copy. Prose dropped anywhere
   else on this site runs together with no gap at all, which reads as one
   long paragraph that happens to break oddly. */
.lab-split p + p { margin-top: var(--s4); }

/* ── Main outposts ─────────────────────────────────────────────────────
   ⛔ STILL NOT A MAP. A drawn world map would have to put a dot at a
      coordinate and claim a precision the page does not have. Each outpost
      carries its own collage instead — a place shown, not a position
      asserted. The art is his, in the site's own palette.
   ⚠️ The collages sit in the top two-thirds of their frame with the bottom
      third already dark, so the label block sits on ground the artwork
      itself provides rather than on a scrim fighting it. */
.outposts { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s5); }
.outpost {
    position: relative; overflow: hidden;
    border: 1px solid var(--hair); border-top: 2px solid var(--acid);
    background: var(--panel);
    transition: border-color var(--swift), transform var(--glide);
}
.outpost:hover { border-color: var(--line); transform: translateY(-4px); }

/* ⚠️ 16/10, NOT 3/2. The source IS 3/2, so a 3/2 frame crops nothing and
   `object-position` there is an inert declaration that reads like a decision
   somebody made. A slightly wider frame crops the empty bottom third of the
   collage — which is the part doing no work — and the position then has
   something real to say. */
.outpost-art { position: relative; margin: 0; overflow: hidden; aspect-ratio: 16/10; }
.outpost-art img {
    width: 100%; height: 100%; object-fit: cover; object-position: 50% 30%;
    /* ⛔ No grayscale. The acid and violet in these collages ARE the palette. */
    filter: contrast(1.03) brightness(.82);
    transform: scale(1.02);
    transition: filter var(--liquid), transform var(--liquid);
}
.outpost:hover .outpost-art img { filter: contrast(1.03) brightness(1); transform: scale(1.07); }
/* The fade the label stands on, drawn here rather than relied on in the art. */
/* ⛔ z-index 3 KEEPS THIS THE TOP LAYER INSIDE THE ART, above both the
   daylight grade and the sweep. Positioned children with a positive
   z-index paint above a positioned pseudo-element with z-index auto, so
   without this the grade would wash the very gradient the city label
   stands on — and a golden-hour card would have lifted the ground under
   its own text while a night card darkened it. Legibility may not depend
   on the hour, exactly as it may not depend on which picture is in the
   slot. Same rule as the standby card. */
.outpost-art::after {
    content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none;
    background: linear-gradient(to top, var(--panel) 0%, rgba(10,8,16,.55) 38%, transparent 72%);
}

.outpost-body {
    position: relative; z-index: 2; margin-top: -1.9rem;
    padding: 0 var(--s5) var(--s5);
    display: flex; flex-direction: column; gap: var(--s2);
}
.outpost-mark { width: 1.5rem; height: 1.5rem; display: block; overflow: visible; color: var(--acid); }
.outpost-mark * {
    fill: none; stroke: currentColor; stroke-width: 6;
    stroke-linecap: round; stroke-linejoin: round;
}
/* The ring drifts up and settles on hover — a mark being PLANTED, which is
   the one thing an outpost card can honestly animate. */
.outpost-mark circle { transition: transform var(--glide); transform-origin: 50% 38%; }
.outpost:hover .outpost-mark circle { transform: translateY(-6px); }
.outpost-no {
    font-family: var(--mono); font-size: .58rem; letter-spacing: .22em; color: var(--magenta);
}
.outpost-city {
    font-family: var(--poster); font-size: clamp(1.05rem, calc(14.42px + 0.6095vw), 1.740rem);
    line-height: 1.05; text-transform: uppercase; color: var(--text);
}
.outpost-where {
    font-family: var(--mono); font-size: .54rem; letter-spacing: .16em;
    /* ⚠️ --dim, not --mute. Measured off real pixels, --mute lands this
       line at 2.20:1 on the panel and AA wants 4.5 — at 8.6px that is not a
       quiet line, it is an unreadable one. --dim is already in the system
       and reads 5.37:1, so nothing new was invented to fix it.
       ⚠️ --mute is used 19 times across this stylesheet and only this one
       has been measured. The rest is owed, not done. */
    text-transform: uppercase; color: var(--dim);
}

/* ══════════ EACH OUTPOST SITS IN ITS OWN DAYLIGHT ══════════
   `data-phase` is written onto the card by the same solar arithmetic that
   drives the clocks on /contact — the sun's real altitude over that city,
   computed from its coordinates. No network, no key, nothing to go stale.

   ⛔ THIS IS THE LICENCE FOR THE WHOLE EFFECT. Decoration may not imply a
      state it cannot know. A warm wash on Tel Aviv while Berlin has gone
      dark is a CLAIM about the world, and it is allowed only because it is
      true. The day it stops being computed, it has to come out.

   ⛔ NO PHASE, NO GRADE. With no script there is no `data-phase` attribute
      and none of these rules match, so the card falls back to the plain
      collage below. The quiet failure is the correct one here: a missing
      wash is an absence, a guessed wash is a lie.

   ⚠️ THE ALPHAS ARE DELIBERATELY LOW and none of these filters touch
      saturation. The acid and the violet in the collages ARE the design
      system, and a grade heavy enough to read as "cinematic" at first
      glance is also heavy enough to eat them. Measured, not eyeballed:
      tools/verify-outpost-sky.php samples the acid out of every graded
      card and fails if the palette has been washed out of the artwork. */
.outpost-grade {
    position: absolute; inset: 0; z-index: 1; pointer-events: none;
    transition: opacity 900ms ease, background 900ms ease;
}
/* ⚠️ THE HIDE IS GATED ON .js, AND THE MOTION GUARD IS RIGHT TO INSIST.
   A bare opacity:0 here failed it, correctly: anything hidden by default
   must prove it can still be revealed with no script running. In this one
   case the honest answer is the reverse — with no script there is no
   phase, so there must be no wash at all. Gating it satisfies both: with
   script and no phase yet it is transparent; with no script it sits at
   opacity 1 carrying NO background, because every background below is
   behind a [data-phase] the script never wrote. Invisible either way, and
   never hiding anything a reader needed. */
.js .outpost-grade { opacity: 0; }    /* nothing until a phase is known */
.outpost[data-phase] .outpost-grade { opacity: 1; }

.outpost[data-phase="day"]      .outpost-grade { background: linear-gradient(to bottom, rgba(130,175,255,.10), rgba(255,250,235,.04)); }
/* ⚠️ GOLDEN IS THE LIGHTEST WASH OF THE FIVE, AND THAT IS NOT TASTE.
   Orange and acid green sit opposite each other, so a warm wash CANCELS
   the acid rather than tinting it: at .20 alpha it took Puerto de la Cruz
   down to 28% of the acid it started with, the worst of any phase,
   including night. Measured, not noticed — on screen it simply looked
   like a nice sunset. Lighter alpha, and the warmth carried by the
   exposure below instead. */
.outpost[data-phase="golden"]   .outpost-grade { background: linear-gradient(to bottom, rgba(255,176,86,.12), rgba(255,130,70,.05)); }
.outpost[data-phase="dusk"]     .outpost-grade { background: linear-gradient(to bottom, rgba(146,92,204,.20), rgba(62,42,94,.16)); }
.outpost[data-phase="twilight"] .outpost-grade { background: linear-gradient(to bottom, rgba(64,54,134,.26), rgba(22,18,46,.26)); }
.outpost[data-phase="night"]    .outpost-grade { background: linear-gradient(to bottom, rgba(24,28,66,.30), rgba(8,8,18,.34)); }

/* The exposure moves with the hour as well as the colour — a graded wash
   over an evenly lit photograph still reads as a filter. Gentle: the base
   is .82 and night only reaches .68, because a card dark enough to be
   convincingly night is also a card whose artwork has stopped being
   visible, and the artwork is what the row is for. */
.outpost[data-phase="day"]      .outpost-art img { filter: contrast(1.05) brightness(.94); }
.outpost[data-phase="golden"]   .outpost-art img { filter: contrast(1.04) brightness(.90) saturate(1.12); }
.outpost[data-phase="dusk"]     .outpost-art img { filter: contrast(1.03) brightness(.80); }
/* ⚠️ THE SATURATE IS PAYING BACK WHAT THE DARKENING COSTS. Dimming
   scales every channel, so it scales the DISTANCE between them too, and
   the acid green goes to mud long before the card looks convincingly
   dark. Measured: night at a flat brightness(.68) dropped the weakest
   card to 57 chroma against a floor of 60. Lifting the floor to fit
   would have been pinning the test to whatever the code happened to do,
   so the grade was fixed instead. Night stays clearly darker than
   twilight; the palette comes back. */
.outpost[data-phase="twilight"] .outpost-art img { filter: contrast(1.02) brightness(.74) saturate(1.15); }
.outpost[data-phase="night"]    .outpost-art img { filter: contrast(1.02) brightness(.66) saturate(1.35); }
.outpost:hover .outpost-art img { filter: contrast(1.05) brightness(1.02); }

/* The drawn pin takes the hour too — but ONLY at golden hour, and that
   restraint is the point. Recolouring it through dusk, twilight and night
   as well looked right in isolation and stripped the acid out of the
   ENTIRE band for most of a European evening: four grey pins, no accent
   anywhere, on a page whose palette is the thing being sold. One accent,
   one meaning. The grade carries the hour; the mark stays ours. */
.outpost[data-phase="golden"] .outpost-mark { color: var(--amber); }

/* ── The sweep ────────────────────────────────────────────────
   One anamorphic streak, once, as the card arrives. ONCE is the whole
   design: a looping glint on four cards is a page that never settles, and
   the house rule is that motion is subtle and nothing is flashy.
   ⛔ NOT `animation-fill-mode: both`. `both` holds an element at its FROM
      state before the delay elapses, which on this fleet has already
      shipped a flourish that held content at opacity 0. `forwards` leaves
      it parked off the right-hand edge, which is where it belongs. */
.outpost-sweep { position: absolute; inset: 0; z-index: 2; pointer-events: none; overflow: hidden; }
.outpost-sweep::before {
    content: ""; position: absolute; top: -60%; bottom: -60%;
    width: 34%; left: 115%;
    transform: skewX(-14deg);
    background: linear-gradient(100deg,
        transparent,
        rgba(236,233,245,.13) 36%,
        rgba(157,255,60,.17) 50%,
        rgba(236,233,245,.13) 64%,
        transparent);
}
.js .outpost.in .outpost-sweep::before {
    animation: outpost-sweep 1150ms cubic-bezier(.22,.7,.2,1) calc(var(--d, 0ms) + 220ms) 1 forwards;
}
@keyframes outpost-sweep { from { left: -40%; } to { left: 115%; } }

/* ── The drift ──────────────────────────────────────────────
   A long settle from 1.085 down to the resting 1.02, once, on arrival.
   ⚠️ NO `forwards` HERE EITHER, and for a different reason: pinning the
      transform would win against the hover rule underneath it and the
      cards would stop responding to a pointer for the rest of the session.
      Letting it release hands the element back to the base rule, which is
      the same value the keyframe ends on, so nothing jumps. */
.js .outpost.in .outpost-art img {
    animation: outpost-drift 2600ms cubic-bezier(.16,.8,.24,1) calc(var(--d, 0ms) + 120ms) 1;
}
@keyframes outpost-drift {
    from { transform: scale(1.085) translate3d(-1.2%, .8%, 0); }
    to   { transform: scale(1.02)  translate3d(0, 0, 0); }
}

/* ── The hour, written out ──────────────────────────────────────
   Both halves are written by script, so the row collapses to nothing when
   there is no script rather than rendering an empty frame or a dash that
   never fills in. */
.outpost-now {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2) var(--s4);
    margin-top: var(--s3); min-height: 0;
}
.outpost-now:empty { display: none; }
.outpost-time {
    font-family: var(--mono); font-size: .72rem; letter-spacing: .1em;
    color: var(--text); font-variant-numeric: tabular-nums;
}
.outpost-phase {
    display: inline-flex; align-items: baseline; gap: .4em;
    font-family: var(--mono); font-size: .54rem; letter-spacing: .14em;
    text-transform: uppercase; color: var(--dim);
}
.outpost-phase i { font-style: normal; color: var(--acid); }
.outpost-phase b { font-weight: 400; }

@media (prefers-reduced-motion: reduce) {
    /* ⛔ THE CRAFT GOES, THE INFORMATION STAYS. The sweep and the drift are
       decoration and are removed outright. The grade, the clock and the
       phase word are facts about the world and are not motion, so they
       remain — switching them off here would be dropping content to
       satisfy a motion preference. */
    .js .outpost.in .outpost-sweep::before,
    .js .outpost.in .outpost-art img { animation: none !important; }
    .outpost-grade { transition: none; }
}

@media (max-width: 900px) {
    .lab-split { grid-template-columns: 1fr; gap: var(--s6); }
    /* ⚠️ The flip is a DESKTOP ordering choice, and `order: 0` would have KEPT
       the picture first in one column — the DOM order is picture-then-text for
       the flipped block. So it has to be pushed explicitly: every section on a
       phone opens with its own heading, never with an unexplained photograph. */
    .lab-split--flip .plate { order: 1; }
    .outposts { grid-template-columns: repeat(2, 1fr); gap: var(--s4); }
}
@media (max-width: 520px) {
    .outposts { grid-template-columns: 1fr; }
}

/* ── A page hero that carries ARTWORK ─────────────────────────────────────
   The collages are drawn pieces, not textures: a subject, torn halftone edges
   and little terminal dialogs that are the whole charm. So they are never
   cropped to fit a box — the piece is shown WHOLE at the hero's height and
   anchored right of the headline, and whatever the hero is wider than simply
   stays page. Cropping a collage throws away the jokes. */
.phero--art .hero-photo img {
    position: absolute; top: 0; right: 0; left: auto;
    height: 100%; width: auto; max-width: none;
    object-fit: fill;             /* the box IS the image's ratio; nothing to fit */
    filter: contrast(1.02) brightness(.80);
}
/* ⛔ No grayscale on these. The acid and violet in the collage ARE the site's
   palette — desaturating them would grey out the one thing that makes the
   set belong to this design system. */
/* ⛔ THE HEADLINE HAS TO YIELD SOME WIDTH, AND HERE IS THE PHYSICS.
   The acid in these collages is #9dff3c — relative luminance about 0.72 —
   and the headline ink is 0.83. Those are near enough identical that white
   type over acid green is roughly 1.1:1 NO MATTER WHAT: there is no scrim
   value that rescues it without blacking the green out entirely. Measured,
   the gaming headline ran 1082px — 81% of the way across — straight through
   the brightest part of the artwork at 2.60:1.
   So the type is smaller on art pages ONLY, enough to finish inside the
   protected column, and the wash holds hard to 58% and then gets out of the
   way so the collage is actually visible. Both numbers were tuned against
   the measurement, not chosen. */
.phero--art h1.poster { font-size: clamp(2.4rem, calc(15.82px + 5.7905vw), 8.370rem); }
.phero--art .hero-photo::after {
    background:
        linear-gradient(94deg,
            rgba(10,8,16,.97) 0%,
            rgba(10,8,16,.96) 40%,
            rgba(10,8,16,.93) 58%,
            rgba(10,8,16,.55) 72%,
            rgba(10,8,16,.22) 86%,
            rgba(10,8,16,.16) 100%),
        linear-gradient(to top,
            rgba(10,8,16,.92) 0%,
            rgba(10,8,16,.30) 24%,
            transparent 52%);
}

@media (max-width: 900px) {
    /* In portrait the artwork becomes a full-width band at the top and the
       type sits under it. ⚠️ The band's height is a function of the viewport
       (100vw ÷ 1.777), so the body is pushed down by 48vw — just inside the
       fade, so the two overlap rather than stacking like two separate blocks. */
    .phero--art .hero-photo img {
        position: absolute; top: 0; left: 0; right: auto;
        width: 100%; height: auto;
        filter: contrast(1.02) brightness(.86);
    }
    /* ⚠️ 48vw was a guess ON TOP of the phero's own top padding, which left
       about 130px of dead page between the artwork and the first line. The
       band's height is exactly 100vw ÷ 1.777 = 56.3vw, so the hero's own top
       padding is removed and the body is pushed down by the band plus one
       gap. Derived, not dialled in. */
    .phero--art { padding-top: 0; }
    .phero--art .phero-body { padding-top: calc(56.3vw + var(--s6)); }
    .phero--art .hero-photo::after {
        background: linear-gradient(to bottom,
            rgba(10,8,16,.30) 0%,
            rgba(10,8,16,.22) 30%,
            rgba(10,8,16,.88) 52%,
            rgba(10,8,16,.97) 68%,
            rgba(10,8,16,1)   100%);
    }
}

/* ── The inline blurb, which the preview card used to carry ───────────── */
.takeover-grid { position: relative; z-index: 2; }   /* above the art layer */
.nav-blurb {
    justify-self: start; align-self: center; max-width: 24ch; margin-left: var(--s5);
    font-family: var(--mono); font-size: .56rem; line-height: 1.5;
    letter-spacing: .16em; text-transform: uppercase; color: var(--dim);
    transition: opacity var(--swift), transform var(--glide);
    /* ⚠️ It occupies its grid cell from the start and only FADES. Revealing
       it with display or height would reflow the row under the cursor —
       the list would twitch every time the pointer crossed it. */
}
/* ⛔ GATED ON .js, like every other hidden thing on this site. The reveal
   here is pure CSS hover, so it would in fact survive JavaScript being off —
   but the guard is right to refuse the exception: an element that starts at
   opacity 0 without the gate is one refactor away from never appearing, and
   ungated it simply shows. */
.js .nav-blurb { opacity: 0; transform: translateX(-8px); }
.js .takeover-list a:hover .nav-blurb,
.js .takeover-list a:focus-visible .nav-blurb { opacity: 1; transform: none; }
.js .takeover-list a.is-here .nav-blurb { opacity: .55; }
@media (max-width: 920px) {
    /* ⛔ No hover on a phone, so a blurb that only appears on hover would
       never appear at all. It is simply always on, at list opacity. */
    .js .nav-blurb { opacity: .5; transform: none; font-size: .5rem; }
}

/* ── A full-colour art plate, and its caption underneath ─────────────────
   ⛔ `.plate--warm` greys its picture by .34 and only lifts on hover. That is
      right for a photograph carrying a section; it is wrong for a collage
      whose colour is the point. This one is left alone. */
.plate--art img { filter: none; }
.plate--art:hover img { filter: none; transform: scale(1.03); }
.plate-note {
    margin-top: var(--s4);
    font-family: var(--mono); font-size: .58rem;
    letter-spacing: .14em; text-transform: uppercase; color: var(--dim);
    line-height: 1.7;
}
.plate-note b { color: var(--acid); font-weight: 400; }

/* ═══════════════════════════════════════════════════════════════════════
   CONTACT — three doors, a headquarters, four clocks, the channels
   ═══════════════════════════════════════════════════════════════════════ */

/* ── The three routes ──────────────────────────────────────────────────── */
.routes { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s5); }
.route {
    position: relative; display: flex; flex-direction: column; gap: var(--s3);
    padding: var(--s6) var(--s5) var(--s5);
    background: var(--panel); border: 1px solid var(--hair);
    border-top: 2px solid var(--magenta);
    transition: border-color var(--swift), transform var(--glide), background var(--swift);
}
.route:hover, .route:focus-visible {
    border-color: var(--line); background: var(--panel-2); transform: translateY(-4px);
}
.route-no   { font-family: var(--mono); font-size: .56rem; letter-spacing: .22em; color: var(--magenta); }
.route-what { font-family: var(--mono); font-size: .58rem; letter-spacing: .16em;
              text-transform: uppercase; color: var(--mute); }
.route-who  { font-family: var(--poster); font-size: clamp(1.15rem, calc(15.73px + 0.6857vw), 1.926rem);
              line-height: 1.05; text-transform: uppercase; color: var(--text); }
.route-why  { font-size: .92rem; color: var(--dim); line-height: 1.65; margin-top: var(--s2); }
.route-go   {
    margin-top: auto; padding-top: var(--s5);
    font-family: var(--mono); font-size: .58rem; letter-spacing: .14em; color: var(--acid);
    display: flex; align-items: center; gap: .6em;
}
/* The arrow moves, the text does not — the row must not reflow on hover. */
.route-go i { font-style: normal; transition: transform var(--glide); }
.route:hover .route-go i { transform: translateX(6px); }

/* ── Headquarters ──────────────────────────────────────────────────────
   A band carrying the artwork, with the copy on the empty third the picture
   leaves.
   ⛔ THE COMMENT HERE USED TO CLAIM A 2/1 CROP THAT THE CSS NEVER APPLIED.
      The height comes from min-height, not aspect-ratio, and the first value
      cropped 67% of the frame — which sliced the top off the SAFEHOUSE
      CONNECTION dialog, the one piece of the artwork that speaks. A false
      claim in a comment is worse than none, because the next reader stops
      checking.
   ⚠️ MEASURED: at 1440 the band is 608px and cover renders the source 810px
      tall, so 202px is lost. object-position 28% puts the cut 57px from the
      top, and the dialog sits at 81–194px of the frame — so it clears by
      24px. Re-derive both numbers if the band's height changes. */
.hq-band { position: relative; overflow: hidden; border: 1px solid var(--hair); }
.hq-art { position: absolute; inset: 0; margin: 0; }
.hq-art img {
    width: 100%; height: 100%; object-fit: cover; object-position: 50% 28%;
    /* ⛔ No grayscale. Same rule as every other piece in this set. */
    filter: contrast(1.03) brightness(.92);
}
/* The wash, heaviest down the left where the words are. */
.hq-art::after {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(94deg,
        rgba(10,8,16,.96) 0%,
        rgba(10,8,16,.93) 38%,
        rgba(10,8,16,.66) 56%,
        rgba(10,8,16,.22) 78%,
        rgba(10,8,16,.12) 100%);
}
.hq-body {
    position: relative; z-index: 2;
    width: min(46%, 34rem);
    padding: var(--s9) var(--s7);
    min-height: min(52vw, 38rem);
    display: flex; flex-direction: column; justify-content: center;
}

@media (max-width: 900px) {
    /* ⛔ In one column the artwork becomes a band ABOVE the copy rather than
       behind it — at 390px the picture's empty third is about 130px wide and
       the heading would sit straight on the building. */
    .hq-band { display: flex; flex-direction: column; }
    .hq-art { position: relative; inset: auto; aspect-ratio: 16/9; }
    .hq-art::after {
        background: linear-gradient(to bottom,
            rgba(10,8,16,.28) 0%, rgba(10,8,16,.20) 54%, rgba(10,8,16,.92) 100%);
    }
    .hq-body { width: 100%; min-height: 0; padding: var(--s6) var(--s5) var(--s7); }
}

/* ── The clocks ────────────────────────────────────────────────────────── */
.zones { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s5); }
.zone {
    display: flex; flex-direction: column; gap: var(--s2);
    padding: var(--s6) var(--s5);
    border: 1px solid var(--hair); background: var(--panel);
    transition: border-color var(--swift), transform var(--glide);
}
.zone:hover { border-color: var(--line); transform: translateY(-3px); }
.zone.is-hq { border-top: 2px solid var(--acid); }
.zone-role  { font-family: var(--mono); font-size: .52rem; letter-spacing: .22em;
              text-transform: uppercase; color: var(--magenta); }
.zone.is-hq .zone-role { color: var(--acid); }
.zone-city  { font-family: var(--poster); font-size: clamp(1rem, calc(13.92px + 0.5333vw), 1.603rem);
              line-height: 1.05; text-transform: uppercase; color: var(--text); }
.zone-where { font-family: var(--mono); font-size: .52rem; letter-spacing: .16em;
              text-transform: uppercase; color: var(--mute); }
.zone-time  {
    margin-top: var(--s4);
    font-family: var(--mono); font-size: 1.45rem; letter-spacing: .06em;
    color: var(--amber); font-variant-numeric: tabular-nums;
    /* ⚠️ tabular-nums so the digits do not jiggle the card once a second.
       It is empty until script fills it — a city with no clock reads as a
       city, where a frozen "--:--" reads as a broken clock. */
}

/* ── Channels ──────────────────────────────────────────────────────────── */
.channels { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 1px;
            background: var(--hair); border: 1px solid var(--hair); }
.channel {
    background: var(--panel); padding: var(--s6) var(--s5);
    display: flex; flex-direction: column; gap: var(--s2);
    transition: background var(--swift);
}
.channel:hover { background: var(--panel-2); }
.channel-name   { font-family: var(--poster); font-size: 1.25rem; line-height: 1.05;
                  text-transform: uppercase; color: var(--text); }
.channel-what   { font-size: .9rem; color: var(--dim); }
.channel-handle { margin-top: var(--s3); font-family: var(--mono); font-size: .56rem;
                  letter-spacing: .14em; color: var(--acid);
                  display: flex; align-items: center; gap: .6em; }
.channel-handle i { font-style: normal; transition: transform var(--glide); }
.channel:hover .channel-handle i { transform: translateX(6px); }

.btn--big { font-size: 1rem; padding: 1.1rem 2rem; }

@media (max-width: 900px) {
    .routes { grid-template-columns: 1fr; }
    .zones  { grid-template-columns: repeat(2, 1fr); gap: var(--s4); }
    .hq     { grid-template-columns: 1fr; gap: var(--s5); }
}
@media (max-width: 520px) {
    .zones { grid-template-columns: 1fr; }
}

/* ── The rest of the House ─────────────────────────────────────────────── */
.house { border-top: 1px solid var(--hair); }
.house-row {
    display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1.4fr) auto;
    gap: var(--s5); align-items: baseline;
    padding: var(--s6) var(--s2);
    border-bottom: 1px solid var(--hair);
    transition: background var(--swift), padding-left var(--glide);
}
.house-row:hover, .house-row:focus-visible { background: var(--panel); padding-left: var(--s5); }
.house-name   { font-family: var(--poster); font-size: clamp(1.1rem, calc(15.22px + 0.6095vw), 1.790rem);
                line-height: 1.05; text-transform: uppercase; color: var(--text); }
.house-what   { font-size: .92rem; color: var(--dim); }
.house-domain { font-family: var(--mono); font-size: .56rem; letter-spacing: .14em;
                color: var(--acid); white-space: nowrap;
                display: flex; align-items: center; gap: .5em; }
.house-domain i { font-style: normal; transition: transform var(--glide); }
.house-row:hover .house-domain i { transform: translate(3px,-3px); }

@media (max-width: 760px) {
    /* ⚠️ Three columns collapse to one, and the domain goes LAST rather than
       keeping its own track — at 390px a nowrap domain in its own column
       squeezes the name to two characters a line. */
    .house-row { grid-template-columns: 1fr; gap: var(--s2); }
}

/* ── The sky on each clock ─────────────────────────────────────────────
   ⛔ EVERY STATE HERE IS COMPUTED, NEVER DECORATIVE. The gradient, the
      disc's height and the stars all follow the sun's real altitude over
      that city. Decoration that implies a state it cannot know is a false
      claim — so if the script never runs, no phase lands and the strip
      stays a plain dark band rather than inventing a sunny afternoon. */
.zone { position: relative; overflow: hidden; }
.zone-sky {
    position: absolute; left: 0; right: 0; top: 0; height: 4.2rem;
    display: block; pointer-events: none; overflow: hidden;
    background: var(--panel-2);
    transition: background 1200ms linear;
}
.zone-sky::after {           /* the horizon hairline the disc sets behind */
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
    background: var(--hair);
}
.zone[data-phase="day"]      .zone-sky { background: linear-gradient(to bottom, #1a2740, #22344f); }
.zone[data-phase="golden"]   .zone-sky { background: linear-gradient(to bottom, #2a2033, #4a2f24); }
.zone[data-phase="dusk"]     .zone-sky { background: linear-gradient(to bottom, #1a1630, #2e1f3a); }
.zone[data-phase="twilight"] .zone-sky { background: linear-gradient(to bottom, #0f0d1e, #171229); }
.zone[data-phase="night"]    .zone-sky { background: linear-gradient(to bottom, #09070f, #0d0b16); }

/* The disc. --sun is 0 at the horizon and 1 high in the sky. */
.zone-disc {
    position: absolute; right: var(--s5);
    bottom: calc(var(--sun, 0) * 2.6rem - .55rem);
    width: 1.1rem; height: 1.1rem; border-radius: 50%;
    transition: bottom 1200ms cubic-bezier(.22,.61,.36,1), background 1200ms linear;
    background: var(--mute);
}
.zone[data-phase="day"]      .zone-disc { background: #ffe9a8; box-shadow: 0 0 18px rgba(255,216,77,.45); }
.zone[data-phase="golden"]   .zone-disc { background: var(--amber); box-shadow: 0 0 22px rgba(255,216,77,.55); }
.zone[data-phase="dusk"]     .zone-disc { background: #c98f6a; box-shadow: 0 0 14px rgba(201,143,106,.4); }
/* Below the horizon it is the moon, so it loses its glow and goes cold. */
.zone[data-phase="twilight"] .zone-disc,
.zone[data-phase="night"]    .zone-disc { background: #cdd3e6; box-shadow: none; }

/* Stars, drawn with one gradient rather than seven elements. They exist
   only where it is actually dark. */
/* ⚠️ No base `opacity: 0` here. The element is created by script, so it
   cannot exist without a phase — and every phase below states its own
   opacity explicitly. An un-gated hide in the stylesheet is what the motion
   guard refuses, and rightly. */
.zone-stars {
    position: absolute; inset: 0;
    transition: opacity 1600ms linear;
    background-image:
        radial-gradient(1.4px 1.4px at 14% 38%, #fff, transparent),
        radial-gradient(1.2px 1.2px at 32% 22%, #e7e3f5, transparent),
        radial-gradient(1.1px 1.1px at 48% 54%, #fff, transparent),
        radial-gradient(1.3px 1.3px at 63% 30%, #dfe6ff, transparent),
        radial-gradient(1px   1px   at 78% 48%, #fff, transparent),
        radial-gradient(1.2px 1.2px at 88% 20%, #eef, transparent);
}
.zone[data-phase="day"]      .zone-stars,
.zone[data-phase="golden"]   .zone-stars,
.zone[data-phase="dusk"]     .zone-stars { opacity: 0; }
.zone[data-phase="twilight"] .zone-stars { opacity: .5; }
.zone[data-phase="night"]    .zone-stars { opacity: .9; animation: twinkle 5.5s ease-in-out infinite; }
@keyframes twinkle { 0%,100% { opacity: .9 } 50% { opacity: .55 } }

/* Room for the sky above the card's own content. */
.zone { padding-top: calc(4.2rem + var(--s5)); }

.zone-phase {
    margin-top: var(--s2);
    display: flex; align-items: center; gap: .5em;
    font-family: var(--mono); font-size: .5rem; letter-spacing: .18em;
    text-transform: uppercase; color: var(--mute);
}
.zone-phase i { font-style: normal; color: var(--acid); }
.zone-phase b { font-weight: 400; }

@media (prefers-reduced-motion: reduce) {
    /* ⛔ The twinkle is the only thing here that loops, so it is the only
       thing cancelled. The disc still moves and the sky still changes —
       those are state, not decoration, and hiding state is not an
       accessibility win. */
    .zone[data-phase="night"] .zone-stars { animation: none; }
}

/* ── The corporate mark in the corner ──────────────────────────────────
   ⚠️ `.mark` carries mix-blend-mode: difference so the chrome stays legible
      over any artwork. That works FOR a pure white logo rather than against
      it: over a bright collage difference renders it dark, over black it
      renders it white. Either way it is visible, which is the whole job. */
.mark-logo {
    width: 17px; height: 17px; display: block; object-fit: contain;
    /* The heartbeat the acid dot used to carry. Same 3.2s beat it always
       had, now belonging to the mark instead of sitting beside it. */
    animation: markbeat 3.2s ease-in-out infinite;
    transition: transform 620ms cubic-bezier(.22,.61,.36,1);
}
@keyframes markbeat {
    0%, 100% { filter: drop-shadow(0 0 3px rgba(157,255,60,.45)); }
    50%      { filter: drop-shadow(0 0 9px rgba(157,255,60,.95)); }
}
/* ⚠️ A 90s spin, ON HOVER ONLY. A logo that spins for ever reads as
   perpetually loading — decoration claiming a state it cannot know. One
   that spins when you point at it is answering you, which is honest and
   is the bit that actually feels like 1998. */
.mark:hover .mark-logo { transform: rotate(360deg); }
@media (max-width: 620px) { .mark-logo { width: 15px; height: 15px; } }
@media (prefers-reduced-motion: reduce) {
    /* ⛔ The beat stops, but the glow does NOT go to zero — it holds at the
       bright end. The signal is "the lab is awake"; cancelling the motion
       must not cancel the meaning. */
    .mark-logo { animation: none; filter: drop-shadow(0 0 6px rgba(157,255,60,.7)); }
    .mark:hover .mark-logo { transform: none; }
}

/* ── The arms, at the foot of every page ───────────────────────────────── */
.crest {
    display: flex; flex-direction: column; align-items: center;
    gap: var(--s3);
    padding: var(--s9) 0 var(--s8);
    border-bottom: 1px solid var(--hair);
    margin-bottom: var(--s6);
    text-align: center;
}
.crest-img {
    width: auto; height: 132px; display: block;
    /* ⚠️ It sits back rather than shouting. A crest at full white next to
       neon type competes with the page; at .62 it reads as a seal, which is
       what it is. It comes up on hover for anyone who looks twice. */
    opacity: .62;
    transition: opacity var(--liquid), transform var(--liquid);
}
.crest:hover .crest-img { opacity: 1; transform: translateY(-3px); }
.crest-motto {
    font-family: var(--serif); font-style: italic;
    font-size: clamp(.95rem, calc(14.01px + 0.3048vw), 1.295rem);
    color: var(--text); letter-spacing: .04em; margin: 0;
}
.crest-gloss {
    font-family: var(--mono); font-size: .52rem;
    letter-spacing: .22em; text-transform: uppercase;
    color: var(--mute); margin: 0;
}
@media (max-width: 620px) {
    .crest { padding: var(--s8) 0 var(--s7); }
    .crest-img { height: 104px; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE ROSTER — six dossier cards
   ⛔ His posters already carry the name and the role in the artwork, so the
      live text beneath does NOT repeat them for decoration: it carries what
      a picture cannot — the full legal name, and whether an agent runs.
   ═══════════════════════════════════════════════════════════════════════ */
.dossier { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s5); }

.dossier-card {
    position: relative;
    border: 1px solid var(--hair);
    background: var(--panel);
    display: flex; flex-direction: column;
    transition: border-color var(--swift), transform var(--glide);
}
.dossier-card:hover { border-color: var(--line); transform: translateY(-5px); }
.dossier-card.is-agent { border-top: 2px solid var(--magenta); }
.dossier-card:not(.is-agent) { border-top: 2px solid var(--acid); }

.dossier-art { position: relative; margin: 0; overflow: hidden; aspect-ratio: 3/4; }
.dossier-art img {
    width: 100%; height: 100%; object-fit: cover;
    /* ⛔ No grayscale. These are the collages and their colour is the palette. */
    filter: contrast(1.02) brightness(.88);
    transform: scale(1.02);
    transition: filter var(--liquid), transform var(--liquid);
}
.dossier-card:hover .dossier-art img { filter: contrast(1.02) brightness(1); transform: scale(1.06); }

/* The deal-in: each card wipes up from its own bottom edge, staggered, as the
   row arrives — a hand being dealt rather than six things fading at once.
   ⛔ Gated on .js so a card is never hidden without a script to reveal it. */
.js .dossier-art img { clip-path: inset(100% 0 0 0); }
.js .dossier.in .dossier-art img {
    clip-path: inset(0 0 0 0);
    transition: clip-path 1000ms cubic-bezier(.16,1,.3,1) var(--d,0ms),
                filter var(--liquid), transform var(--liquid);
}

/* A hairline that sweeps down the card once as it lands — the scan of a
   dossier being read, not a loop. */
/* ⚠️ A ::after, not a <span>. It is pure decoration, and the motion guard
   correctly refuses a real element hidden with no script to reveal it —
   while gating it on .js would leave a bright line parked at the top of
   every card for a no-JS visitor. A pseudo-element is the honest home for
   something that is only ever a flourish. */
.dossier-art::after {
    content: ""; position: absolute; left: 0; right: 0; top: 0; height: 2px;
    background: linear-gradient(to right, transparent, var(--acid), transparent);
    opacity: 0; pointer-events: none;
}
.js .dossier.in .dossier-art::after { animation: dossierscan 1500ms ease-out var(--d,0ms) 1; }
@keyframes dossierscan {
    0%   { opacity: 0;  transform: translateY(0); }
    12%  { opacity: .9; }
    85%  { opacity: .5; }
    100% { opacity: 0;  transform: translateY(42vh); }
}

.dossier-body { padding: var(--s5); display: flex; flex-direction: column; gap: var(--s2); }
.dossier-kind {
    display: flex; align-items: center; gap: .6em;
    font-family: var(--mono); font-size: .5rem; letter-spacing: .22em;
    text-transform: uppercase; color: var(--mute);
}
.dossier-dot {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--acid); box-shadow: 0 0 10px var(--acid);
}
/* ⛔ In build is AMBER AND STILL, not acid and pulsing. A live dot that beats
   says "running"; this one must not, because it is not. */
.is-build .dossier-dot { background: var(--amber); box-shadow: none; }
.is-build .dossier-kind b { color: var(--amber); }
.dossier-kind b { font-weight: 400; color: var(--acid); }

.dossier-name { font-size: clamp(1.05rem, calc(15.31px + 0.3810vw), 1.481rem); line-height: 1.1; margin: 0; }
.dossier-role {
    font-family: var(--mono); font-size: .54rem; letter-spacing: .16em;
    text-transform: uppercase; color: var(--magenta);
}
.dossier-what { font-size: .9rem; color: var(--dim); line-height: 1.6; margin-top: var(--s2); }

@media (max-width: 920px) { .dossier { grid-template-columns: repeat(2, 1fr); gap: var(--s4); } }
@media (max-width: 560px) { .dossier { grid-template-columns: 1fr; } }

@media (prefers-reduced-motion: reduce) {
    /* The deal ARRIVES; the scan does not run at all. */
    .js .dossier-art img { clip-path: inset(0 0 0 0) !important; transition: none !important; }
    .js .dossier.in .dossier-art::after { animation: none; opacity: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE GAMING PAGE — the tradition, and the room it happens in
   ═══════════════════════════════════════════════════════════════════════ */

/* ── The seam rule. Two place names with a drawn hairline between them and
      a notch where the worlds meet. ETCH law 1: drawn, never downloaded —
      one rotated square and two borders, no icon, no request.
   ⛔ `aria-hidden` is NOT used on the labels. They are the only thing on the
      page that says WHERE each half of the picture is, and the picture
      itself is decorative to a screen reader. Hiding them would delete the
      information rather than decorate it. ────────────────────────────── */
.worlds-rule {
    display: flex; align-items: center; gap: var(--s4);
    margin-bottom: var(--s5);
    font-family: var(--mono); font-size: .56rem;
    letter-spacing: .18em; text-transform: uppercase;
}
.worlds-rule span { color: var(--dim); white-space: nowrap; }
.worlds-rule i { flex: 1; height: 1px; background: var(--hair); display: block; }
.worlds-rule b {
    width: 7px; height: 7px; flex: none;
    background: var(--amber); transform: rotate(45deg);
}

/* ── The reveal. The picture is two worlds meeting at a torn seam, so it
      opens FROM that seam outward rather than wiping up from the floor
      like every other plate on the site.
   ⚠️ The source seam is not a straight line down the middle — it runs
      diagonally, roughly 46% at the top to 55% at the foot. So the image is
      NOT split into two elements and butted back together; it is one
      picture, uncut, with the clip opening symmetrically. Splitting it
      would have put a CSS edge through the artist's own edge. ────────── */
.js .plate--worlds figure { clip-path: inset(0 50% 0 50%); }
.js .plate--worlds.in figure {
    clip-path: inset(0 0 0 0);
    transition: clip-path 1500ms cubic-bezier(.16,1,.3,1) var(--d,0ms);
}

/* ── The machine room plate. Corner brackets, drawn as two borders each.
   ⚠️ Decoration only, and it may not imply a state it cannot know: there is
      no lamp here and no "live" mark. The page cannot see the channel, so
      it does not get to suggest it. That belongs on /stream, which asks. */
/* ⚠️ FIRST VERSION SAT ON THE PHOTOGRAPH AND COULD NOT BE SEEN. A 1px line
      at --hair-acid (.28 alpha) is correct against the near-black ground and
      invisible over a bright, busy collage — I had to sample the pixels to
      find out, because at a glance "subtle" and "absent" look the same. A
      decoration nobody can see is not restraint, it is dead code.
   ⛔ So the brackets sit OUTSIDE the plate, on the ground they were designed
      for, and the plate gets `overflow: visible` to let them out. */
.deck-plate { position: relative; overflow: visible; }
.deck-plate::before,
.deck-plate::after {
    content: ''; position: absolute; width: 28px; height: 28px;
    border: 1px solid var(--acid); opacity: .5;
    pointer-events: none; z-index: 2;
}
.deck-plate::before { top: -9px;    left: -9px;  border-right: 0; border-bottom: 0; }
.deck-plate::after  { bottom: -9px; right: -9px; border-left: 0;  border-top: 0; }

/* ── The quiet line. One sentence, serif, wider than the mono notes and
      narrower than the column — the page's only piece of writing that is
      not about a product. ─────────────────────────────────────────────── */
.worlds-line {
    font-family: var(--serif); font-size: clamp(1.25rem, calc(16.43px + 0.9143vw), 2.284rem);
    line-height: 1.45; color: var(--text);
    max-width: 30ch; margin: var(--s6) 0 0;
}
.worlds-line em { color: var(--amber); font-style: normal; }

@media (max-width: 720px) {
    .worlds-rule { font-size: .5rem; gap: var(--s3); letter-spacing: .12em; }
    .deck-plate::before, .deck-plate::after { width: 16px; height: 16px; }
}

/* ── THE FEATURE BAND. One section on a page may carry more weight than the
      others; on /gaming that is EchoBorn, because it is the only thing there
      that is live and takes money. Everything else on that page is a work in
      progress or a piece of history, and three identical spreads gave all
      three the same voice.
   ⚠️ The weight is scale and ground, NOT another photograph. ───────────── */
.feature {
    position: relative;
    margin-top: var(--s9);
    padding: var(--s8) var(--s7);
    background: linear-gradient(180deg, var(--panel) 0%, var(--ink) 100%);
    border: 1px solid var(--hair);
}
/* The featured mark: one acid hairline along the top edge, drawn, no icon. */
.feature::before {
    content: ''; position: absolute; top: -1px; left: -1px; right: -1px;
    height: 1px; background: linear-gradient(90deg, var(--acid), transparent 62%);
}
.feature-head {
    display: flex; align-items: flex-end; justify-content: space-between;
    gap: var(--s6); flex-wrap: wrap; margin-bottom: var(--s7);
}
.feature-lede { display: flex; align-items: flex-start; gap: var(--s6); }
.feature-title {
    font-size: clamp(2rem, calc(23.68px + 2.1333vw), 4.413rem); line-height: .96;
    margin-top: var(--s4);
}
/* ⛔ The numeral is the spreads' component reused, so its flood has to be
   taught this container as well — it is gated on `.spread.in`, and a band
   that is not a .spread would have shown a hollow outline that never fills.
   Exactly the bug this page already had on all four of its numerals. */
.feature.in .spread-no { -webkit-text-stroke-color: var(--line-mag); }
.js .feature.in .spread-no::after {
    clip-path: inset(0 0 0 0);
    transition: clip-path 1100ms cubic-bezier(.16,1,.3,1) var(--d,0ms);
}

.feature-grid {
    display: grid; grid-template-columns: 1.35fr 1fr; gap: var(--s8);
    align-items: start;
}
/* The spreads declare this on .spread-copy, and the feature is not a spread
   — so the two paragraphs ran together with no gap at all. Second time a
   component moved into a new container and quietly lost a rule that was
   keyed to the old one; the numeral flood was the first, one block above. */
.feature-copy p + p { margin-top: var(--s4); }
.feature-engines { display: grid; gap: var(--s4); }
.engine {
    border: 1px solid var(--hair); border-left: 2px solid var(--hair-acid);
    padding: var(--s5);
    transition: border-left-color var(--swift), background var(--swift);
}
.engine:hover { border-left-color: var(--acid); background: rgba(157,255,60,.03); }
.engine b {
    display: block; font-family: var(--mono); font-size: .62rem;
    letter-spacing: .22em; color: var(--acid); margin-bottom: var(--s3);
}
.engine span { display: block; font-size: .86rem; line-height: 1.6; color: var(--dim); }

@media (max-width: 900px) {
    .feature { padding: var(--s7) var(--s5); }
    .feature-grid { grid-template-columns: 1fr; gap: var(--s6); }
    .feature-lede { gap: var(--s4); }
}

/* ── THE RECORD. A credits list, not a brag wall. Each row is a partner and
      one line saying what the work actually was.
   ⛔ THE DOCTRINE APPLIES HARDER HERE THAN ANYWHERE ELSE ON THE SITE. These
      rows name three real companies, so every word is a claim somebody could
      be asked to stand behind. No superlatives, no "first ever", no "the
      only" — "among the first wave" is both true and unarguable, where "the
      first" invites one screenshot and a correction. Restraint reads as
      strength, and a modest claim nobody can knock down is worth more than a
      big one somebody can.
   ⚠️ Each row can carry a link. Until it does it renders as plain text and
      NOT as a dead anchor — the same rule the contact page follows. ────── */
.credits { display: grid; margin-top: var(--s6); }
.credit {
    display: grid; grid-template-columns: 15rem minmax(0,1fr);
    gap: var(--s6); align-items: baseline;
    padding: var(--s5) 0;
    border-top: 1px solid var(--hair);
}
.credit:last-child { border-bottom: 1px solid var(--hair); }
.credit-who {
    font-family: var(--mono); font-size: .62rem;
    letter-spacing: .18em; text-transform: uppercase;
    color: var(--text);
    position: relative; padding-left: var(--s5);
}
/* The tick. Drawn, 1px, and it is the only thing that moves on hover. */
.credit-who::before {
    content: ''; position: absolute; left: 0; top: .42em;
    width: 9px; height: 1px; background: var(--acid);
    transition: width var(--swift);
}
.credit:hover .credit-who::before { width: 16px; }
.credit-what { font-size: .95rem; line-height: 1.65; color: var(--dim); }
.credit-what b { color: var(--text); font-weight: 500; }
.credit-what em { font-style: italic; color: var(--text); }
.credit-when {
    display: block; margin-top: var(--s2);
    font-family: var(--mono); font-size: .54rem;
    letter-spacing: .16em; color: var(--mute);
}

@media (max-width: 760px) {
    .credit { grid-template-columns: 1fr; gap: var(--s3); }
    .credit-what { font-size: .9rem; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE STREAM PAGE — the lineup
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Two drivers and a rotation, and the grid says which is which before a
      word is read: the mains take two columns each, the rest take one.
   ⚠️ Weight here is SIZE, not colour. Colour on this site carries meaning
      (acid = live/ours, magenta = the house, amber = a mark), and spending
      it on "this one is bigger" would make the real acid mean less. ───── */
.lineup {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--s4);
}
.lane {
    position: relative;
    display: flex; flex-direction: column; gap: var(--s3);
    padding: var(--s5);
    background: var(--panel);
    border: 1px solid var(--hair);
    overflow: hidden;
    transition: border-color var(--swift), background var(--swift), transform var(--glide);
}
.lane--main { grid-column: span 2; padding: var(--s6); }
/* The open slot is a STATEMENT, not an item, so it spans the row as a thin
   bar. As a normal card it sat alone on a third row with three empty cells
   beside it, which read as a layout that had run out rather than as a list
   that is deliberately unfinished. */
.lane--open {
    grid-column: 1 / -1;
    background: none; border-style: dashed;
    flex-direction: row; align-items: center; gap: var(--s4);
    padding: var(--s4) var(--s5);
}
/* The hierarchy is STATED as well as drawn. He said it plainly - Warframe and
   the builds carry most of it - and a reader should not have to infer that
   from one card being wider than another.
   This block spent one render matching NOTHING. A blind first-match replace
   left the previous rule's selector dangling in front of this comment, and
   CSS allows a comment inside a selector and strips it - so the selector
   quietly became a descendant of a lane that has no tier label. No error
   anywhere. Found by reading the computed style, not by looking at the page:
   the colour from the more specific rule below still landed, which was
   enough to make a dead rule look deliberate. */
.lane-tier {
    font-family: var(--mono); font-size: .5rem;
    letter-spacing: .2em; text-transform: uppercase;
    color: var(--mute);
}
.lane--main .lane-tier { color: var(--magenta); }
.lane--open .lane-name { font-size: .95rem; white-space: nowrap; }
.lane--open .lane-line { margin-left: auto; text-align: right; }

/* The sweep. One drawn hairline that crosses the card on hover, in place of
   a lift-and-shadow — this design system has no shadows anywhere. */
.lane::after {
    content: ''; position: absolute; left: 0; right: 0; top: 0; height: 1px;
    background: linear-gradient(90deg, transparent, var(--acid), transparent);
    transform: translateX(-100%);
    transition: transform 900ms cubic-bezier(.22,.61,.36,1);
}
.lane:hover, .lane:focus-within { border-color: var(--hair-acid); background: var(--panel-2); }
.lane:hover::after, .lane:focus-within::after { transform: translateX(100%); }

.lane-mark { color: var(--mute); font-size: 26px; transition: color var(--swift), transform var(--glide); }
.lane--main .lane-mark { font-size: 38px; color: var(--dim); }
.lane:hover .lane-mark, .lane:focus-within .lane-mark { color: var(--acid); transform: rotate(90deg); }

.lane-name {
    font-family: var(--poster);
    font-size: clamp(1.05rem, calc(15.02px + 0.4571vw), 1.567rem);
    line-height: 1; letter-spacing: .01em;
    color: var(--text);
}
.lane--main .lane-name { font-size: clamp(1.5rem, calc(19.84px + 1.0667vw), 2.707rem); }
.lane-line { font-size: .84rem; line-height: 1.6; color: var(--dim); margin: 0; }
.lane--main .lane-line { font-size: .95rem; max-width: 42ch; }

/* ⛔ THE ONLY PLACE ACID BECOMES A LAMP, and it is rendered server-side from
   a real Twitch answer. It cannot appear on a page that does not know. */
.lane-now {
    display: inline-flex; align-items: center; gap: var(--s2);
    align-self: flex-start;
    font-family: var(--mono); font-size: .52rem;
    letter-spacing: .18em; text-transform: uppercase;
    color: var(--void); background: var(--acid);
    padding: .26rem .6rem;
}
.lane-now i { width: 6px; height: 6px; border-radius: 50%; background: var(--void); animation: beat 1.4s ease-in-out infinite; }
.lane.is-on { border-color: var(--acid); }

/* The whole card is the hit area where there is somewhere to go. The anchor
   stays a real anchor in the markup so it keeps its keyboard focus and its
   middle-click; the pseudo-element just grows it to the card. */
.lane-go {
    margin-top: auto;
    font-family: var(--mono); font-size: .54rem;
    letter-spacing: .18em; text-transform: uppercase;
    color: var(--dim); text-decoration: none;
    transition: color var(--swift);
}
.lane-go::before { content: ''; position: absolute; inset: 0; z-index: 1; }
.lane-go span::after { content: ' \2192'; }
.lane:hover .lane-go, .lane-go:focus-visible { color: var(--acid); }

@media (max-width: 900px) {
    .lineup { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .lane--main { grid-column: span 2; }
}
@media (max-width: 560px) {
    .lineup { grid-template-columns: 1fr; }
    .lane--main { grid-column: span 1; }
}

/* ── THE STANDBY FRAME. Off air, the screen holds a still of the room the
      stream comes from instead of an empty panel.
   ⛔ IT IS A HELD FRAME, NOT A VISUALISER. Nothing here pulses, bounces or
      sweeps in a way that suggests audio or signal. The one panel whose job
      is to say NOTHING IS TRANSMITTING may not carry decoration implying
      otherwise — EchoBorn's standby card idles its dead-air meter at the
      floor for exactly this reason.
   ⚠️ The drift below IS motion, and it is the honest kind: a still frame
      moving slowly claims nothing about a signal. It is cancelled outright
      under prefers-reduced-motion. ──────────────────────────────────────── */
.standby-art { position: absolute; inset: 0; overflow: hidden; }
.standby-art img {
    width: 100%; height: 100%; object-fit: cover;
    /* The grade is where the legibility comes from, NOT a backdrop-filter:
       an unsupported filter degrades to nothing and would leave white text
       on a bright monitor. A baked-in grade cannot fail. */
    filter: grayscale(.25) brightness(.66) contrast(1.04);
    transform: scale(1.06);
}
/* ⚠️ The wash is on the ALPHA, for the same reason. Measured, not judged:
   the worst pixel under every line of this card is sampled against that
   line's own ink colour and the numbers are in the commit. */
.standby-art::after {
    content: ''; position: absolute; inset: 0;
    background:
        radial-gradient(120% 110% at 50% 45%, transparent 0%, rgba(10,8,16,.55) 78%),
        linear-gradient(180deg, rgba(10,8,16,.30), rgba(10,8,16,.62));
}
/* ⛔ THE LEGIBILITY IS A PANEL BEHIND THE WORDS, NOT A DIMMER ON THE ROOM.
   First version graded the picture to brightness(.38) and washed the whole
   frame to carry the body copy - every line passed and the room was barely
   visible, which defeats the only reason the picture is here. The grade came
   back up and the contrast moved to a contained ground under the text, so
   the art stays art and the copy stays readable.
   ⚠️ It is an rgba PANEL, never a backdrop-filter. An unsupported filter
   degrades to nothing and would leave this text on a lit monitor. */
/* ⛔ A LOWER THIRD, NOT A CARD IN THE MIDDLE. Centred, the panel sat
   directly over him - his words, and the screenshot made it obvious: the
   one thing the picture is there to show was the thing the card covered.
   Broadcast furniture already solved this. The status runs along the foot
   of the frame, the room stays whole above it, and it reads as the idiom it
   borrows from rather than as a box dropped on a photograph.
   ⚠️ It is anchored to the FOOT of the picture, which is desk and keyboard
   in this frame and is the least interesting band of almost any frame he
   will wire in later. */
.standby-body {
    position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
    /* A grid, not a flex row: the status sits ABOVE the headline on the left
       and the button holds the right edge, which is the shape the idiom
       actually has. As three flex children in a row the chip drifted into
       the middle of the frame with nothing under it. */
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "status ." "text btn";
    align-items: end; gap: var(--s3) var(--s6);
    padding: var(--s6) var(--s7);
    /* ⛔ LEFT, NOT CENTRED. .screen-off centres its content for the old
       middle card, and a lower third with centred copy reads as a caption
       that lost its box. */
    text-align: left;
    /* .82, not .74. At .74 the body copy measured 4.56:1 against a 4.5
       floor - passing, with nothing left. This picture is a SLOT and he
       swaps it often; four hundredths of headroom is not a margin. */
    /* The ground fades UP out of the frame rather than ending on a hard
       edge, so the strip belongs to the picture instead of sitting on it.
       The solid floor is what carries the contrast. */
    background: linear-gradient(180deg, transparent, rgba(10,8,16,.86) 42%, rgba(10,8,16,.95));
    border-top: 1px solid var(--hair);
}
.standby-text { grid-area: text; min-width: 0; }
.standby-text h3 { color: var(--text); margin-bottom: var(--s2); }
.standby-text p { max-width: 56ch; }
.standby-third .standby-status { grid-area: status; justify-self: start; }
.standby-third .btn { grid-area: btn; }
.js .standby-art img { animation: standby-drift 38s ease-in-out infinite alternate; }
@keyframes standby-drift {
    from { transform: scale(1.06) translate3d(0, 0, 0); }
    to   { transform: scale(1.12) translate3d(-1.2%, -1%, 0); }
}

/* The corner marks. A held frame wears the furniture of a held frame —
   drawn, 1px, no icon and no request. */
.screen-off .standby-corner {
    position: absolute; width: 22px; height: 22px; z-index: 1;
    border: 1px solid var(--hair-acid); pointer-events: none;
}
.screen-off .standby-corner--tl { top: 14px;    left: 14px;  border-right: 0; border-bottom: 0; }
.screen-off .standby-corner--br { bottom: 14px; right: 14px; border-left: 0;  border-top: 0; }

/* ⛔ The lamp stays DEAD here. It is the one element on the page that means
   "live", and this is the branch that renders when he is not. */
.screen-off .lamp { box-shadow: none; animation: none; }

@media (prefers-reduced-motion: reduce) {
    .js .standby-art img { animation: none !important; transform: scale(1.06) !important; }
}

/* ⛔ THE RATIO BELONGS TO THE PLAYER, NOT TO THE CARD. Measured at 390px:
   the standby card needs 260px of height and a 16:9 box gives 173px, with
   overflow:hidden on it — so the OFF AIR status and the Follow button were
   sliced off, top and bottom, on every phone since this page was built. The
   embed still gets its exact ratio; the card gets a floor and grows. */
/* THE RATIO ONLY MOVES WHERE IT HAS TO. Letterboxing the card on a wide
   screen cropped the room hard, top and bottom, and tuning object-position
   only traded one bad crop for another - the picture is 16:9 and the card
   was not, so something had to be thrown away either way. Above 700px the
   content fits inside 16:9 with room to spare, so the picture keeps its own
   shape and nothing is cropped at all. Below 700px it genuinely does not
   fit, which was the real bug, so there the box grows instead. */
@media (max-width: 700px) {
    .screen--standby { aspect-ratio: auto; min-height: 420px; }
}
@media (max-width: 420px) {
    .screen--standby { min-height: 460px; }
}

/* The status chip. A solid ground under the one line that states the state,
   so it reads at 4.5:1 against whatever picture is in the slot — including
   one he has not wired yet. */
.standby-status {
    display: inline-flex; align-items: center; gap: var(--s3);
    background: rgba(10,8,16,.92);
    border: 1px solid var(--hair);
    padding: .4rem .8rem;
}
.standby-status .mono-s { color: var(--text); }
.standby-status .lamp { width: 8px; height: 8px; flex: none; }

/* The panel's side padding is desktop generosity; at 390 it squeezed the
   copy into six short lines inside a card that had room. */
@media (max-width: 760px) {
    /* One column, rather than squeezing a headline, a paragraph and a
       button into a row that none of them fit. */
    .standby-body {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "status" "text" "btn";
        justify-items: start; gap: var(--s4);
    }
}
@media (max-width: 560px) {
    .standby-body { padding: var(--s5) var(--s4); }
    .screen-off { padding: var(--s4); }
    .standby-third .standby-status { margin-left: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE SUPPORT PAGE — the ladder, and the receipts
   ═══════════════════════════════════════════════════════════════════════ */

/* ── The rungs. One row of equal cards and ONE of them leads, because the
      module's table says which — `featured`, which the first version of
      this page read and then ignored entirely. ───────────────────────── */
.ladder {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 1px;
    background: var(--hair);
    border: 1px solid var(--hair);
}
.rung {
    position: relative;
    display: flex; flex-direction: column; gap: var(--s2);
    padding: var(--s6) var(--s5) var(--s5);
    background: var(--panel);
    text-decoration: none;
    transition: background var(--swift);
}
a.rung:hover, a.rung:focus-visible { background: var(--panel-2); }
.rung-amt {
    font-family: var(--poster);
    font-size: clamp(2.1rem, calc(28.25px + 1.3714vw), 3.651rem); line-height: .9;
    color: var(--amber);
}
.rung-name {
    font-family: var(--mono); font-size: .52rem;
    letter-spacing: .18em; text-transform: uppercase; color: var(--text);
}
.rung-line { font-size: .82rem; line-height: 1.6; color: var(--dim); margin: var(--s2) 0 0; }
.rung-go {
    margin-top: auto; padding-top: var(--s4);
    font-family: var(--mono); font-size: .54rem;
    letter-spacing: .18em; text-transform: uppercase;
    color: var(--mute); transition: color var(--swift);
}
a.rung:hover .rung-go, a.rung:focus-visible .rung-go { color: var(--acid); }

/* ⛔ The lead rung is marked by WEIGHT, not by a second accent. Amber already
   means money on this page; spending acid here too would leave the page with
   two "look at this" colours and no hierarchy between them. */
.rung--lead { background: var(--panel-2); }
.rung--lead .rung-amt { color: var(--amber); }
.rung-flag {
    position: absolute; top: 0; left: 0; right: 0; height: 2px;
    background: var(--amber);
    /* The words are for screen readers and for nobody else — the rule says
       it visually, and a second label would crowd a 190px card. */
    overflow: hidden; text-indent: -9999px; white-space: nowrap;
}
/* The open rung is the quiet one: same card, no amount to shout. */
.rung--open .rung-amt { color: var(--mute); }

/* ── The receipts. Rows that each point at a thing that already exists. ── */
.freebies { display: grid; gap: 1px; margin-top: var(--s5); background: var(--hair); border: 1px solid var(--hair); }
/* ⚠️ Its top margin is for when it FOLLOWS a paragraph. As the first thing
   in a copy column it pushed that column 24px below the heading - the one
   row out of eighteen across six pages that did not align. The spread
   reserves a shared strip for exactly this reason; a stray first-child
   margin defeats it. */
.spread-copy > .freebies:first-child { margin-top: 0; }
.freebie {
    display: grid; gap: var(--s2);
    padding: var(--s4) var(--s5);
    background: var(--panel);
    text-decoration: none;
    transition: background var(--swift), padding-left var(--swift);
}
.freebie:hover, .freebie:focus-visible { background: var(--panel-2); padding-left: calc(var(--s5) + 6px); }
.freebie b {
    font-family: var(--mono); font-size: .58rem;
    letter-spacing: .16em; text-transform: uppercase; color: var(--text);
}
.freebie:hover b, .freebie:focus-visible b { color: var(--acid); }
.freebie span { font-size: .84rem; line-height: 1.55; color: var(--dim); }

@media (max-width: 560px) {
    .ladder { grid-template-columns: 1fr 1fr; }
    .rung { padding: var(--s5) var(--s4) var(--s4); }
    /* Five rungs in two columns leaves the open one alone beside an empty
       cell, which reads as a layout that ran out rather than a deliberate
       last option. It takes the full width instead - the same fix the open
       slot on /gaming needed, for the same reason. */
    .rung--open { grid-column: 1 / -1; }
}

/* ⚠️ The unconfigured notice was a full `.empty` box with s8 padding and a
   separate centred button under it — two blocks and a lot of air to say one
   sentence, which reads as a hole in the page rather than as a state. One
   row: the sentence, and the thing to do about it. */
.notready {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--s5); flex-wrap: wrap;
    margin-top: var(--s4);
    padding: var(--s4) var(--s5);
    border: 1px dashed var(--hair);
}
.notready .mono-s { color: var(--mute); }

/* ═══════════════════════════════════════════════════════════════════════
   LODESTAR — a product page for something that is not released yet
   ═══════════════════════════════════════════════════════════════════════ */

/* ⛔ THE STATUS CHIP SAYS "NOT RELEASED" AND IT DOES NOT PULSE. An amber dot
   that breathes reads as a live indicator, and this one indicates the exact
   opposite — the thing is not out. It is a flat mark on a flat chip. */
.status-chip {
    display: inline-flex; align-items: center; gap: var(--s3);
    font-family: var(--mono); font-size: .56rem;
    letter-spacing: .18em; text-transform: uppercase;
    color: var(--amber);
    border: 1px solid rgba(255,216,77,.38);
    background: rgba(255,216,77,.06);
    padding: .4rem .8rem;
}
.status-chip i { width: 6px; height: 6px; background: var(--amber); flex: none; }

/* A row in a list that is not a link. Same shape as .freebie so the two read
   as one family, without the hover affordance of something clickable —
   a cursor change on a thing that cannot be pressed is a small lie. */
.freebie--flat { cursor: default; }
.freebie--flat:hover { background: var(--panel); padding-left: var(--s5); }
.freebie--flat:hover b { color: var(--text); }

/* ⚠️ The disclaimer is NOT styled down to disappear. It is the sentence that
   makes this page allowed to exist, so it keeps body-sized type and real
   contrast — small print that cannot be read is not a disclaimer, it is a
   decoration shaped like one. */
.smallprint {
    margin-top: var(--s9);
    border-top: 1px solid var(--hair);
    padding-top: var(--s6);
    display: grid; gap: var(--s3);
}
.smallprint p { font-size: .86rem; line-height: 1.7; color: var(--dim); max-width: 70ch; margin: 0; }
.smallprint b { color: var(--text); font-weight: 500; }

/* ── A feature band that carries a picture. The /gaming one deliberately has
      none (its only art duplicates a photograph further down that page); the
      homepage one is a product shot of the thing being described. ────────── */
.feature-grid--art { grid-template-columns: 1.1fr 1fr; align-items: center; }
.feature--art .plate { align-self: stretch; }
.feature--art .plate figure { height: 100%; }

/* The lead row in a receipts list: the same component, given a picture and
   the room to use it. ⚠️ One row only — promoting all four would promote none. */
.freebie--lead {
    display: grid; grid-template-columns: 180px minmax(0, 1fr);
    gap: 0; align-items: stretch;
    margin-top: var(--s5);
    background: var(--panel);
    border: 1px solid var(--hair);
    text-decoration: none;
    transition: background var(--swift), border-color var(--swift);
}
.freebie--lead:hover, .freebie--lead:focus-visible { background: var(--panel-2); border-color: var(--hair-acid); }
.freebie-art { position: relative; overflow: hidden; background: var(--ink); }
.freebie-art img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.freebie-body { display: grid; gap: var(--s2); padding: var(--s5); align-content: center; }
.freebie--lead b {
    font-family: var(--mono); font-size: .64rem;
    letter-spacing: .16em; text-transform: uppercase; color: var(--text);
}
.freebie--lead:hover b { color: var(--acid); }
.freebie-body > span > span, .freebie-body span { font-size: .86rem; line-height: 1.6; color: var(--dim); }
/* ⚠️ The list below the lead row loses its own top margin: the lead row
   already supplies the gap, and two stacked margins read as a broken join. */
.freebie--lead + .freebies { margin-top: 1px; }

@media (max-width: 760px) {
    .feature-grid--art { grid-template-columns: 1fr; }
    .freebie--lead { grid-template-columns: 1fr; }
    .freebie-art { aspect-ratio: 16/9; }
}
