/* RollerHoster marketing landing — Phosphor Instrument, extended for long-form marketing.
   Mono for the terminal-brand headlines + labels; a readable sans for body copy. */
:root { --sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
html { scroll-behavior: smooth; scroll-padding-top: 74px; }

.landing { position: relative; z-index: 1; }
.landing a { color: inherit; text-decoration: none; }
.wrap { width: min(1120px, 92vw); margin: 0 auto; }
.eyebrow { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.28em; text-transform: uppercase; color: var(--green-dim); }
.btn-lg { padding: 13px 26px; font-size: 13px; }

/* ── nav ─────────────────────────────────────────────────── */
.lnav { position: sticky; top: 0; z-index: 40; border-bottom: 1px solid transparent; transition: border-color 0.2s, background 0.2s, backdrop-filter 0.2s; }
.lnav.scrolled { background: color-mix(in srgb, var(--bg) 82%, transparent); backdrop-filter: blur(10px); border-color: var(--line-hair); }
.lnav-inner { display: flex; align-items: center; gap: 24px; height: 58px; }
/* LONGER TEXT (see the block at the top of base.css). The nav bar is one unwrapping row of
   brand + five links + two CTAs between 861px and ~1100px, and there is no slack in it: a
   German set of labels ("Übersicht", "Funktionen", "Preise", "Server bereitstellen") overruns
   the wrap and the whole PAGE scrolls sideways, which is a failure, not a cosmetic issue.
   Tightening the gutters buys back the ~90px that costs. Scoped off English, which fits. */
:root:not([lang="en"]) .lnav-inner { gap: 16px; }
:root:not([lang="en"]) .lnav-links { gap: 14px; margin-left: 6px; }
.lnav-brand { display: flex; align-items: center; gap: 9px; font-family: var(--mono); font-weight: 700; letter-spacing: 0.14em; color: var(--head); font-size: 15px; }
.lnav-brand .sig { color: var(--green); text-shadow: var(--glow); }
.lnav-links { display: flex; gap: 22px; margin-left: 12px; font-family: var(--mono); font-size: 12.5px; }
.lnav-links a { color: var(--muted); transition: color 0.15s; }
.lnav-links a:hover { color: var(--head); }
.lnav-spacer { flex: 1; }
.lnav-signin { font-family: var(--mono); font-size: 12.5px; color: var(--muted); transition: color 0.15s; }
.lnav-signin:hover { color: var(--head); }
/* THE CONVERSION STRIP NEVER GIVES. This row is a fixed 58px and does not wrap, and its only
   flexible element is .lnav-spacer — which is already 0px wide at 901px in seven of the
   eleven shipped locales. So when the language chip joined the row (css/langpicker.css), its
   width came straight out of these two and broke "Sign in" and "Deploy a server ❯" onto a
   second line inside a 58px bar: 901-960px in English, 901-1040px in French and Spanish.
   They are the last things in this row that should bend, so now they do not. */
.lnav-signin, .lnav-cta { flex: none; white-space: nowrap; }
/* Refusing to wrap turns an over-budget row into an over-budget DOCUMENT, and making the chip
   globe-only did not buy back enough for the two longest sign-in strings — French
   ("Se connecter") still ran 19px over at 901px, Spanish ("Iniciar sesión") 41px. So the row
   sheds the same control it ALREADY sheds at 900px, just a little earlier. "Sign in" is the
   one item here that costs nothing to lose: .lnav-cta points at the very same /login.html. */
@media (max-width: 1000px) {
  .lnav-inner .lnav-signin { display: none; }
}

/* ── hero ────────────────────────────────────────────────── */
.lhero { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 48px; align-items: center; padding: 70px 0 60px; }
/* min-width: 0 — a grid item's automatic minimum size is its MIN-CONTENT width, and the
   picker's pill row is a nowrap scroll container: without this the track grows to fit every
   pill (measured 416px inside a 360px viewport), which widens the whole hero column, reflows
   the copy and gives the page a horizontal scrollbar. Zero lets the track keep the layout
   width and the row scroll inside it, which is the whole point of the scroll container. */
.lhero-copy { max-width: 560px; min-width: 0; }
.lhero h1 { font-family: var(--mono); font-size: clamp(2.1rem, 5vw, 3.35rem); line-height: 1.05; letter-spacing: -0.01em; margin: 16px 0 0; color: var(--head); font-weight: 700; }
.lhero h1 .hl { color: var(--green); text-shadow: var(--glow); }
.lhero-sub { font-family: var(--sans); font-size: 16px; line-height: 1.6; color: var(--muted); margin: 20px 0 28px; }
.lhero-cta { display: flex; gap: 12px; flex-wrap: wrap; }
.lhero-trust { display: flex; align-items: center; gap: 8px; margin-top: 22px; font-family: var(--mono); font-size: 11.5px; color: var(--faint); }
.lhero-trust b { color: var(--green-dim); }
.lhero-trust .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: var(--glow); }

/* ── hero game picker ────────────────────────────────────────
   Filled by js/entry/landing.js once the public games feed answers. Same pill vocabulary
   as .lshow-tab — this is the page's second segmented selector, not a new component
   language.

   THE BOX IS RESERVED FROM FIRST PAINT. The control arrives after a fetch, and it sits
   directly above the hero's primary CTA: injected into normal flow it shoved "Deploy a
   server" down ~120px on arrival, under whatever the reader's pointer was already on. So
   the height is held in the authored CSS instead — the arithmetic below adds up to the
   built control, and every part of it is fixed-height by construction (one pill row that
   scrolls rather than wraps, one blurb slot that reserves its worst case). Nothing here
   is data-dependent, so neither the number of packs nor the length of their copy can move
   the CTA. When there is nothing to show, .lpick-none takes the whole box back out. */
.lpick {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 26px;
  position: relative; /* containing block for .lpick-change — see below */
  /* label 16.3 (10.5px × 1.55) + gap 8 + row 43.4 (pill 33.4 + 5px focus-ring padding ×2)
     + gap 8 + blurb 80.6 (4 × 1.55em @13px) = 156.3 */
  min-height: 157px;
}
.lpick-none { display: none; } /* no JS, no feed, no sellable game → hero exactly as authored */
.lpick-label { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.28em; text-transform: uppercase; color: var(--green-dim); padding-right: 122px; }
/* "Change game" — reopens the full-screen stage for a visitor who has already chosen (the
   stage is first-visit-only, so without this there is no way back to it). Wired by the stage
   script; this is only its look.

   ABSOLUTELY POSITIONED ON PURPOSE. #lpick's height is reserved from first paint down to the
   pixel (the arithmetic above) precisely so the hero's CTA never moves; a fourth row in that
   column flex would blow the reservation and shove the CTA down the moment the feed lands.
   Out of flow it costs the box nothing, it sits on the label's line whatever order the script
   inserts it in, and .lpick-none takes it away with everything else when there is no feed —
   which is right: with no games there is no stage to go back to. The label reserves the
   gutter so long-form copy can never run under it. */
.lpick-change {
  /* Authored in index.html, but shown only once landing.js has built the pill row: the
     stage it reopens is mounted by the same module, so a page whose entry script never
     ran would otherwise offer a control that cannot do anything. */
  display: none;
  position: absolute; top: -6px; right: 0; z-index: 1;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; line-height: 1.5;
  color: var(--green-dim); background: var(--surf-2); border: 1px solid var(--line); border-radius: 100px;
  /* 6px of block padding, not 2: 15px of line box + 12 + 2 borders = 29px, which clears
     WCAG 2.2 SC 2.5.8's 24x24 outright instead of leaning on the spacing exception (it
     measured 118x21, saved only by the 27.8px gap to the nearest pill — one tightening
     anywhere in #lpick would have tipped it into a failure). It is the sole way back to
     the stage, so it must not depend on its neighbours to pass. AAA's 44px is deliberately
     NOT taken: the box is out of flow at top/right and the pill row below it scrolls
     horizontally under that corner, so a 44px chip would sit on the pills' own hit areas.
     Growing upward keeps the 29px clear of the row (chip -6..23, pills from 29.3). */
  padding: 6px 12px; cursor: pointer; align-items: center; gap: 6px;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.lpick:has(.lpick-row) .lpick-change { display: inline-flex; }
/* …and never while the chooser it reopens is the screen. Offering "change game" from
   inside the game chooser is a contradiction, and openStage() refuses a stage that is
   already open — so the control was visible, focusable (tab stop 12 of the open stage)
   and a no-op that only scrolled focus 873px away from it. Ties with the rule above on
   specificity, so it has to sit after it. */
:root[data-stage="on"] .lpick-change { display: none; }
.lpick-change:hover { color: var(--green-text); border-color: var(--line-2); background: var(--surf-3); }
.lpick-change:focus-visible { outline: 2px solid var(--green); outline-offset: 3px; }
/* One row, always: wrapping made the reserved height depend on how many packs the
   deployment enables and how wide the viewport is. Overflow scrolls instead (arrow keys
   move focus, which scrolls the pill into view); the scrollbar is hidden because a
   platform that renders a classic one would add its height back to the reservation. */
.lpick-row { display: flex; flex-wrap: nowrap; min-width: 0; gap: 8px; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; padding: 5px 0; }
.lpick-row::-webkit-scrollbar { display: none; }
.lpick-btn { flex: 0 0 auto; white-space: nowrap; font-family: var(--mono); font-size: 12.5px; color: var(--muted); background: var(--surf-2); border: 1px solid var(--line); border-radius: 100px; padding: 7px 16px; cursor: pointer; transition: color 0.15s, border-color 0.15s, background 0.15s; }
.lpick-btn:hover { color: var(--text); border-color: var(--line-2); }
.lpick-btn[aria-checked="true"] { color: var(--green-text); border-color: var(--green); background: var(--surf-3); box-shadow: 0 0 0 1px var(--green-soft); }
.lpick-btn:focus-visible { outline: 2px solid var(--green); outline-offset: 3px; }
/* Four lines are reserved, which is what the schema's 200-character blurb cap comes to at
   this column width — switching between games of different copy lengths must not shuffle
   the CTA under the reader's cursor either. The slot always holds copy (the "All games"
   state has its own, authored as data-blurb on #lpick), so the reservation is never blank
   space, and it is a live region: it must stay in the tree to be announced at all. */
.lpick-blurb { font-family: var(--sans); font-size: 13px; line-height: 1.55; color: var(--muted); margin: 0; max-width: 56ch; min-height: 6.2em; }
.lpick-blurb b { color: var(--green-text); font-weight: 600; }

/* hero product mockup — a stylised server deck */
.mock { border: 1px solid var(--line); border-radius: 12px; background: var(--surf-2); overflow: hidden; box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55); }
.mock-bar { display: flex; align-items: center; gap: 7px; padding: 10px 14px; border-bottom: 1px solid var(--line-hair); background: var(--surf-1); }
.mock-bar i { width: 9px; height: 9px; border-radius: 50%; background: var(--line-2); }
.mock-bar i:first-child { background: #e0554f; }
.mock-bar i:nth-child(2) { background: #e0a23c; }
.mock-bar i:nth-child(3) { background: var(--green); }
.mock-bar span { margin-left: 8px; font-family: var(--mono); font-size: 10.5px; color: var(--faint); letter-spacing: 0.08em; }
.mock-body { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.mock-name { display: flex; align-items: center; gap: 10px; font-family: var(--mono); font-size: 15px; color: var(--head); font-weight: 600; }
.mock-badge { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.1em; color: var(--green); border: 1px solid var(--green-dim); border-radius: 100px; padding: 2px 9px; }
.mock-connect { display: flex; flex-direction: column; gap: 4px; background: var(--inset); border: 1px solid var(--line); border-left: 2px solid var(--green); border-radius: 8px; padding: 10px 13px; }
.mock-connect .k { font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--green-dim); }
.mock-connect .v { font-family: var(--mono); font-size: 15px; color: var(--green); }
.mock-specs { display: flex; flex-wrap: wrap; gap: 14px; font-family: var(--mono); font-size: 11px; color: var(--muted); }
.mock-specs b { color: var(--head); font-weight: 600; }
.mock-spark { height: 46px; }
.mock-spark svg { width: 100%; height: 100%; display: block; }
.mock-console { background: var(--inset); border: 1px solid var(--line-hair); border-radius: 8px; padding: 10px 12px; font-family: var(--mono); font-size: 11px; line-height: 1.75; color: var(--muted); }
.mock-console .t { color: var(--faint); }
.mock-console .g { color: var(--green-dim); }

/* ── stats strip ─────────────────────────────────────────── */
.lstats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--line-hair); border: 1px solid var(--line-hair); border-radius: 12px; overflow: hidden; margin: 8px 0 24px; }
.lstat { background: var(--surf-2); padding: 22px 18px; text-align: center; }
.lstat .n { font-family: var(--mono); font-size: 26px; font-weight: 700; color: var(--green-text); }
/* LONGER TEXT (see the block at the top of base.css): four fixed 1fr tiles, uppercase mono at
   10px with letter-spacing — "max RAM / server" becomes "max. Arbeitsspeicher / Server", one
   unbreakable 19-character word included. The tiles are one grid row so they already share a
   height; these two properties are what stop the long word overflowing its own tile. */
.lstat .l { font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); margin-top: 4px; overflow-wrap: anywhere; hyphens: auto; }

/* ── generic section ─────────────────────────────────────── */
.lsection { padding: 68px 0; }
.lsec-head { max-width: 620px; margin: 0 auto 40px; text-align: center; }
.lsec-title { font-family: var(--mono); font-size: clamp(1.5rem, 3.4vw, 2.1rem); color: var(--head); margin: 10px 0 0; letter-spacing: -0.01em; }
.lsec-sub { font-family: var(--sans); font-size: 15px; line-height: 1.6; color: var(--muted); margin: 14px 0 0; }

/* ── features ────────────────────────────────────────────── */
.lfeatures { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.lfeat { background: var(--surf-2); border: 1px solid var(--line); border-radius: 12px; padding: 22px; transition: border-color 0.16s, transform 0.16s, box-shadow 0.16s; }
.lfeat:hover { border-color: var(--line-2); transform: translateY(-3px); box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35); }
.lfeat-ic { width: 40px; height: 40px; border: 1px solid var(--green-dim); border-radius: 10px; display: grid; place-items: center; color: var(--green); margin-bottom: 14px; }
.lfeat-ic svg { width: 20px; height: 20px; }
.lfeat h3 { font-family: var(--mono); font-size: 15px; color: var(--head); margin: 0 0 7px; }
.lfeat p { font-family: var(--sans); font-size: 13.5px; line-height: 1.6; color: var(--muted); margin: 0; }

/* ── how it works ────────────────────────────────────────── */
.lsteps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; counter-reset: step; }
.lstep { position: relative; background: var(--surf-2); border: 1px solid var(--line); border-radius: 12px; padding: 26px 22px; }
.lstep .lstep-n { font-family: var(--mono); font-size: 12px; color: var(--on-accent); background: var(--green); width: 26px; height: 26px; border-radius: 8px; display: grid; place-items: center; font-weight: 700; margin-bottom: 14px; }
.lstep h3 { font-family: var(--mono); font-size: 15px; color: var(--head); margin: 0 0 7px; }
.lstep p { font-family: var(--sans); font-size: 13.5px; line-height: 1.6; color: var(--muted); margin: 0; }

/* ── pricing ─────────────────────────────────────────────── */
.lpricing { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: stretch; }
.lprice { position: relative; display: flex; flex-direction: column; background: var(--surf-2); border: 1px solid var(--line); border-radius: 14px; padding: 26px 24px; }
.lprice.pop { border-color: var(--green); box-shadow: 0 0 0 1px var(--green-soft), 0 20px 50px rgba(0, 0, 0, 0.4); }
.lprice-ribbon { position: absolute; top: -11px; left: 24px; font-family: var(--mono); font-size: 9px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--on-accent); background: var(--green); padding: 3px 11px; border-radius: 100px; font-weight: 700; }
.lprice-name { font-family: var(--mono); font-size: 14px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--head); }
.lprice-amt { font-family: var(--mono); font-size: 2.1rem; color: var(--green-text); font-weight: 700; margin: 10px 0 2px; }
.lprice-amt small { font-size: 13px; color: var(--muted); font-weight: 400; }
.lprice-note { font-family: var(--sans); font-size: 12.5px; color: var(--faint); min-height: 18px; }
.lprice-list { list-style: none; padding: 0; margin: 18px 0 22px; display: flex; flex-direction: column; gap: 9px; flex: 1; }
.lprice-list li { font-family: var(--sans); font-size: 13.5px; color: var(--muted); display: flex; gap: 9px; align-items: flex-start; }
.lprice-list li::before { content: "›"; color: var(--green); font-family: var(--mono); }
.lprice .btn { width: 100%; justify-content: center; display: flex; }

/* ── faq ─────────────────────────────────────────────────── */
.lfaq { max-width: 760px; margin: 0 auto; }
.lfaq details { border-bottom: 1px solid var(--line-hair); }
.lfaq summary { list-style: none; cursor: pointer; padding: 16px 2px; font-family: var(--mono); font-size: 14px; color: var(--text); display: flex; justify-content: space-between; gap: 12px; }
.lfaq summary::-webkit-details-marker { display: none; }
.lfaq summary::after { content: "+"; color: var(--green-dim); font-size: 18px; line-height: 1; }
.lfaq details[open] summary::after { content: "\2212"; }
.lfaq details[open] summary { color: var(--head); }
.lfaq p { font-family: var(--sans); font-size: 13.5px; line-height: 1.65; color: var(--muted); margin: 0 2px 18px; }

/* ── final CTA ───────────────────────────────────────────── */
.lcta { text-align: center; padding: 76px 32px; border: 1px solid var(--line-2); border-radius: 18px; background: var(--surf-2); box-shadow: inset 0 1px 0 var(--green-soft), 0 24px 60px rgba(0, 0, 0, 0.4); }
.lcta h2 { font-family: var(--mono); font-size: clamp(1.6rem, 4vw, 2.4rem); color: var(--head); margin: 0 0 12px; letter-spacing: -0.01em; }
.lcta p { font-family: var(--sans); font-size: 15px; color: var(--muted); margin: 0 0 26px; }

/* ── footer ──────────────────────────────────────────────── */
.lfoot { border-top: 1px solid var(--line-hair); margin-top: 76px; padding: 44px 0 30px; }
.lfoot-cols { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1fr; gap: 28px; }
.lfoot-brand .b { display: flex; align-items: center; gap: 9px; font-family: var(--mono); font-weight: 700; letter-spacing: 0.14em; color: var(--head); }
.lfoot-brand .b .sig { color: var(--green); }
.lfoot-brand p { font-family: var(--sans); font-size: 12.5px; color: var(--faint); line-height: 1.6; margin: 12px 0 0; max-width: 260px; }
.lfoot-col h4 { font-family: var(--mono); font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--faint); margin: 0 0 14px; }
.lfoot-col a { display: block; font-family: var(--sans); font-size: 13px; color: var(--muted); margin: 8px 0; transition: color 0.15s; }
.lfoot-col a:hover { color: var(--green-text); }
.lfoot-bottom { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 36px; padding-top: 22px; border-top: 1px solid var(--line-hair); font-family: var(--mono); font-size: 11px; color: var(--faint); flex-wrap: wrap; }
/* The language switcher used to be a native <select> in this row. It is in the nav now
   (css/langpicker.css, mounted by js/entry/landing.js) — the rules that dressed it down to
   the footer went with it, rather than being left here for a host element that no longer
   exists. index.html records why it moved. */

/* ── dashboard showcase ──────────────────────────────────── */
.lshow { max-width: 1000px; margin: 0 auto; }
.lshow-tabs { display: flex; justify-content: center; flex-wrap: wrap; gap: 8px; margin-bottom: 22px; }
.lshow-tab { font-family: var(--mono); font-size: 12.5px; color: var(--muted); background: var(--surf-2); border: 1px solid var(--line); border-radius: 100px; padding: 8px 18px; cursor: pointer; transition: color 0.15s, border-color 0.15s, background 0.15s; }
.lshow-tab:hover { color: var(--text); border-color: var(--line-2); }
.lshow-tab.on { color: var(--green-text); border-color: var(--green); background: var(--surf-3); box-shadow: 0 0 0 1px var(--green-soft); }
.lshow-frame { border: 1px solid var(--line-2); border-radius: 12px; overflow: hidden; background: var(--surf-2); box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6); }
.lshow-stage { position: relative; aspect-ratio: 1360 / 850; background: var(--bg); }
.lshow-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top left; opacity: 0; transition: opacity 0.45s ease; pointer-events: none; }
.lshow-img.on { opacity: 1; }
.lshow-cap { text-align: center; font-family: var(--sans); font-size: 14px; color: var(--muted); margin: 22px auto 0; max-width: 620px; line-height: 1.55; min-height: 44px; transition: opacity 0.25s; }

/* ── skip link + a11y ────────────────────────────────────── */
.skip-link { position: fixed; top: -60px; left: 12px; z-index: 100; background: var(--green); color: var(--on-accent); font-family: var(--mono); font-size: 12px; padding: 9px 16px; border-radius: 8px; transition: top 0.15s; }
.skip-link:focus { top: 12px; }
#topo { opacity: 0.62; } /* keep the constellation from competing with headline text */

/* ── mobile nav ──────────────────────────────────────────── */
.lnav-burger { display: none; background: var(--surf-2); border: 1px solid var(--line); color: var(--green); border-radius: 8px; width: 38px; height: 34px; font-size: 16px; cursor: pointer; }
/* The panel SCROLLS. .lnav is sticky, so anything in here below the fold cannot be reached by
   scrolling the document — and at the SC 1.4.10 reflow size (320x256, i.e. 400% zoom of
   1280x1024) this panel is taller than the whole viewport, so its last items were simply
   unreachable. That was true of the CTA before the language row was added and is what makes
   it worth fixing rather than reordering. */
.lnav-mobile { display: none; flex-direction: column; gap: 2px; padding: 10px 6vw 16px; border-bottom: 1px solid var(--line-hair); background: color-mix(in srgb, var(--bg) 96%, transparent); backdrop-filter: blur(10px); max-height: calc(100dvh - 58px); overflow-y: auto; overscroll-behavior: contain; }
.lnav-mobile a { font-family: var(--mono); font-size: 14px; color: var(--text); padding: 11px 4px; border-bottom: 1px solid var(--line-hair); }
.lnav-mobile a.btn { border: 1px solid var(--green); justify-content: center; display: flex; margin-top: 10px; color: var(--on-accent); }
.lnav-badge { display: inline-flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.06em; color: var(--green-text); background: var(--green-soft); border: 1px solid var(--green-dim); border-radius: 100px; padding: 4px 12px; }
.lhero-badge { display: inline-flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 11px; color: var(--green-text); background: var(--green-soft); border: 1px solid var(--green-dim); border-radius: 100px; padding: 5px 13px; margin-bottom: 16px; }

/* ── deploy demo terminal ────────────────────────────────── */
.ldemo { max-width: 760px; margin: 0 auto; }
.lterm .mock-bar span { color: var(--green-dim); }
.lterm-body { padding: 18px 20px 22px; font-family: var(--mono); font-size: 13px; line-height: 1.95; min-height: 268px; color: var(--text); }
.lterm-body .p { color: var(--green); }
.lterm-body .cmd { color: var(--head); }
.lterm-body .ok { color: var(--green); }
.lterm-body .dim { color: var(--faint); }
.lterm-body .warn { color: var(--amber); }
.lterm-cur { display: inline-block; width: 8px; height: 14px; background: var(--green); vertical-align: -2px; animation: termBlink 1.05s steps(1) infinite; }
@keyframes termBlink { 50% { opacity: 0; } }

/* ── who it's for ────────────────────────────────────────── */
.lsegs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.lseg { background: var(--surf-2); border: 1px solid var(--line); border-radius: 12px; padding: 24px; transition: border-color 0.16s, transform 0.16s; }
.lseg:hover { border-color: var(--line-2); transform: translateY(-3px); }
.lseg-ic { font-size: 22px; margin-bottom: 12px; }
.lseg h3 { font-family: var(--mono); font-size: 15px; color: var(--head); margin: 0 0 8px; }
.lseg p { font-family: var(--sans); font-size: 13.5px; line-height: 1.6; color: var(--muted); margin: 0; }

/* ── community + launch notify ───────────────────────────── */
.lcommunity { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.lcard-cta { background: var(--surf-2); border: 1px solid var(--line); border-radius: 14px; padding: 30px 28px; }
.lcard-cta.disc { border-color: var(--line-2); }
.lcard-cta h3 { font-family: var(--mono); font-size: 18px; color: var(--head); margin: 12px 0 8px; }
.lcard-cta p { font-family: var(--sans); font-size: 14px; line-height: 1.6; color: var(--muted); margin: 0 0 18px; }
.lcard-eyebrow { font-family: var(--mono); font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--green-dim); }
.lnotify-form { display: flex; gap: 8px; flex-wrap: wrap; }
.lnotify-form input { flex: 1; min-width: 180px; background: var(--inset); border: 1px solid var(--line); border-radius: 9px; padding: 11px 14px; color: var(--head); font-family: var(--mono); font-size: 13px; outline: none; }
.lnotify-form input:focus { border-color: var(--green); }
.lnotify-msg { margin-top: 11px; font-family: var(--mono); font-size: 12px; min-height: 16px; color: var(--green-text); }
.lnotify-msg.err { color: var(--red); }
/* Consent fine print: withdrawal has to be as visible as the signup that asks for it. */
.lnotify-fine { margin-top: 9px; font-size: 11px; line-height: 1.6; color: var(--muted); }
.lnotify-fine a { color: var(--green-dim); text-decoration: underline; }
.lnotify-fine a:hover { color: var(--cyan); }

/* ── comparison table ────────────────────────────────────── */
.lcompare { max-width: 900px; margin: 0 auto; overflow-x: auto; border: 1px solid var(--line); border-radius: 12px; }
.lcompare table { width: 100%; border-collapse: collapse; font-family: var(--sans); font-size: 13.5px; min-width: 560px; }
.lcompare th, .lcompare td { padding: 13px 16px; text-align: center; border-bottom: 1px solid var(--line-hair); }
.lcompare th:first-child, .lcompare td:first-child { text-align: left; }
.lcompare thead th { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); background: var(--surf-1); }
.lcompare thead th.pop { color: var(--green-text); }
.lcompare td.feat { color: var(--muted); font-family: var(--sans); }
.lcompare .yes { color: var(--green); font-family: var(--mono); }
.lcompare .no { color: var(--faint); }
.lcompare .val { color: var(--text); font-family: var(--mono); font-size: 12.5px; }
.lcompare tbody tr:last-child td { border-bottom: 0; }
.lcompare tbody tr:hover { background: var(--surf-1); }
.lprice-reassure { text-align: center; font-family: var(--mono); font-size: 12px; color: var(--faint); margin-top: 18px; }
.lprice-reassure b { color: var(--green-dim); }

/* ── showcase pause control ──────────────────────────────── */
.lshow-pause { font-family: var(--mono); font-size: 11px; color: var(--faint); background: none; border: 0; cursor: pointer; margin-top: 6px; }
.lshow-pause:hover { color: var(--green-dim); }

/* ── scroll reveal (staggered) ───────────────────────────── */
.reveal { opacity: 0; transform: translateY(18px); transition: opacity 0.6s cubic-bezier(0.2, 0.7, 0.2, 1), transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1); will-change: opacity, transform; }
.reveal.in { opacity: 1; transform: none; }
.lfeatures .reveal:nth-child(3n + 2), .lsteps .reveal:nth-child(3n + 2), .lpricing .reveal:nth-child(3n + 2), .ltrust-grid .reveal:nth-child(4n + 2), .ltestis .reveal:nth-child(3n + 2) { transition-delay: 0.08s; }
.lfeatures .reveal:nth-child(3n), .lsteps .reveal:nth-child(3n), .lpricing .reveal:nth-child(3n), .ltrust-grid .reveal:nth-child(4n + 3), .ltestis .reveal:nth-child(3n) { transition-delay: 0.16s; }
.ltrust-grid .reveal:nth-child(4n) { transition-delay: 0.24s; }

/* ── live hero mockup ────────────────────────────────────── */
.mock-badge .live { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--green); margin-right: 5px; animation: pulseDot 2s ease-in-out infinite; }
@keyframes pulseDot { 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 55%, transparent); } 50% { box-shadow: 0 0 0 5px transparent; } }
.mock-spark polyline.line { stroke-dasharray: 1; stroke-dashoffset: 1; }
/* The area wash under the sparkline is authored as a literal green in the SVG (a paint
   fallback for no-CSS); CSS re-derives it from the accent so a themed page isn't a green
   smear under a red line. */
.mock-spark polyline:not(.line) { fill: color-mix(in srgb, var(--green) 9%, transparent); }
.mock-spark.drawn polyline.line { animation: drawLine 1.6s ease forwards; }
@keyframes drawLine { to { stroke-dashoffset: 0; } }
.mock-console .row { opacity: 0; animation: logIn 0.4s ease forwards; }
@keyframes logIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* ── reliability band ────────────────────────────────────── */
.ltrust-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.ltrust { background: var(--surf-2); border: 1px solid var(--line); border-radius: 12px; padding: 22px 20px; }
.ltrust-ic { color: var(--green); margin-bottom: 12px; }
.ltrust-ic svg { width: 22px; height: 22px; }
.ltrust h3 { font-family: var(--mono); font-size: 13.5px; color: var(--head); margin: 0 0 6px; }
.ltrust p { font-family: var(--sans); font-size: 12.5px; line-height: 1.55; color: var(--muted); margin: 0; }

/* ── testimonials ────────────────────────────────────────── */
.ltestis { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.ltesti { display: flex; flex-direction: column; background: var(--surf-2); border: 1px solid var(--line); border-radius: 12px; padding: 22px; }
.ltesti-q { font-family: var(--sans); font-size: 14px; line-height: 1.6; color: var(--text); margin: 0 0 18px; flex: 1; }
.ltesti-q::before { content: "\201C"; color: var(--green); font-family: var(--mono); font-size: 22px; margin-right: 2px; }
.ltesti-who { display: flex; align-items: center; gap: 10px; }
.ltesti-av { width: 32px; height: 32px; border-radius: 8px; background: var(--surf-3); border: 1px solid var(--green-dim); display: grid; place-items: center; font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--green); flex: none; }
.ltesti-name { font-family: var(--mono); font-size: 12.5px; color: var(--head); }
.ltesti-role { font-family: var(--sans); font-size: 11.5px; color: var(--faint); }

/* ── theme preview swap ──────────────────────────────────────
   The picker repaints the page by swapping <html data-theme>, which is otherwise
   instant. This class rides along for the length of one cross-fade and is then
   removed, so no element carries a transition it didn't ask for outside that
   window. landing.js never adds it under reduced motion.

   THE STAGE IS EXCLUDED. Its theme wash fires on every hover, not just on a commit,
   so it carries its own permanent colour transitions instead (see below) — and this
   rule is a blanket `transition` SHORTHAND at (0,3,0), which would otherwise outrank
   the stage's own rules and reset transition-property on them. That matters during the
   collapse, whose height and transform are not in the list above: the section would
   snap shut instead of collapsing, and a panel mid-morph would pick up a 0.38s ease
   it never asked for. Excluding the subtree is cheaper than a specificity war. */
:root.theme-swap body, :root.theme-swap .landing, :root.theme-swap .landing *:not(.lstage, .lstage *) {
  transition: background-color 0.38s ease, border-color 0.38s ease, color 0.38s ease, box-shadow 0.38s ease,
    fill 0.38s ease, stroke 0.38s ease, opacity 0.38s ease, transform 0.38s ease;
}

/* ── a11y + focus ────────────────────────────────────────── */
.landing a:focus-visible, .lfaq summary:focus-visible { outline: 2px solid var(--green); outline-offset: 3px; border-radius: 4px; }
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1 !important; transform: none !important; transition: none !important; } .mock-badge .live, .mock-spark.drawn polyline.line, .mock-console .row { animation: none !important; } .mock-spark polyline.line { stroke-dashoffset: 0; } html { scroll-behavior: auto; } }
:root[data-motion="reduced"] .reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
:root[data-motion="reduced"] .mock-badge .live, :root[data-motion="reduced"] .mock-spark.drawn polyline.line, :root[data-motion="reduced"] .mock-console .row { animation: none !important; }

/* ── responsive ──────────────────────────────────────────── */
@media (max-width: 900px) {
  .lhero { grid-template-columns: 1fr; gap: 36px; padding: 44px 0; }
  .lhero-visual { order: -1; }
  .lfeatures, .lsteps, .lpricing, .ltestis, .lsegs, .lcommunity { grid-template-columns: 1fr; }
  .lstats, .ltrust-grid { grid-template-columns: repeat(2, 1fr); }
  .lnav-links, .lnav-signin { display: none; }
  .lnav-burger { display: block; }
  .lnav-mobile.open { display: flex; }
  .lfoot-cols { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 520px) {
  .lstats, .ltrust-grid { grid-template-columns: 1fr; }
  .lnav-cta { display: none; } /* the CTA lives in the burger menu at this width */
  .lfoot-cols { grid-template-columns: 1fr; }
}

/* ── THE STAGE ────────────────────────────────────────────────────────────────
   The full-screen game chooser that opens the page for a first-time visitor
   (markup + the heading-level reasoning: index.html, section#lstage).

   Kept whole and last rather than folded into the sections above, because it is one
   feature with a lifecycle — reserved, filled, hovered, collapsed — and reading it in
   one piece is worth more than filing eight fragments by topic. It also has to come
   after the .theme-swap block to win the source-order tie-breaks it depends on.

   NO COLOUR IS SPELLED OUT ANYWHERE BELOW. Every surface, line and glyph is a token,
   so the hover wash — the script swaps <html data-theme> to the hovered game's theme,
   exactly as the hero picker already does — repaints the whole stage for free, in any
   theme a pack ever declares, with no rule added here. */

.lstage {
  /* .lnav-inner is 58px and sticky, so it occupies the top of the flow: the stage takes
     what is left and the pair fills the first screen exactly — nothing about the stage is
     below the fold, including the way out of it. dvh with a vh fallback (a browser that
     doesn't parse dvh keeps the first declaration). */
  --lstage-nav: 58px;
  --lstage-panel-h: clamp(258px, 40vh, 420px);
  --lstage-ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  /* FAIL CLOSED. Rendered only where js/theme-boot.js has decided, pre-paint, that this
     visitor has never chosen. Attribute absent (no JS, blocked storage, a crawler that
     doesn't run script) and data-stage="off" (returning visitor, or the moment a choice
     lands) both fall through to this: display:none, zero box, zero reserved space, and a
     hero that sits exactly where it sat before this section was written. */
  display: none;
  position: relative;
  z-index: 1;
  flex-direction: column;
  justify-content: center;
  width: min(1240px, 92vw);
  margin: 0 auto;
  padding: 30px 0 36px;
  min-height: calc(100vh - var(--lstage-nav));
  min-height: calc(100dvh - var(--lstage-nav));
  border-bottom: 1px solid var(--line-hair);
  /* The section is transparent on purpose — #topo's constellation runs behind it, and
     glTopo already re-reads its palette from the tokens when data-theme changes, so the
     ground washes with the panels instead of sitting under them as a green constant. */
}
/* The single switch. Everything that ends with no picker also ends with data-stage="off"
   (entry/landing.js: a non-200, a bad payload, an empty list, or a feed that has not
   answered in six seconds), which is the same door the returning visitor came through —
   so a chooser with nothing to choose is never a viewport-height hole. */
:root[data-stage="on"] .lstage { display: flex; }

.lstage-eyebrow { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.28em; text-transform: uppercase; color: var(--green-dim); margin: 0; }
.lstage-title {
  position: relative; /* containing block for the backdrop quiet-zone below */
  font-family: var(--mono); font-size: clamp(1.95rem, 4.6vw, 3.1rem); line-height: 1.06;
  letter-spacing: -0.02em; font-weight: 700; color: var(--head); margin: 10px 0 0;
  transition: color 0.34s ease;
}
/* THE BACKDROP'S QUIET ZONE. #topo's constellation runs behind this section on purpose
   (see .lstage), but it was laid out to thread the HERO's gutter — and the stage's headline
   runs straight through that same channel, so a node marker and its "MASTER" label print
   through the largest type on the page: measured 285px of backdrop ink inside the headline
   box at 1440x900, 309 at 1280x900, and 4976px (5.3% of the box) at 1024x768, where the
   marker lands on the ascender of "changes". On the first screen a new visitor ever sees,
   that reads as a rendering fault.

   So the wash is kept and the ground is put back only where the words are: one soft radial
   of --bg, in the NEGATIVE-Z LAYER of .lstage's own stacking context (the section is
   position:relative z-index:1). That is above #topo and below everything the section itself
   paints — including the eyebrow, which sits inside the top inset — so it hides the
   constellation without hiding a single thing the stage draws. Feathered to nothing well
   before the rack, because the panels are exactly where the constellation should still show. */
.lstage-title::before {
  content: "";
  position: absolute;
  z-index: -1;
  /* Inset horizontally to ZERO, not bled outwards: the section is already width:min(1240px,
     92vw) and the headline never fills it, so there is nothing to reach for — and an
     absolutely-positioned child that pokes past the section counts towards scrollWidth,
     which put 5-13px of horizontal page overflow on every narrow viewport (measured). The
     one direction on this page that must never scroll. */
  inset: -40px 0 -20px;
  pointer-events: none;
  background: radial-gradient(
    72% 128% at 30% 50%,
    color-mix(in srgb, var(--bg) 92%, transparent) 0%,
    color-mix(in srgb, var(--bg) 64%, transparent) 46%,
    transparent 76%
  );
}
.lstage-title span { display: block; }
/* Same accent treatment the hero headline uses — this page has one display voice. */
.lstage-title .hl { color: var(--green); text-shadow: var(--glow); transition: color 0.34s ease; }

.lstage-rack {
  display: grid;
  /* auto-fit collapses the tracks it can't fill, so the three games this deployment sells
     become three equal columns rather than three-in-four; a deployment that enables more
     wraps into equal columns instead of shrinking them to slivers. min() keeps the track
     from outgrowing a narrow viewport before the stacked layout takes over. */
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: 14px;
  flex: 0 0 auto;
  margin: 26px 0 0;
}
/* The rack is empty from first paint until the feed answers, and the section is already
   on screen by then. This holds the exact box the panels land in — same height, same
   radius, same 1px — so their arrival moves nothing, and so a slow feed shows a surface
   that is plainly still filling rather than a viewport-height void. A feed that never
   answers is the script's job, and it uses the one switch above rather than a class of
   its own: entry/landing.js flips <html data-stage="off">, which is the same door the
   returning visitor comes through. */
.lstage-rack:empty::before {
  content: "";
  grid-column: 1 / -1;
  /* base.css sets box-sizing on `*`, which does not match a pseudo-element, and box-sizing
     is not inherited — without this the 1px border lands OUTSIDE the reserved height and
     the rack measures 362 empty against the panels' 360, i.e. the two-pixel shift this
     placeholder exists to prevent. */
  box-sizing: border-box;
  min-height: var(--lstage-panel-h);
  background: var(--surf-2);
  border: 1px solid var(--line-hair);
  border-radius: var(--r-lg);
}

/* ── panels ──────────────────────────────────────────────────────────────────
   A poster: procedural FX up top, the words anchored along the bottom edge. Flat
   --surf-2 plate, one hairline, the same hover vocabulary as .lfeat and the same
   selected vocabulary as .lpick-btn — this is the page's third selector, not a new
   component language. */
.lstage-panel {
  position: relative;
  isolation: isolate; /* keep the FX/scrim/text stack local to the panel */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-start;
  gap: 7px;
  min-width: 0; /* a grid item's automatic minimum is min-content — see .lhero-copy */
  min-height: var(--lstage-panel-h);
  padding: 20px 22px 22px;
  text-align: left;
  cursor: pointer;
  font-family: var(--mono);
  color: var(--text);
  background: var(--surf-2);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  transition: border-color 0.28s ease, background-color 0.28s ease, box-shadow 0.28s ease,
    color 0.28s ease, transform 0.28s var(--lstage-ease);
}
.lstage-panel:hover { border-color: var(--green); transform: translateY(-3px); box-shadow: 0 18px 44px rgba(0, 0, 0, 0.42); }
.lstage-panel:focus-visible { border-color: var(--green); outline: 2px solid var(--green); outline-offset: 3px; }
.lstage-panel[aria-checked="true"] { border-color: var(--green); background: var(--surf-3); box-shadow: 0 0 0 1px var(--green-soft), 0 18px 44px rgba(0, 0, 0, 0.42); }

/* The FX lane paints into this. It is pinned behind everything and capped well under full
   strength: the scrim below guarantees the words, this keeps the plate reading as a surface
   in the design system rather than as a lightbox. */
.lstage-panel-fx {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0.48;
  transition: opacity 0.28s ease;
}
.lstage-panel:hover .lstage-panel-fx,
.lstage-panel:focus-visible .lstage-panel-fx,
.lstage-panel[aria-checked="true"] .lstage-panel-fx { opacity: 0.66; }

/* Legibility scrim, and the opacity cap above is half of it. The canvas belongs to another
   lane and may paint ANY brightness, so contrast cannot be left to what it happens to draw:
   the band the words occupy is put back onto near-solid ground before they are painted on it.

   THE GUARANTEE IS RELATIVE, and stated that way on purpose: every line reads at or above
   the contrast that same token reaches on a bare --surf-2 plate elsewhere on this page, in
   EVERY theme — so the FX lane cannot cost legibility anything, whatever it paints and
   whatever colours a pack declares. An absolute figure here would be a phosphor-only claim:
   --bg, --head, --muted and --faint are all theme tokens, and measuring one theme's numbers
   and calling them the floor is how a 4.1 turns into a 2.9 on another palette.

   The stops are derived from the worst case that can physically occur — a canvas filled
   solid white, at the 0.66 the hover state allows, which composites over --surf-2 to ~187 —
   evaluated at the heights the three lines MEASURE at on a 404x360 desktop panel (the text
   is bottom-anchored: name 68-75%, tag 77-88%, specs 88-96%). In phosphor, for scale:
     name  --head/--green-text on ~80% scrim → ground 43 → 11.0:1
     tag   --muted             on ~85% scrim → ground 32 →  7.4:1
     specs --muted             on ~92% scrim → ground 21 →  8.4:1

   THE SPEC LINE IS --muted, NOT --faint, and that is the one absolute here. --faint is the
   design system's de-emphasised META token and measured 4.09:1 on --surf-2 — under WCAG 2.1
   SC 1.4.3's 4.5:1 for text this size, which is survivable for a decorative caption and is
   not survivable for the hardware line, the only information on the plate a visitor is
   actually comparing between games ("2 GiB RAM · 1 vCPU · up to 100 players"). Size, case,
   family and tracking already carry the hierarchy under the tagline; the colour does not
   have to, and it was the only sub-threshold token on the whole stage. Measured on this
   worst-case ground in all eight shipped themes, the spec line now runs 5.56:1 (matrix,
   where --faint was 2.72) to 8.37:1 (phosphor) — every one of them clear of 4.5.

   This is the one gradient on a stage of flat surfaces, and it is deliberate: an overlay
   doing a legibility job, not an element background faking elevation, which is what the
   design system's flat-surfaces rule is actually about. A hard-edged flat plate cutting
   across a procedural field reads as a rendering bug. */
.lstage-panel::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent 0%,
    color-mix(in srgb, var(--bg) 30%, transparent) 40%,
    color-mix(in srgb, var(--bg) 78%, transparent) 66%,
    color-mix(in srgb, var(--bg) 90%, transparent) 84%,
    color-mix(in srgb, var(--bg) 95%, transparent) 100%
  );
}
/* Selection indicator: a hairline that wipes across the top edge. content:"" so it never
   joins the button's accessible name (a counter or a glyph here would). */
.lstage-panel::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 2;
  background: var(--green);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.3s var(--lstage-ease), background-color 0.28s ease;
}
.lstage-panel:hover::before,
.lstage-panel:focus-visible::before,
.lstage-panel[aria-checked="true"]::before { transform: scaleX(1); }

.lstage-panel-name,
.lstage-panel-tag,
.lstage-panel-specs { position: relative; z-index: 2; }
.lstage-panel-name { font-family: var(--mono); font-size: clamp(1.05rem, 1.5vw, 1.4rem); font-weight: 700; letter-spacing: -0.01em; color: var(--head); transition: color 0.28s ease; }
.lstage-panel:hover .lstage-panel-name,
.lstage-panel:focus-visible .lstage-panel-name,
.lstage-panel[aria-checked="true"] .lstage-panel-name { color: var(--green-text); }
/* Feed copy, so its length is not this file's to trust: clamped to three lines, which is
   what the schema's tagline cap comes to at a column of this width. */
.lstage-panel-tag {
  font-family: var(--sans); font-size: 13.5px; line-height: 1.5; color: var(--muted);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;
}
/* TWO LINES, NOT ONE WITH AN ELLIPSIS. A single tracked line does not fit this column at
   several ordinary widths, and what it dropped was always the tail — "· up to N players",
   the one comparative number the stage exists to surface. Measured clipped on ALL THREE
   panels at 1024x768, 844x390, 360x640 and 320x568; 1024x768 is a plain laptop, and it is
   exactly the width at which a visitor is weighing 100 against 32 against 300. gameStage.js
   joins each figure internally with NBSP, so a wrap can only fall on a "·" separator and
   never inside "2 GiB RAM". The clamp is the backstop for a pack whose numbers are longer
   still: the panel is bottom-anchored, so a second line grows upward into scrim that is
   thicker, not thinner. */
.lstage-panel-specs {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase;
  line-height: 1.45; color: var(--muted); max-width: 100%;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}

/* ── skip ────────────────────────────────────────────────────────────────────
   The explicit way past. Quiet, but a real bordered control rather than a link-coloured
   afterthought — it is the escape hatch, and a visitor who does not want to choose must
   be able to see it without hunting. Escape and simply scrolling do the same thing. */
.lstage-skip {
  align-self: center;
  margin: 30px 0 0;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--mono); font-size: 12px; letter-spacing: 0.06em;
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 100px;
  padding: 9px 18px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background-color 0.15s;
}
.lstage-skip:hover { color: var(--head); border-color: var(--line-2); background: var(--surf-2); }
.lstage-skip:focus-visible { outline: 2px solid var(--green); outline-offset: 3px; }
/* --muted, not --faint, for the same reason as the spec line: this keycap is the shortcut
   for the escape hatch, 10px is well inside SC 1.4.3's small-text bracket, and --faint
   measured 4.25:1 on --surf-1 here. A visitor who cannot read "Esc" has lost one of the
   three ways past a full-screen section. --muted measures 5.77:1 (matrix) to 8.77:1
   (phosphor) in the same place, across all eight themes. */
.lstage-skip kbd { font-family: var(--mono); font-size: 10px; color: var(--muted); border: 1px solid var(--line); border-radius: var(--r-xs); padding: 1px 5px; background: var(--surf-1); }
.lstage-skip:hover kbd { color: var(--head); }

/* ── the flying panel ────────────────────────────────────────────────────────
   js/ui/gameStage.js does not collapse the stage with an animation: it flips
   <html data-stage="off"> synchronously, which retires the section in one frame, and
   morphs the CHOSEN PANEL ITSELF — the node, not a clone — into that game's pill. The
   panel is lifted onto <body> and pinned to the viewport coordinates it already occupied
   (position/left/top/width/height/z-index, all set inline so the collapse underneath it
   cannot move it a pixel), then one FLIP transform + opacity takes it to the pill's rect.
   Its FX canvas is deliberately kept alive for the whole flight.

   Everything geometric there is inline and outranks this file, which is right — only
   script can measure a rect. What is left for CSS is the one thing FLIP is bad at: the
   panel ends at roughly a tenth of its own height, so any glyph still painted is a smear
   long before it arrives. The words are dropped almost immediately (the plate, its border
   and its procedural field are what should read as moving), and `transition: none` keeps
   the panel's own 0.28s hover transitions from firing on the frame it is pinned. */
.lstage-flyer {
  will-change: transform, opacity;
  transition: none;
}
.lstage-flyer .lstage-panel-name { opacity: 0; transition: opacity 0.13s linear; }
.lstage-flyer .lstage-panel-tag,
.lstage-flyer .lstage-panel-specs { opacity: 0; transition: opacity 0.1s linear; }

/* ── the page rising into view ───────────────────────────────────────────────
   The other half of the collapse. The stage leaves instantly, so without this the hero
   would simply be there — a cut, not a reveal. gameStage.js carries <html> through
   .lstage-rising for the length of the flight; this lifts the page in behind the panel.

   #lpick IS EXCLUDED, AND THAT IS LOAD-BEARING. It holds the pill the panel is flying
   into, and the module measures that pill's rect one frame after this class goes on. Any
   transform on the pill or an ancestor of it would be measured as its resting position
   and the flight would land wherever the animation happened to be at that instant. So the
   picker stays put, at full opacity, and the page assembles around it — which is also the
   better reading: the thing the visitor just chose is already there, waiting. */
@keyframes lstageRise {
  from { opacity: 0; transform: translateY(22px); }
  to { opacity: 1; transform: none; }
}
:root.lstage-rising .lhero-copy > *:not(.lpick),
:root.lstage-rising .lhero-visual,
:root.lstage-rising .landing > section:not(.lhero):not(.lstage) {
  animation: lstageRise 0.46s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
:root.lstage-rising .lhero-visual { animation-delay: 0.05s; }
:root.lstage-rising .lhero-copy > *:nth-child(3) { animation-delay: 0.04s; }
:root.lstage-rising .lhero-copy > *:nth-child(4) { animation-delay: 0.07s; }
:root.lstage-rising .lhero-copy > *:nth-child(n + 6) { animation-delay: 0.1s; }

/* ── reduced motion ──────────────────────────────────────────────────────────
   No morph and no rise. gameStage.js already takes the other branch — it cross-fades the
   section out on its own opacity and never pins a panel — so this is the belt to that
   braces: nothing here can fly or lift even if something upstream changes its mind.
   (With the site's own data-motion="reduced", themes.css additionally forces every
   duration on the page to 0.001ms, so that path is simply instantaneous — the most
   reduced reading of the setting, and the visitor asked for it explicitly.) */
@media (prefers-reduced-motion: reduce) {
  .lstage-panel, .lstage-panel::before, .lstage-panel-fx, .lstage-title, .lstage-skip { transition: none; }
  .lstage-panel:hover { transform: none; }
  .lstage-panel::before { transform: scaleX(1); opacity: 0; }
  .lstage-panel:hover::before, .lstage-panel:focus-visible::before, .lstage-panel[aria-checked="true"]::before { opacity: 1; }
  :root.lstage-rising .lhero-copy > *,
  :root.lstage-rising .lhero-visual,
  :root.lstage-rising .landing > section { animation: none; }
}
:root[data-motion="reduced"] .lstage-panel:hover { transform: none; }

/* ── stage, narrow ───────────────────────────────────────────────────────────
   Panels become a full-width stack that absorbs the slack (grid rows share what the flex
   parent hands the rack), so a phone gets a screen that is filled rather than three
   letterbox strips floating in the middle of it. Deliberately NOT an inner scroll
   container: on a touch screen that would swallow the page scroll, and scrolling past
   the stage is one of the three ways out of it. A deployment selling enough games to
   overflow the screen simply makes the section taller and the page scrolls, which is the
   behaviour that cannot trap anyone. */
@media (max-width: 900px) {
  .lstage {
    --lstage-panel-h: clamp(104px, 14vh, 168px);
    justify-content: flex-start;
    padding: 22px 0 28px;
  }
  .lstage-title { margin-top: 8px; }
  .lstage-rack {
    flex: 1 1 auto;
    grid-template-columns: 1fr;
    grid-auto-rows: minmax(var(--lstage-panel-h), 1fr);
    gap: 10px;
    margin-top: 20px;
  }
  .lstage-rack:empty::before { min-height: 100%; }
  .lstage-panel { justify-content: center; gap: 4px; padding: 16px 18px; }
  .lstage-panel-tag { -webkit-line-clamp: 2; font-size: 12.5px; }
  .lstage-skip { margin-top: 20px; }
  /* These plates are short and wide and their words sit in the MIDDLE of them, not along
     the bottom edge (measured 27-73% of a 359x172 panel), so the scrim covers the whole
     face rather than ramping up to it — which is also why entry/landing.js does not mount
     the FX lane at this width at all: a scrim that starts at 68% of --bg on the panel's TOP
     edge leaves the procedural field a maximum channel delta of 6/255 (measured), i.e. the
     noise floor, so three canvases, three rAF loops and ~2.3MB of bitmaps were being spent
     on something no phone could see. The scrim stays: it is what gives the plate its tone,
     and it is still the guarantee if a future width mounts a canvas here.
     Same worst case as above, re-evaluated at these heights:
       name  on ~79% scrim → ground 44 → 10.9:1
       tag   on ~85% scrim → ground 34 →  7.3:1
       specs on ~91% scrim → ground 23 →  8.2:1 (--muted; 5.49 to 8.24 across the eight) */
  .lstage-panel::after {
    background: linear-gradient(
      to bottom,
      color-mix(in srgb, var(--bg) 68%, transparent) 0%,
      color-mix(in srgb, var(--bg) 88%, transparent) 50%,
      color-mix(in srgb, var(--bg) 96%, transparent) 100%
    );
  }
}
/* Short viewports — a desktop window dragged down, a phone held sideways, and the one that
   matters most: 400% zoom, which is 320x256 of CSS pixels and is the condition WCAG 1.4.10
   is written about. The stage owns one screen wherever it can, so the poster proportions
   give way first; below roughly 540px of height it CANNOT — three dignified panels plus a
   headline are 2.5 screens at 256px, and no floor small enough to fix that is a panel any
   more. There is no horizontal overflow at any size (measured 0 at every viewport tested),
   so 1.4.10 itself holds and the content is reachable by scrolling in one direction. What
   does not hold at that size is this file's other promise — that the way out is on screen —
   so the skip control is pinned instead of shrunk. */
@media (max-height: 640px) {
  .lstage { --lstage-panel-h: clamp(112px, 32vh, 240px); padding: 18px 0 22px; }
  .lstage-title { font-size: clamp(1.35rem, 3vw, 2rem); }
  .lstage-rack { margin-top: 16px; }
  /* THE WAY OUT, PINNED. Measured before this: on a 320x256 viewport the stage ran to 641px
     and the skip button sat at y=642 — below the fold for precisely the visitor who zooms.
     Sticky holds it against the bottom edge for as long as the section is on screen, so it
     is reachable from the first frame at every height, and it costs a viewport that DOES
     fit nothing at all: a sticky element that never leaves its flow position never moves.
     DOM order is untouched, so the focus order still matches the reading order — which is
     why this is sticky rather than a flex `order` swap. It needs the page's own ground
     under it because it now floats over a panel. */
  .lstage-skip { position: sticky; bottom: 12px; z-index: 3; margin-top: 16px; background: var(--bg); }
}
/* Narrow AND short is a phone in landscape, and it is the one case where the two rules
   above disagree: width alone picks the stacked column, and three stacked rows do not fit
   in 390px of height at any dignified size (measured 662px of stage on an 844x390 screen).
   That viewport has width to spare and no height at all, so the rack goes back to columns.

   It also has to take the panel height back. `max-height: 640px` matches a 320x568 phone in
   PORTRAIT as readily as a landscape one, and there the short-viewport floor above was
   ENLARGING the panels the narrow rules had just sized down — 32vh of 568 is 182px against
   the 104px the phone layout asks for, which is how three panels came to 766px of stage on
   a 568px screen. This is the more specific of the two viewports, so it states its own. */
@media (max-width: 900px) and (max-height: 640px) {
  .lstage { --lstage-panel-h: clamp(96px, 22vh, 168px); }
  .lstage-rack {
    flex: 0 0 auto;
    /* 200px, not 160: auto-fit takes every column the track minimum lets it, and 160 let a
       360x640 PORTRAIT phone — which is short enough to land in this rule — split into two
       128px text columns, where the tagline ran three words to a line and the spec line was
       clipped on all three panels. 200 still gives the landscape phone this rule is for its
       three columns (844x390: 776px of content, three ~249px tracks) and gives the portrait
       one the single column the narrow layout above already chose for it. */
    grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
    grid-auto-rows: auto;
  }
}
