/* site-v2/defender/style.css — Edition № V.
   Rides on /styles.css. Every colour here is one of the site's role tokens (--bg, --fg,
   --fg-muted, --rule, --accent, --surface); this file adds no palette of its own and no new
   type family. The GAME is a loud 1981 arcade; the page around it is arondale.com.

   The one deliberate exception: the stage is #000 in both themes. A CRT is black on a bright
   morning too, and letting the light theme wash it grey would be a lie about the artefact. */

:root{
  /* ── THE FOLD BUDGET ──────────────────────────────────────────────────────
     On a landscape laptop the stage takes the viewport height that the nav, the masthead
     and the control strip do not:

        --stage-h = 100svh − --nav-h − --hero-h − --controls-h

     These three numbers also live in src/shell/geometry.js as CHROME.{nav,hero,controls},
     which models this layout so the geometry can be proved headlessly at a table of real
     laptop sizes. tests/shell.test.js READS THIS FILE and asserts the two copies agree — a
     layout constant that lives in two places and drifts is how a game ends up half a screen
     tall for a year without anyone noticing. */
  --nav-h: 76px;                  /* the sticky site nav */
  --hero-h: 86px;                 /* the compact landscape masthead */
  --controls-h: 176px;            /* strip + legend + provenance + the section's bottom padding */
  /* THE MARQUEE IS PART OF THE BUDGET. It sits above the stage inside #cabinet, so its height
     is height the picture does not get. It was not in this formula, which is why it had to stay
     small to be safe — a marquee that grew simply pushed the cabinet past the viewport. Declared
     here, subtracted below, and now it can be the size it should be. */
  /* THE MARQUEE IS THE WIDTH OF THE MACHINE. On the cabinet it spanned the whole head, so it
     spans the whole stage here. Its height is therefore a CONSEQUENCE of the cabinet's width and
     the artwork's own 3.163:1 — not a number picked by eye — which is what this computes: the
     cabinet is min(viewport minus its 24px gutters, --cab-max-w).
     The 34vh ceiling is the safety valve. On a short landscape screen a full-width marquee would
     take more height than the picture below it, so there it goes narrower rather than the game
     going shorter. Height always drives width, so it cannot distort in either direction. */
  --marquee-ar: 3.163;
  /* 1132 = --cab-max-w (1180) minus .cab-in's two 24px gutters: the stage's INNER width, which
     is the number the marquee has to match. Using the outer 1180 left it 56px narrow. */
  /* Full-bleed: the marquee is 100vw wide, so its height is 100vw / 3.163. The 40vh ceiling is
     the safety valve — past it the ARTWORK is what gives way (it goes narrower than the
     viewport), never the game. */
  /* The marquee is as wide as the cabinet (1132 = --cab-max-w minus .cab-in's two 24px
     gutters), so its height is that over the artwork's 3.163:1. This is the number the page
     budget subtracts; the render derives the same height from its own width, so they agree. */
  --marquee-h: calc(min(100vw - 48px, 1132px) / var(--marquee-ar));
  --stage-min-h: 240px;           /* one whole vertical scale; below it we stop shrinking */
  --stage-max-h-fold: 1080px;     /* and above this it stops growing; a 4K panel is not a cabinet */
  /* THE CABINET IS THE SITE'S COLUMN, NOT A SHAPE OF ITS OWN. 1180px is /styles.css's
     --maxw, the same box .nav-in and .wrap use, so the stage's left rule lands on the
     `arondale` wordmark and its right rule on the LinkedIn glyph. Declared as a literal
     because tests/shell.test.js parses this file for a px number and CAB_MAX_W in
     geometry.js has to equal it. */
  --cab-max-w: 1180px;

  /* The PRE-FOLD budget — portrait, and any landscape too small for the cabinet. A height
     budget rather than a viewport fill, so that on a phone the nav, the hero, the canvas,
     the control strip AND the provenance line still sit above the fold: the ten-second test
     (SHELL_AND_LIFECYCLE §3.1) is a layout constraint, not a copy exercise. */
  --stage-max-h: min(46vh, 460px);
  --cab: #05060a;                 /* the bezel — near-black, a touch cool, never pure */
}

/* ── hero ─────────────────────────────────────────────────────────────────── */
/* TWO COLUMNS: the back arrow gets its own margin, the edition line and title get the rest.
   `auto 1fr` rather than a fixed gutter so the arrow keeps its own intrinsic width at every
   size and the title's left edge stays put. `align-items:center` because the arrow reads as a
   control paired with the title, not as a bullet hanging off the eyebrow. */
/* THE TITLE ALIGNS WITH THE WORDMARK; THE ARROW HANGS OUTSIDE IT.
   The arrow's column is ZERO wide, so column 2 — the edition line and the title — starts at
   .ed-hero's content edge, which is the same 24px inset .nav-in uses. That is what puts
   `Defender` and `Edition № III` on the same left rule as `arondale`. The arrow then pulls
   itself left by its own width plus the gap, into the page margin, where it belongs: it is
   navigation away from this page, not part of its masthead.

   Below --maxw + a arrow's worth of margin there is no margin to hang in, so the column
   becomes real again and the arrow sits inline. Nothing is ever clipped off-screen. */
/* TWO ROWS, so the arrow can centre on the TITLE rather than on the masthead block. Centred on
   the block it sat high, because the edition line above it dragged the block's midpoint up.
   `.ed-head` is display:contents so the kicker and the title become direct grid items and the
   arrow can share the title's row — exact, with no magic offset to re-tune when the type
   changes. */
.ed-hero{--back-w:clamp(44px,4.4vw,64px);--back-gap:20px;
  max-width:var(--maxw);margin:0 auto;padding:32px 24px 14px;
  display:grid;grid-template-columns:0 minmax(0,1fr);grid-template-rows:auto auto;gap:0}
.ed-head{display:contents}
.ed-hero .kicker{grid-column:2;grid-row:1}
.ed-hero h1{grid-column:2;grid-row:2}
.ed-hero .backlink{grid-column:1;grid-row:2;align-self:center}
.ed-hero .backlink{
  display:flex;align-items:center;justify-content:center;
  color:var(--paper);
  width:var(--back-w);                    /* a real target, not a glyph: 44px is the floor */
  margin-left:calc(-1 * (var(--back-w) + var(--back-gap)));
  padding:6px 0;text-decoration:none;border-radius:3px;
  transition:color .15s ease,transform .15s ease;
}
/* Set vertically, reading downward, hanging under the arrow in the same margin. `vertical-rl`
   rather than a rotate() so it is still text — selectable, searchable, and it reflows instead
   of overlapping when the type scales. */
/* `upright`, not `mixed`. Mixed is the CSS default for Latin in a vertical flow and it lays the
   glyphs on their side — the reader has to tilt their head. Upright keeps every letter standing
   and stacks them one per row, which is how a cabinet's side art and a lift's floor plate do it.
   In upright mode `letter-spacing` is the VERTICAL gap, so it is tuned down: .24em was leading
   between rows, not tracking. */
.ed-hero .backlabel{
  writing-mode:vertical-rl;text-orientation:upright;
  font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:.6rem;letter-spacing:.06em;text-transform:uppercase;
  /* The word gaps have to be pulled in too: a full space between BACK / TO / LAB reads as a
     paragraph break when the text runs downward. */
  word-spacing:-.25em;
  /* OUT OF FLOW. In flow this 139px column was the tallest thing in the masthead's second
     row, so it set the row's height and opened ~250px of dead black between the title and
     the marquee. It hangs in the margin beside content it is not part of, so it should not
     be able to size that content: absolute, under the arrow, contributing nothing. */
  position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:10px;
  color:var(--fg-muted);white-space:nowrap;line-height:1;
  transition:color .15s ease;
}
.ed-hero .backlink:hover .backlabel{color:var(--accent)}
/* --maxw is 1180; 1180 + 2 x (64 + 20) = 1348. Below that the margin cannot hold the arrow. */
@media(max-width:1347px){
  .ed-hero{grid-template-columns:auto minmax(0,1fr);gap:0 var(--back-gap)}
  .ed-hero .backlink{margin-left:0}
}
.ed-hero .backlink svg{width:100%;height:auto;display:block}
/* The hit area is expanded by a pseudo-element rather than by padding, because --hero-h is an
   84px measured budget the stage height is computed against and growing the box would take
   pixels off the picture. The glyph can be 37px and the target still 44px. */
.ed-hero .backlink{position:relative}
.ed-hero .backlink::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:max(100%,44px);height:44px}
.ed-hero .backlink:hover{color:var(--accent);transform:translateX(-3px)}
.ed-hero .backlink:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
.ed-hero .kicker{margin-bottom:10px}
.ed-hero h1{font-size:clamp(2.1rem,4.4vw,2.9rem);line-height:1.02;margin-bottom:0}
.ed-hero h1 .thin{font-weight:300;font-style:italic;color:var(--fg-muted);font-size:.55em;
  display:block;letter-spacing:0;margin-top:8px;line-height:1.28}

@media(max-width:820px){.ed-hero{padding-top:28px}}

/* ── THE MASTHEAD ON A PHONE ──────────────────────────────────────────────────
   Two faults, one cause: there is no margin on a 390px screen for something to hang in.

   THE LABEL. `BACK TO LAB` is set vertically and positioned `top:100%` — it hangs BELOW the
   arrow, out of flow, in a gutter that exists only on a wide viewport. On a phone the gutter
   is gone, so 139px of upright type ran straight down through the stats bar and `Sol 5.6`
   was reading `Sol` with `A / C / K` stacked through it. It is `display:none` nowhere,
   though: the comment above is right that the visible text IS this link's accessible name,
   and hiding it outright would leave the only route off this page announcing itself as
   nothing at all. Off-screen keeps the name and takes the collision.

   THE ARROW. `align-self:center` centres it on the whole of row 2, and on a phone row 2 is
   the title PLUS the two wrapped lines of italic standfirst under it — so the arrow sat
   level with the middle of the subtitle, pointing away from the word it belongs to.
   `align-self:start` puts it back on `Defender`'s own line; the nudge is the difference
   between the two boxes' optical centres, measured, not guessed. */
@media(max-width:820px){
  .ed-hero .backlabel{
    position:absolute;width:1px;height:1px;margin:0;padding:0;overflow:hidden;
    clip-path:inset(50%);white-space:nowrap;
  }
  /* -3px, and it is measured: at 375px the title's first line box centres on y=151 and the
     arrow's own box centres on y=154. Below 820px `h1` is pinned to the clamp's 2.1rem floor
     and `--back-w` to its 44px floor, so both boxes are a constant size across the whole
     phone range and one number is right for all of it. */
  .ed-hero .backlink{align-self:start;margin-top:-3px}
}

/* ── THE STATS BAR ────────────────────────────────────────────────────────────
   A ledger line, not a hero band: monospace, muted labels, the numbers doing the talking.
   `dl` because these are term/value pairs and that is what a description list is for — the
   markup means the same thing as the picture. Rules BETWEEN cells only (border-left on every
   cell after the first), so the row does not end in a hanging stroke. */
.stats{
  display:flex;flex-wrap:wrap;gap:0;margin:0 0 16px;
  font-family:'IBM Plex Mono',ui-monospace,monospace;
}
.stats .stat{padding:2px 26px;border-left:1px solid var(--rule)}
.stats .stat:first-child{padding-left:0;border-left:0}
.stats dt{
  font-size:.72rem;letter-spacing:.12em;color:var(--fg-muted);
  margin-bottom:4px;font-weight:400;
}
.stats dd{margin:0;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.stats dd b{font-size:.95rem;font-weight:600;color:var(--paper);letter-spacing:.02em}
/* The qualifier is what keeps the number honest — "54,501" alone invites the reader to guess
   what was counted. It is small, but it is not optional. */
.stats dd span{font-size:.68rem;letter-spacing:.06em;color:var(--fg-muted)}

@media(max-width:820px){
  .stats .stat{padding:2px 14px}
  .stats dd span{display:none}       /* the qualifiers are the first thing to go, not the numbers */
}

/* THE CABINET MARQUEE, above the glass. Centred on the machine and height-bounded, so it
   cannot eat the stage — the picture's height is what is left after --nav-h, --hero-h and
   --controls-h, and a marquee free to grow with the viewport would take it silently. The
   artwork is ~3.67:1, so 92px of height is ~338px of width. */
/* HEIGHT DRIVES WIDTH, never the other way round. A fixed width with a max-height squashes the
   artwork — it rendered at 6.79:1 against its own 3.16:1 and the letters went thin. Setting the
   height from the budget and letting width follow makes distortion impossible to express, and
   --marquee-h is the same number the stage formula subtracts, so the two can never disagree. */
/* FULL BLEED. The marquee is the cabinet head, and a cabinet head runs the whole width of the
   machine — so it breaks out of .cab-in's 24px gutters to the viewport edges. The 50vw/-50%
   pair is the standard escape from a centred column without needing a wrapper.
   Width leads here, unlike the stage-width version: at 100vw the height is 100vw/3.163, and
   --marquee-h below has to say the same thing or the budget and the render disagree. */
/* The marquee is the width of the machine — the same box the stage uses, so the two share a
   left and a right rule. Width leads and height follows, so it cannot distort. */
.cab-marquee{display:block;width:100%;height:auto;margin:0 auto 14px}
@media(max-width:820px){
  :root{--marquee-h:clamp(48px,13vh,92px)}
  .cab-marquee{margin-bottom:10px}
}

/* ── §01's two columns ────────────────────────────────────────────────────────
   The prose sets at ~62ch by design, so on any laptop the right half of this section was
   empty black. The marquee takes it. Single column below 900px — the artwork is 3.67:1 and
   would be 90px tall in a phone column, which is a smudge, not a marquee. */
.what-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:32px;align-items:start}
.what-art{margin:0}
/* No border and no radius: the frame is a black screenshot on a black page, so a rule around it
   would draw a box around nothing. It reads as an image because it is one. */
.what-art img{display:block;width:100%;height:auto}
@media(min-width:900px){
  /* The prose keeps its measure and the picture takes the rest. `start` because this frame is
     portrait and taller than the copy — centring it would push its top below the first
     paragraph and break the reading line the two columns share. */
  /* The measure gives ground to the picture. The gameplay capture is LANDSCAPE (1058 x 772)
     where the still it replaced was portrait, so at 58ch the copy left it a column narrower
     than it is tall and it shrank to a stamp. 46ch is still a comfortable measure — the site's
     body text sets around there — and it buys the picture the width the motion needs. */
  .what-grid{grid-template-columns:minmax(0,46ch) minmax(0,1fr);gap:48px;align-items:start}
  /* Width is DEFINITE and the height follows it. `width:auto` with only a max-height collapses a
     not-yet-loaded lazy image to a 0x0 box, which never intersects the viewport, so the lazy
     load never fires and the picture never appears — it deadlocks on itself. */
  .what-art img{width:100%;height:auto;margin-left:auto}
}

/* ── the cabinet ──────────────────────────────────────────────────────────── */
#cabinet{padding:0 0 46px}
/* the site's own gutter rhythm — .wrap is 24px, 16px below 430 */
.cab-in{max-width:var(--maxw);margin:0 auto;padding:0 24px}
@media(max-width:430px){.cab-in{padding:0 16px}}

/* THE NARROW STAGE — phones, tablets, and any landscape too small for the cabinet. Here the
   stage IS the picture: 4:3, width leading and height following, capped by the height budget
   that keeps the canvas above the fold, so a narrow viewport shrinks the box rather than
   letterboxing inside a box that is itself the wrong shape. The fold rule below replaces
   this on a landscape laptop, where the stage becomes the black surround instead and the
   picture is fitted inside it. THE PICTURE IS 4:3 IN BOTH. */
#stage{
  position:relative;
  aspect-ratio:4/3;
  width:min(100%, calc(var(--stage-max-h) * 4 / 3));
  height:auto;
  margin:0 auto;
  background:var(--cab);
  border:1px solid var(--rule);
  border-radius:3px;
  display:flex;align-items:center;justify-content:center;
  overflow:hidden;
}
#screen{
  display:block;
  /* JS sets the exact INTEGER pixel box from fitBox(); these are the pre-script defaults, and
     they are 4:3 rather than 100 % × 100 % so that the first paint — and a paint in a browser
     where the module never loads — is the right shape rather than a stretched one. The stage
     is now allowed to be wider than the picture, so `width:100%` would have squashed it. */
  height:100%;width:auto;aspect-ratio:4/3;max-width:100%;
  background:#000;
  outline:none;
}
/* Keyboard focus must be visible — but drawn INSIDE the canvas it was a 2px amber frame
   ruled across the picture for the whole of a keyboard-played game, which on a fullscreen
   1200 × 900 tube is a border on the artefact. Where :has() exists the ring moves onto the
   cabinet instead: same indicator, same contrast, off the picture. The canvas rule stays as
   the fallback for engines without :has(), where it is still better than nothing. */
#screen:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
@supports selector(:has(*)){
  #screen:focus-visible{outline:none}
  #stage:has(#screen:focus-visible){outline:2px solid var(--accent);outline-offset:-2px}
}

/* THE POINTER, WHILE THE KEYBOARD IS FLYING. A keyboard player's cursor is parked wherever
   they clicked to focus the canvas — the middle of the playfield — and then sits on top of
   the ship for the whole run, an arrow the game never drew.
   main.js writes [data-pointer] and appstate.js's hidesPointer() decides it; the moment the
   pointer moves it is back. Only ever set during a live, focused, core-routed run, so every
   control that ends that run is still reachable with the mouse. The descendant rule is not
   redundant: `cursor` inherits, but #screen and the pillar captions can set their own. */
#stage[data-pointer="hidden"],
#stage[data-pointer="hidden"] *{ cursor:none }

/* ── THE FOLD: a 1981 tube on a 2026 laptop ───────────────────────────────────
   A landscape viewport at least 900 px wide and at least 5:4 gets the cabinet treatment.
   The STAGE — the black surround — takes all the viewport height the page chrome does not
   and grows sideways to at most 16:9, the shape of the panel it is being shown on. The
   PICTURE inside it stays exactly 4:3 at PAR 80:73, pillarboxed, sized by fitBox() in JS.

   The picture is never squashed to fill the panel. The pillars are not dead space either:
   they are the honest consequence of a 4:3 tube on a widescreen laptop, and they carry the
   caption that says so. */
@media (min-width: 900px) and (min-aspect-ratio: 5/4){
  :root{
    --stage-h: clamp(var(--stage-min-h),
                     calc(100vh - var(--nav-h) - var(--hero-h) - var(--controls-h) - var(--marquee-h)),
                     var(--stage-max-h-fold));
    /* svh where it exists: on a laptop it equals vh, and on a phone in landscape it is the
       height that survives the browser's own chrome. The vh line above is the fallback. */
    --stage-h: clamp(var(--stage-min-h),
                     calc(100svh - var(--nav-h) - var(--hero-h) - var(--controls-h) - var(--marquee-h)),
                     var(--stage-max-h-fold));
    /* THE CABINET FILLS ITS COLUMN. It used to be `min(100%, --stage-h * 16/9)` — width
       derived from height — which meant the black surround floated at some width nothing
       else on the page shared, so its two rules landed inside the nav's gutters and every
       horizontal edge on the page disagreed with every other one. Now the stage takes the
       full .cab-in content box, which is the same box .nav-in uses, and the page has ONE
       left edge and ONE right edge from the wordmark down.

       The 4:3 picture inside is still sized by fitBox() and still pillarboxed; on a short
       viewport the surround is now allowed past 16:9 and the pillars are wider. That is the
       cost of alignment and it is deliberate — the pillars carry the caption that explains
       them. Declared once so the control panel below is the width of its own machine. */
    --stage-w: 100%;
  }

  /* the masthead, compacted onto two lines so the cabinet gets the height instead */
  /* The compact landscape masthead. Still two columns — the arrow keeps its margin — but the
     right column collapses the edition line and the title onto one baseline row, which is what
     keeps --hero-h at 86px. The arrow shrinks with it rather than setting the row height. */
  .ed-hero{
    max-width:min(var(--cab-max-w), 100%);
    padding:14px 24px 10px;
    /* NO column gap here. The arrow's column is zero-wide and the arrow hangs outside the box,
       so any gap on this grid is pure indent on the title — it was pushing `Defender` 18px off
       the wordmark's rule, which is the one alignment this masthead exists to hold. The narrow
       fallback below re-declares its own gap, where the column is real again. */
    gap:0;
  }
  .ed-hero .backlink{--back-w:clamp(34px,2.6vw,44px)}
  /* .ed-head stays display:contents from the base rule. It used to be a flex row here, but the
     title carried `flex:1 0 100%` and wrapped to its own line anyway, so the two lines were
     already stacked — the flex container was buying nothing and it broke the arrow out of the
     title's grid row. Two grid rows produce the identical result and keep the alignment. */
  /* 2px, not the 6px the flex row used: --hero-h is 86px and the stage height is
     `100svh − nav − hero − controls`, so every pixel spent here comes off the picture.
     Measured: 0px gives an 84px hero, 2px gives 86px, 6px gives 90px and overruns the budget.
     The arrow stays centred on the title at every one of them. */
  .ed-hero{row-gap:2px}
  .ed-hero .kicker{margin-bottom:0}
  .ed-hero h1{font-size:clamp(1.5rem,2.1vw,2rem);line-height:1.1}
  .ed-hero h1 .thin{display:inline;font-size:.6em;margin-top:0;margin-left:.5em;line-height:1.1}

  #cabinet{padding-bottom:26px}
  .cab-in{max-width:min(var(--cab-max-w), 100%)}
  /* THE STAGE IS THE PICTURE, AT THE WIDTH OF THE MACHINE.
     It used to be a height-budgeted box of whatever shape the leftover viewport made — roughly
     16:9 — with the 4:3 picture pillarboxed inside it. That is why the game was a stamp in a
     field of black: the box was wide, the picture is not, and the picture could only ever be as
     tall as the box. The box is now 4:3 itself and takes the cabinet's full width, so the
     picture fills it edge to edge and is ~1130px across instead of ~400.

     The trade is that the cabinet no longer fits in the viewport height and the page scrolls to
     the control strip. That is the right way round: a playable picture that needs a scroll beats
     a fold-perfect layout you cannot see the ship in. Fullscreen (below) is still there for
     anyone who wants the whole screen. */
  #stage{
    aspect-ratio:4/3;
    width:100%;
    height:auto;
  }
  /* No pillars to caption any more — the picture IS the box. */
  #stage .pillar{display:none}
}

/* Fullscreen and the iOS "maximise" fallback are the same box at two sizes. Both beat the
   fold rule above on specificity, so the stage abandons its budgeted height and takes the
   screen; the 4:3 box then comes back from JS via fitBox(), which means the bars are OURS
   and black rather than the compositor's. */
#stage:fullscreen,#stage:-webkit-full-screen{
  width:100%;height:100%;max-width:none;max-height:none;
  aspect-ratio:auto;border:0;border-radius:0;background:#000;
}
#stage.maximised{
  position:fixed;inset:0;z-index:200;width:100%;height:100%;max-width:none;max-height:none;
  aspect-ratio:auto;border:0;border-radius:0;background:#000;
}

/* ── the caption in the pillars ───────────────────────────────────────────────
   The letterbox is a design decision, so it is signed. Shown only where the pillar is
   genuinely wide enough to hold it (a viewport at least 1100 px and at least 3:2 always
   leaves ≥ 40 px either side — asserted over the viewport table in tests/shell.test.js),
   never interactive, and dimmed while a game is running. */
.pillar{
  position:absolute;top:0;bottom:0;width:2.4em;
  display:none;align-items:center;justify-content:center;
  pointer-events:none;user-select:none;
  writing-mode:vertical-rl;text-orientation:mixed;
  font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:.62rem;letter-spacing:.28em;text-transform:uppercase;
  color:#7b818f;opacity:.8;transition:opacity .3s ease;
}
.pillar.l{left:8px}
.pillar.r{right:8px;transform:rotate(180deg)}
#stage[data-state="PLAYING"] .pillar,#stage[data-state="TRAINER"] .pillar{opacity:.3}
/* TWO GATES, AND THE SECOND IS THE REAL ONE. The media query is a cheap pre-filter;
   [data-pillar="wide"] is written by main.js's measure() from the MEASURED pillar, because
   since the cabinet became the page's column a tall viewport makes the picture width-limited
   and the pillar 0px — and a caption with no pillar to sit in lands on the playfield. */
@media (min-width:1100px) and (min-aspect-ratio:3/2){
  #stage[data-pillar="wide"] .pillar{display:flex}
}

/* the overlay — the game's own voice, not the site's */
/* THE BUTTON SITS HIGH IN THE STAGE, NOT IN THE MIDDLE OF IT.
   Centred, it landed ~424px down an 847px picture — and with the nav, the masthead and a 358px
   marquee above, that put the one control the page exists for BELOW THE FOLD on first load. A
   call to action you have to scroll to find is not a call to action. Top-aligned with a small
   offset, it lands in the first viewport at every size the page supports, and the empty picture
   below it reads as the screen waiting rather than as dead space. */
.ovl{
  position:absolute;inset:0;display:flex;flex-direction:column;
  align-items:center;justify-content:flex-start;gap:14px;
  padding-top:clamp(24px,7vh,96px);
  /* No backdrop-filter. It buys a 1 px blur of a black rectangle, and a filtered layer
     stacked over a low-latency canvas is exactly the configuration in which macOS promotes
     the canvas to an overlay plane and reads back empty. Opacity does the same job. */
  /* Nearly opaque now that the title card sits underneath: the card IS the backdrop, and a
     62 % scrim over it would just make Williams' red look like brick. */
  background:rgba(0,0,0,.86);
  font-family:'IBM Plex Mono',ui-monospace,monospace;
}
.ovl[hidden]{display:none}
.ovl .reason:empty{display:none}

/* ── THE CONTROL CARD ─────────────────────────────────────────────────────────
   Defender's controls are the one thing a new player cannot guess. There is no left/right
   axis on the cabinet — an arrow does not steer, it thrusts wherever the ship already points
   and flips the facing when it opposes it — so a reader who presses left expecting to move
   left and sees the ship carry on right concludes the game is broken. This card is shown
   BEFORE the first start and never again.

   Keycaps are drawn, not typed: a <kbd> with a bottom border reads as a key at a glance in a
   way a monospace letter does not.

   WHAT WAS HERE BEFORE, AND WHY IT WENT. An inverted-T diagram stacked on a five-row legend:
   eleven keycaps for five controls, 30px each, 220 x 269, and the play button pushed under
   the fold behind it. The diagram and the legend carried the same five facts in two visual
   languages, so the reader paid for each one twice — extraneous load in Sweller's sense, the
   one kind you are allowed to simply delete. Eleven simultaneous glyphs is a Hick count for a
   five-decision problem, and five ungrouped rows read in series is the Miller failure under
   it. 30px is below any reasonable target floor, Fitts or otherwise, for the thing the whole
   card exists to be looked at.

   WHAT IS HERE NOW. One board, every key exactly once, at 48px. One label at a time, so
   working memory holds one binding instead of five. The board demonstrates: it presses each
   key in turn and the little screen shows what that key does — because "smart bomb" is a
   verb no label can carry and a wavefront crossing a screen can. 269px tall becomes 102px,
   and the button is back above the fold on every viewport the card appears on.

   The cycle lives in src/page/controlcard.js. Everything below is the picture it drives. */
.startcard{
  display:flex;align-items:center;justify-content:center;gap:clamp(18px,2.4vw,34px);
  font-family:'IBM Plex Mono',ui-monospace,monospace;
  margin-bottom:4px;
}
.startcard[hidden]{display:none}

/* ── the board ─────────────────────────────────────────────────────────────── */
/* `align-items:end` so `space`, `B` and `H` sit on the same bottom rule as ← ↓ →. That row
   IS the home row of the hand that will play this, and lining them up is what makes the
   board read as a keyboard rather than as a row of chips. */
.cc-board{
  --k:clamp(42px,3.3vw,50px);
  display:flex;align-items:end;gap:14px;
}
.cc-arrows{
  display:grid;grid-template-columns:repeat(3,var(--k));grid-auto-rows:var(--k);gap:6px;
}
.cc-k-up{grid-column:2;grid-row:1}
.cc-k-left{grid-column:1;grid-row:2}
.cc-k-down{grid-column:2;grid-row:2}
.cc-k-right{grid-column:3;grid-row:2}

.cc-key{
  display:inline-flex;align-items:center;justify-content:center;
  width:var(--k);height:var(--k);padding:0;
  font-family:inherit;font-size:calc(var(--k) * .38);line-height:1;
  color:var(--paper);background:#141414;
  border:1px solid #3a3a3a;border-bottom-width:4px;border-radius:7px;
  /* The travel is a transform and the face is a colour, so a press costs one composited
     frame and never reflows the board under the reader's eye. */
  transition:transform .09s ease-out,border-bottom-width .09s ease-out,
             background-color .16s ease,color .16s ease,border-color .16s ease;
}
.cc-key.cc-wide{width:calc(var(--k) * 2.4);font-size:calc(var(--k) * .26);letter-spacing:.12em;
  text-transform:uppercase}

/* PRESSED. Not a highlight — a press: the cap travels the full depth of its own bevel and
   the bevel goes with it, which is the same physical model .play-btn uses two rules down.
   The yellow is the marquee's, so the one thing moving on the overlay is the one colour the
   page already spends on "this is the control". */
.cc-key.is-down{
  transform:translateY(3px);border-bottom-width:1px;
  background:#F5D601;border-color:#F5D601;color:#141414;
}

/* ── the little screen ─────────────────────────────────────────────────────── */
.cc-screen{
  display:flex;flex-direction:column;align-items:center;gap:7px;
  width:clamp(150px,15vw,182px);
}
.cc-svg{
  display:block;width:100%;height:auto;
  background:#080808;border:1px solid #2a2a2a;border-radius:3px;
}
.cc-name{
  font-size:.7rem;letter-spacing:.14em;text-transform:uppercase;
  color:#F5D601;text-align:center;line-height:1.2;min-height:1.2em;
}

/* One scene at a time. `display` and not `opacity`, because toggling display is also what
   restarts the keyframes inside — every scene plays from frame zero on the step it belongs
   to, with no reflow and no reach into the Web Animations API. */
.cc-scene{display:none}
.cc-screen[data-scene="thrust"] .cc-s-thrust,
.cc-screen[data-scene="climb"]  .cc-s-climb,
.cc-screen[data-scene="fire"]   .cc-s-fire,
.cc-screen[data-scene="bomb"]   .cc-s-bomb,
.cc-screen[data-scene="warp"]   .cc-s-warp{display:block}

.cc-hull{fill:#e8e8e8}
.cc-flame{fill:#F5D601;animation:cc-flame 140ms steps(2,end) infinite}
.cc-foe{fill:#4ade5a}
.cc-streak{fill:#4a4a4a}
.cc-guide{stroke:#2e2e2e;stroke-width:1;stroke-dasharray:2 5;fill:none}
.cc-laser{fill:#fff}
.cc-boom path{stroke:#F5D601;stroke-width:1.5;fill:none}
.cc-ring{fill:none;stroke:#fff;stroke-width:1.5}
.cc-flash{fill:#fff}
.cc-warp-flash{fill:#8be9ff}

@keyframes cc-flame{ from{opacity:1;transform:scaleX(1)} to{opacity:.45;transform:scaleX(.6)} }

/* thrust — the ship holds station, the world goes past. Four streaks at four speeds; the
   spread is the parallax, and it is the whole reason this reads as motion and not as a
   flashing rectangle. */
.cc-streak{animation:cc-streak linear infinite}
.cc-s-thrust .s1{animation-duration:900ms}
.cc-s-thrust .s2{animation-duration:1400ms;animation-delay:-300ms}
.cc-s-thrust .s3{animation-duration:700ms;animation-delay:-500ms}
.cc-s-thrust .s4{animation-duration:1700ms;animation-delay:-900ms}
@keyframes cc-streak{ from{transform:translateX(170px)} to{transform:translateX(-40px)} }

/* climb / dive */
.cc-climb{animation:cc-climb 2000ms ease-in-out infinite}
@keyframes cc-climb{ 0%,100%{transform:translateY(0)} 25%{transform:translateY(-15px)}
  75%{transform:translateY(15px)} }

/* fire — the shot crosses, the lander goes, the burst marks where it was. The three
   timings are one event told in three parts and they have to stay in step: the laser
   arrives at 46%, the target dies at 48%, the burst is spent by 70%. */
.cc-laser{animation:cc-laser 1500ms linear infinite}
@keyframes cc-laser{
  0%{transform:translateX(0);opacity:0}
  6%{opacity:1}
  46%{transform:translateX(80px);opacity:1}
  47%,100%{transform:translateX(80px);opacity:0}
}
.cc-foe-hit{animation:cc-foe-hit 1500ms steps(1,end) infinite}
@keyframes cc-foe-hit{ 0%,46%{opacity:1} 48%,88%{opacity:0} 90%,100%{opacity:1} }
.cc-boom{animation:cc-boom 1500ms ease-out infinite;transform-box:fill-box;transform-origin:center}
@keyframes cc-boom{ 0%,46%{opacity:0;transform:scale(.2)} 50%{opacity:1;transform:scale(1)}
  70%,100%{opacity:0;transform:scale(1.5)} }

/* smart bomb — the wavefront leaves the ship and everything it reaches stops existing. The
   three landers die in the order the ring reaches them, which is the only detail that makes
   this read as a wave rather than as a screen wipe. */
.cc-ring{animation:cc-ring 2200ms ease-out infinite;transform-box:view-box;transform-origin:22px 28px}
@keyframes cc-ring{ 0%,8%{transform:scale(.2);opacity:0} 12%{opacity:1}
  70%{transform:scale(11);opacity:0} 100%{transform:scale(11);opacity:0} }
.cc-flash{animation:cc-flash 2200ms linear infinite}
@keyframes cc-flash{ 0%,8%{opacity:0} 11%{opacity:.5} 22%,100%{opacity:0} }
.cc-s-bomb .cc-foe{animation:cc-vanish 2200ms steps(1,end) infinite}
.cc-s-bomb .b1{animation-delay:0ms}
.cc-s-bomb .b2{animation-delay:-40ms}
.cc-s-bomb .b3{animation-delay:-90ms}
@keyframes cc-vanish{ 0%,26%{opacity:1} 28%,86%{opacity:0} 90%,100%{opacity:1} }

/* hyperspace — out here, in over there. The squash on the way out is what stops it reading as
   the ship simply being switched off.
   THE ONE RULE THESE THREE TIMINGS EXIST TO KEEP: the screen is never empty. The first cut
   had the ship gone at 42% and the arrival not lit until 54%, and a quarter of a second of
   blank black in a 65px box does not read as a teleport — it reads as a panel that failed to
   load, which is the single worst thing this card could say about the machine. The departure
   hands off to the flash, and the flash hands off to the arrival, with an overlap at each
   seam. */
.cc-warp-out{animation:cc-warp-out 2000ms linear infinite;transform-box:fill-box;transform-origin:left center}
@keyframes cc-warp-out{
  0%,26%{opacity:1;transform:scaleX(1)}
  30%{opacity:.3;transform:scaleX(1)}
  33%{opacity:1;transform:scaleX(.45)}
  37%,88%{opacity:0;transform:scaleX(.1)}
  93%,100%{opacity:1;transform:scaleX(1)}
}
.cc-warp-flash{animation:cc-warp-flash 2000ms ease-out infinite;transform-box:fill-box;transform-origin:center}
@keyframes cc-warp-flash{ 0%,30%{opacity:0;transform:scaleY(.06)} 36%{opacity:1;transform:scaleY(1)}
  52%,100%{opacity:0;transform:scaleY(.06)} }
.cc-warp-in{animation:cc-warp-in 2000ms linear infinite}
@keyframes cc-warp-in{ 0%,38%{opacity:0} 42%{opacity:.45} 46%,88%{opacity:1} 93%,100%{opacity:0} }

/* ── the text the picture stands in for ────────────────────────────────────── */
/* Off-screen rather than display:none, because display:none is not read aloud either. */
.cc-sr{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0;list-style:none;
}

/* REDUCED MOTION TAKES THE MECHANISM AWAY, SO IT HAS TO GIVE THE FACTS BACK. The board
   stays — it is a still picture of a keyboard and it was never the moving part — but the
   demonstration goes, and with it the only thing that was carrying which key does what. The
   off-screen list becomes the visible legend in its place. It repeats the glyphs, which the
   animated card exists to avoid; that is the correct trade here and only here. */
@media(prefers-reduced-motion:reduce){
  .startcard{flex-direction:column;gap:14px}
  .cc-screen{display:none}
  .cc-key{transition:none}
  .cc-sr{
    position:static;width:auto;height:auto;margin:0;overflow:visible;clip-path:none;
    white-space:normal;display:flex;flex-wrap:wrap;justify-content:center;gap:6px 18px;
    font-size:.72rem;letter-spacing:.04em;color:var(--fg-muted);max-width:560px;
  }
  .cc-sr b{color:var(--paper);font-weight:400;margin-right:5px}
}

/* The card is 102px tall now instead of 269, so the short-viewport rule that used to throw
   the legend away has nothing left to throw: it fits inside the fold budget at every height
   the fold layout is used at. The width rule stays — below it the overlay is too narrow for
   a 600px board, and a phone has no keyboard to press anyway. */
@media(max-width:760px){ .startcard{display:none} }

/* ── THE PLAY BUTTON ──────────────────────────────────────────────────────────
   The two colours are lifted off the marquee directly rather than invented: the yellow of the
   letterforms and the red of their drop shadow. The 3D edge is that same drop shadow — a solid
   offset, no gradient and no blur, because that is how the artwork does it and how a 1980
   screen-printed control panel does it.

   It attracts. Every 4s it shakes for about half a second and then sits still, which is the
   cabinet's own logic: an idle machine nags, and a machine you are already using does not. */
.ovl .play-btn{
  font-family:'Nunito',sans-serif;font-weight:800;
  font-size:clamp(1.15rem,2.6vw,2rem);letter-spacing:.06em;text-transform:uppercase;
  color:#B4141E;                      /* the marquee's red */
  background:#F5D601;                 /* the marquee's yellow, specified exactly */
  border:0;border-radius:7px;cursor:pointer;
  padding:clamp(12px,1.9vh,22px) clamp(26px,4.5vw,64px);
  /* Two lines now, so the button is a column rather than a text run. `line-height:1` on both
     spans keeps the block tight — the default leading on a 2rem face would put a visible gap
     between the label and its qualifier and make them read as two separate things. */
  display:flex;flex-direction:column;align-items:center;gap:5px;
  box-shadow:0 9px 0 #B4141E;         /* the letterforms' own drop shadow, as a bevel */
  transform:translateZ(0);
  animation:play-attract 4000ms ease-in-out infinite;
}
/* The qualifier is deliberately quiet: same red, smaller, lighter, and NOT uppercase, so it
   reads as an aside rather than as a second instruction. It is a warning the reader needs
   before they click, not a thing competing with the verb above it. */
.ovl .play-btn .pb-main{line-height:1;display:block}
.ovl .play-btn .pb-sub{
  display:block;line-height:1;
  font-size:.46em;font-weight:600;letter-spacing:.04em;text-transform:none;
  opacity:.72;
}
.ovl .play-btn:hover{background:#FFE633}   /* the same hue, lifted, not a different yellow */
/* Press travels the full 9px of the bevel and takes the shadow with it, so the button bottoms
   out against the panel instead of sliding over it. */
.ovl .play-btn:active{transform:translateY(9px);box-shadow:0 0 0 #B4141E;animation:none}
.ovl .play-btn:focus-visible{outline:3px solid #fff;outline-offset:4px}

/* ── THE SECONDARY ────────────────────────────────────────────────────────────
   Two doors, and they must not look like two of the same door. The primary is the marquee's
   own yellow on a 9px bevel and it shakes at you; this one is a rule and a word. Reading
   order does the ranking — the eye lands on the yellow first and finds this underneath —
   so the secondary needs no colour of its own to be found, and taking one would start an
   argument with the artwork behind it.

   It sits INSIDE .play-slot, so it parallaxes with the primary rather than shearing away
   from it; parallax.js freezes the pair while either one is pointed at or focused. */
.ovl .play-secondary{
  display:inline-block;margin-top:clamp(14px,1.8vh,22px);
  font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:clamp(.72rem,1.05vw,.84rem);letter-spacing:.16em;text-transform:uppercase;
  color:#F5D601;text-decoration:none;
  padding:8px 2px;border-bottom:1px solid rgba(245,214,1,.42);
  transition:color .18s ease,border-color .18s ease;
}
.ovl .play-secondary:hover{color:#FFE633;border-bottom-color:#FFE633}
.ovl .play-secondary:focus-visible{outline:3px solid #fff;outline-offset:4px;border-radius:2px}

/* The shake lives in the first 12.5% — 500ms of a 4000ms cycle — and the remaining 3.5s is
   deliberately dead. A button that never stops moving is noise; one that twitches and then
   waits is a machine asking for a coin. */
@keyframes play-attract{
  0%,  12.5%, 100% { transform:translate(0,0) rotate(0deg) }
  1.5%             { transform:translate(-5px,0) rotate(-1.6deg) }
  3.5%             { transform:translate(5px,0)  rotate(1.6deg) }
  5.5%             { transform:translate(-4px,0) rotate(-1.1deg) }
  7.5%             { transform:translate(4px,0)  rotate(1.1deg) }
  9.5%             { transform:translate(-2px,0) rotate(-.5deg) }
  11%              { transform:translate(2px,0)  rotate(.5deg) }
}
/* ACCESSIBILITY_SPEC: reduced motion kills the attract loop outright. The button is the most
   important control on the page and it must not be the thing that makes someone ill. */
@media(prefers-reduced-motion:reduce){
  .ovl .play-btn{animation:none}
}

/* THE TITLE CARD. Height-bounded and `contain`, so it never crops and never dictates the
   overlay's size — the overlay is pinned to the stage, and the stage's height is the picture's.
   `pixelated` because this is a 292 × 240 raster being shown far above 1:1; smoothing it would
   turn 1980 pixels into mush, which is the one thing this page cannot do. */
#titleCard{
  display:block;max-width:min(72%,420px);max-height:62%;width:auto;height:auto;
  object-fit:contain;image-rendering:pixelated;margin-bottom:4px;
}
@media(max-height:520px){#titleCard{max-height:48%}}
.ovl .reason{
  font-size:clamp(.72rem,1.5vw,.9rem);letter-spacing:.32em;text-transform:uppercase;
  color:#ffb224;text-align:center;padding:0 18px;
}
.ovl button{
  font-family:inherit;font-size:clamp(.78rem,1.6vw,.95rem);letter-spacing:.3em;text-transform:uppercase;
  color:#000;background:#ffb224;border:0;border-radius:2px;padding:12px 26px;cursor:pointer;
  transition:filter .15s ease;
}
.ovl button:not(.play-btn):hover,.ovl button:not(.play-btn):focus-visible{filter:brightness(1.12)}

/* ── control strip ────────────────────────────────────────────────────────── */
.strip{
  max-width:min(100%,900px);margin:12px auto 0;
  display:flex;flex-wrap:wrap;gap:8px;align-items:center;
}
.strip button,.strip .seg{
  font-family:'Inter',system-ui,sans-serif;font-size:.78rem;letter-spacing:.12em;text-transform:uppercase;
  color:var(--fg);background:transparent;border:1px solid var(--rule);border-radius:2px;
  padding:9px 15px;cursor:pointer;transition:border-color .18s ease,color .18s ease;
}
.strip button:hover,.strip button:focus-visible{border-color:var(--fg-muted);color:var(--accent)}
.strip button[aria-pressed="true"],.strip button.is-on{border-color:var(--accent);color:var(--accent)}
.strip button:disabled{color:var(--fg-faint);border-color:var(--rule);cursor:not-allowed}
.strip button:disabled:hover{color:var(--fg-faint);border-color:var(--rule)}
.strip .spacer{flex:1}
/* On the fold the toolbar is the cabinet's control panel, so it takes the cabinet's width —
   a strip that starts fifty pixels inside the machine it operates reads as a different
   object. This override lives HERE, after the rule it overrides, rather than up in the fold
   block: same specificity, so source order decides. The prose below keeps its own measure
   and stays centred — 1000px of .82rem text is 140 characters a line. */
@media (min-width: 900px) and (min-aspect-ratio: 5/4){
  /* …but never narrower than the toolbar's own single row (~545px of buttons). A cabinet
     that has been squeezed by a zoomed-in viewport would otherwise wrap the strip onto a
     second line, which costs 44px of height that --controls-h has not budgeted for and
     pushes the provenance line below the fold. Alignment is worth a lot; it is not worth
     the one line a sceptic reads first. */
  .strip{max-width:max(var(--stage-w), 620px)}
}
.strip .state{
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:.7rem;letter-spacing:.18em;
  color:var(--fg-muted);
}

/* the provenance line — small, but the first thing a sceptic reads */
.prov{
  max-width:min(100%,900px);margin:11px auto 0;
  font-size:.82rem;line-height:1.5;color:var(--fg-muted);
}
.prov a{color:var(--fg);text-decoration:none;border-bottom:1px solid var(--accent)}
.prov a:hover{color:var(--accent)}

/* The Lab instrumentation's container. `.dlab` supplies its own grid and reads the site's
   colour tokens, so all this does is give it width and a little air. */
.lab-mount{max-width:min(100%,900px);margin:18px auto 0}
.lab-mount:empty{display:none}

/* a persistent condition, stated once and left alone */
.notice{
  max-width:min(100%,900px);margin:9px auto 0;
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:.72rem;letter-spacing:.04em;
  color:var(--accent);line-height:1.6;
}
.notice[hidden]{display:none}

/* Shown only where the controls genuinely cannot be produced. INPUT_MAPPING §7.1 refuses a
   pointer-driven flight mode for Classic Mode on purpose — every way of turning a continuous
   2-D position into Defender's grammar invents a mechanic — so the honest thing is to say so
   rather than to ship a control that feels broken. */
.touch-only{display:none;color:var(--fg-muted);text-transform:none;letter-spacing:0;
  font-family:'Inter',system-ui,sans-serif;font-size:.84rem}
@media (hover:none) and (pointer:coarse){.touch-only{display:block}}

/* ── the controls card ────────────────────────────────────────────────────── */
#keysPanel{
  max-width:min(100%,900px);margin:16px auto 0;padding:20px 22px;
  border:1px solid var(--rule);border-radius:3px;background:var(--surface);
}
#keysPanel[hidden]{display:none}
/* The panel now has two rows of pills — the control GRAMMAR, then the layout within it — and
   an unlabelled pair of rows reads as one confusing row of eight. These are the labels. */
.kb-head{
  font-family:'Inter',system-ui,sans-serif;font-size:.66rem;letter-spacing:.16em;text-transform:uppercase;
  color:var(--fg-faint);margin:0 0 8px;
}
.kb-head + .kb-presets{margin-bottom:18px}
.kb-presets{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
.kb-preset{
  font-family:'Inter',system-ui,sans-serif;font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;
  color:var(--fg-muted);background:transparent;border:1px solid var(--rule);border-radius:999px;
  padding:6px 14px;cursor:pointer;
}
.kb-preset.is-on{color:var(--accent);border-color:var(--accent)}
.kb-list{display:grid;grid-template-columns:auto 1fr;gap:8px 18px;align-items:center;margin:0}
.kb-list dt{font-size:.86rem;color:var(--fg-muted)}
.kb-list dd{margin:0}
.kb-key{
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:.8rem;
  color:var(--fg);background:var(--surface-2);border:1px solid var(--rule);border-radius:3px;
  padding:5px 12px;min-width:5.5em;cursor:pointer;text-align:center;
}
.kb-key:hover,.kb-key:focus-visible{border-color:var(--accent);color:var(--accent)}
.kb-key.is-capturing{border-color:var(--accent);color:var(--accent);border-style:dashed}
.kb-key.is-unbound{color:var(--fg-faint)}
.kb-error{margin-top:14px;font-size:.82rem;color:var(--accent-3)}
.kb-warn{margin-top:8px;font-size:.8rem;color:var(--fg-muted)}

/* the flight legend — one line, always current, rebuilt from the live bindings */
.legend{
  max-width:min(100%,900px);margin:10px auto 0;
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:.74rem;letter-spacing:.04em;
  /* --fg-muted, not --fg-faint: this is the control legend, the one line a player actually
     has to read, and faint lands near 3:1 on light paper. */
  color:var(--fg-muted);line-height:1.9;
}

/* ── toast ────────────────────────────────────────────────────────────────── */
/* Bottom RIGHT, not centre: centre-bottom is exactly where the provenance line sits on the
   landing view, and a transient notice must never cover the one line a sceptic reads first.
   Bottom-left is the back-to-top button's. */
#toast{
  position:fixed;right:22px;bottom:22px;z-index:210;max-width:min(90vw,420px);
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:.72rem;letter-spacing:.1em;
  text-transform:uppercase;line-height:1.5;
  color:var(--fg);background:var(--surface);border:1px solid var(--rule);
  padding:9px 14px;border-radius:2px;
}
#toast[hidden]{display:none}
@media(max-width:640px){#toast{right:12px;bottom:12px;left:12px;max-width:none}}

/* ── section prose ────────────────────────────────────────────────────────────
   `*{box-sizing:border-box;margin:0;padding:0}` in /styles.css strips the default
   paragraph margin from every element on the site, and that sheet restores it only
   for the classes IT uses — `.note`, `.case .lede`, `.chapter p`. This page's own
   `.lede` and `.body` were never given it back, so four separate <p> elements
   rendered as one unbroken wall of text with no paragraph break anywhere in it.
   The measure is 64ch, which is `.note`'s in the global sheet; at the 1180px wrap
   these lines were running past 120 characters. */
.lede{
  font-family:'Nunito',system-ui,sans-serif;font-weight:300;
  font-size:1.2rem;line-height:1.55;letter-spacing:-.005em;
  max-width:64ch;color:var(--fg);
}
.body{max-width:64ch;color:var(--fg-muted)}
/* THE BUILD LINE. A rule and a colour, not a box: it sits inside the same prose column and has
   to read as an aside to the argument rather than as a pull-quote competing with the lede
   above it. The accent is on the three proper nouns only — who wrote the plan, who built from
   it — because those are the whole claim and the rest is scaffolding around them. */
.build-note{
  border-left:2px solid var(--accent);padding-left:16px;
  font-size:.92rem;line-height:1.6;
}
.build-note b{color:var(--fg);font-weight:600}
.lede + .body,.body + .body,.lede + .lede{margin-top:20px}

/* ── the receipts table ───────────────────────────────────────────────────── */
.receipts{display:grid;gap:0;margin-top:30px}
.receipt{
  display:grid;grid-template-columns:230px 1fr;gap:24px;
  padding:20px 0;border-top:1px solid var(--rule);
}
.receipt:last-child{border-bottom:1px solid var(--rule)}
/* §06's card ships one set of rows per control scheme and main.js shows the true one. The
   `hidden` attribute alone cannot do that here: `display:grid` above outranks the UA sheet's
   `[hidden]{display:none}`, so the "wrong" rows would stay on screen. */
.receipt[hidden]{display:none}
.receipt h3{font-size:1.02rem;line-height:1.3}
.receipt p{color:var(--fg-muted);max-width:66ch;font-size:.94rem}
.receipt code{font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:.86em;color:var(--fg)}
@media(max-width:720px){.receipt{grid-template-columns:1fr;gap:6px}}

/* ── modes, side by side ──────────────────────────────────────────────────── */
.modes{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:30px}
@media(max-width:760px){.modes{grid-template-columns:1fr}}
.mode{border:1px solid var(--rule);border-radius:3px;padding:24px;background:var(--surface)}
.mode .no{font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);margin-bottom:9px}
.mode h3{font-size:1.15rem;margin-bottom:8px}
.mode p{color:var(--fg-muted);font-size:.92rem}
.mode ul{margin:14px 0 0 1.1em;color:var(--fg-muted);font-size:.88rem}
.mode li{margin-bottom:5px}

/* ── the research index ───────────────────────────────────────────────────── */
.corpus{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr));gap:14px;margin-top:28px}
.corpus a{
  display:block;text-decoration:none;border:1px solid var(--rule);border-radius:3px;
  padding:16px 18px;transition:border-color .18s ease;
}
.corpus a:hover{border-color:var(--fg-muted)}
.corpus .f{font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:.76rem;color:var(--accent);
  letter-spacing:.02em;display:block;margin-bottom:6px;word-break:break-word}
.corpus .d{font-size:.86rem;color:var(--fg-muted);line-height:1.5}
.note a{color:var(--fg);text-decoration:none;border-bottom:1px solid var(--accent)}
.note a:hover{color:var(--accent)}

/* ── narrow screens: the game is still the hero, just shorter ─────────────── */
@media(max-width:640px){
  :root{--stage-max-h:min(40vh,340px)}
  .strip{justify-content:center}
}

@media (prefers-reduced-motion: reduce){
  .strip button,.corpus a,.kb-key,.ovl button{transition:none}
}

/* ═══════════════ THE SCENE — Defender's world, behind the page ═══════════════
   Six planes of the actual game: the star density the cabinet draws, three ranges built from
   ALTBL — the real 1024-entry altitude profile — and two bands of the real ROM sprites. The
   arithmetic and the provenance are in src/page/parallax_math.js; this file is where it is
   put on the screen.

   HOW IT SITS BEHIND EVERYTHING WITHOUT TOUCHING ANYTHING
   `#scene` is a negative-z child of <body>, and negative-z children paint ABOVE their parent's
   own background and BELOW every in-flow sibling. So the only change the rest of the page needs
   is for <body> to stop painting an opaque rectangle over it — and it loses nothing by that,
   because /styles.css already paints the identical colour on <html>, which propagates to the
   canvas behind the lot. NO section, nav or footer gets a z-index here. That matters: giving
   #cabinet a stacking context would trap the CSS fullscreen fallback's z-index:200 inside it.

   It is DECORATION: aria-hidden in the markup, pointer-events:none, no focusable content, and
   it is never over a control — it is under all of them by construction. */
body{background:transparent}

#scene{
  position:fixed;inset:0;z-index:-1;
  pointer-events:none;
  overflow:hidden;
  /* contain:paint stops the layers' oversized drift strips from contributing scrollable
     overflow to the document. Without it a 3072px-wide terrain tile would widen the page. */
  contain:layout paint;
}
/* A light page is not a black tube. The scene is drawn in CRTAB's own colours, which sit well
   on ink and only shout on paper, so on the light theme it steps back. Applied to the LAYERS,
   not to #scene, so it cannot out-specify the is-playing rule below and keep the scene bright
   over a running game. */
:root[data-theme="light"] .pxs-l{opacity:calc(var(--o,1) * .5)}

.pxs-l{
  position:absolute;inset:0;
  opacity:var(--o,1);
  /* The parallax transform is written here, by the rAF in src/page/parallax.js. The DRIFT is
     on the child, so the two never contend for one property. */
  will-change:transform;
}

/* THE DRIFT. One tile of the 2048px world, repeated across a strip one tile wider than the
   viewport, sliding left by exactly one tile and starting over. TDATA closes the random walk
   — 1024 up bits and 1024 down — so the profile is a ring and the seam is not a seam. */
.pxs-drift{
  position:absolute;left:0;bottom:0;
  width:calc(100% + var(--tw,0px));
  background-position:left bottom;
  animation:pxs-drift linear infinite;
  will-change:transform;
}
.pxs-l[data-kind="stars"] .pxs-drift{top:0;height:auto;background-position:left top}
@keyframes pxs-drift{
  from{transform:translate3d(0,0,0)}
  to  {transform:translate3d(calc(-1 * var(--tw,0px)),0,0)}
}

/* THE SHIPS. The exported ROM sprites, nothing redrawn, shown pixelated because a smoothed
   lander is not a lander. Sized in the markup at PAR 80:73 by spriteBox(). */
.pxs-ship{
  position:absolute;left:0;
  image-rendering:pixelated;
  animation:pxs-fly linear infinite;
  will-change:transform;
}
@keyframes pxs-fly{
  from{transform:translate3d(-22vw,0,0)}
  to  {transform:translate3d(122vw,0,0)}
}
/* On a phone the ranges and the stars carry the depth on their own. Seven more composited
   elements crossing the screen would be seven more for no gain, on the device least able to
   spare them. */
@media(max-width:699px){.pxs-l[data-kind="ships"]{display:none}}

/* THE SCRIM. Legibility first: the scene is alive behind the cabinet and settles to a texture
   behind four thousand words of prose. Opacity only, written by the same rAF, ramping over the
   first screen and a half. */
.pxs-scrim{position:absolute;inset:0;background:var(--bg);opacity:0}

/* THE GAME COMES FIRST. While the simulation is running the whole scene stops compositing and
   drops to a whisper. Six animated layers competing for the GPU with a 16.640 ms frame is the
   one way a decorative background can break the thing this page exists for. */
#scene.is-playing{opacity:.12}
#scene.is-playing .pxs-drift,
#scene.is-playing .pxs-ship{animation-play-state:paused}
#scene{transition:opacity .6s ease}

/* ── THE PLAY BUTTON'S BAND ───────────────────────────────────────────────────
   The button parallaxes inside its own wrapper, never on the button itself: `.play-btn` already
   animates `transform` for the attract shake and takes `translateY(9px)` on :active, and an
   inline transform would lose to the keyframes and fight the press. The wrapper carries the
   travel; the button keeps its own motion. */
.play-slot{display:block;will-change:transform}

@media(prefers-reduced-motion:reduce){
  /* ACCESSIBILITY_SPEC. The scene goes STATIC — no drift, no fly-through — and the rAF stops
     applying any parallax offset (frameOffsets() returns zeros). Not negotiable. */
  .pxs-drift,.pxs-ship{animation:none}
  #scene{transition:none}
}
