/* VOIDLINE INTERACTIVE — compiled stylesheet. Source: src/styles/*.css. */
/* VOIDLINE INTERACTIVE — Design Tokens
   Implements ARCHITECTURE.md §3 (Design DNA). Do not add ad-hoc colors,
   spacing, or type sizes outside this file — extend the scale instead. */

:root {
  /* ---- Color: void-neutral foundation + Cinematic Spectral role-split ----
     PROMOTED SITEWIDE (Production Master Build — see
     docs/COLOR_UI_DESIGN_SYSTEM_v1.0.md and the brief's §04/§72-B).
     Signal Lime (#c8ff3d) is RETIRED from all production-facing UI,
     sitewide, effective this phase — not just Home. This supersedes the
     three prior Home-only phases (v0.4 Home Experience, the disapproved
     flat cyan re-swap, and its corrective role-split fix): the same
     role-split that was proven correct and Product-Owner-approved on
     Home now becomes the site's one and only accent system. There is no
     more "Home vs. everywhere else" color fork.
     Foundation tones (--void-*) are kept as the neutral near-black scale
     that was already sitewide-authoritative — this is a deliberate,
     documented assumption (IMPORTANT, brief §29): Home's richer,
     violet-tinted graphite (#1b1730 etc.) was tuned and screenshot-
     verified specifically for Home's dense, atmosphere-heavy
     composition and stays scoped to `.home` in home.css; text-dense
     utility pages (legal, FAQ, careers, contact) keep the calmer
     neutral foundation for readability while still using the full
     spectral accent system for every interactive/brand element. Both
     satisfy the brief's "DARK STRUCTURAL FOUNDATION" requirement. */
  --void-000: #0a0b0c;   /* page background */
  --void-100: #131518;   /* raised surface / panel */
  --void-150: #191c20;   /* hover surface */
  --void-200: #23262b;   /* hairline borders, dividers */
  --ink-000: #f4f5f2;    /* primary text on void */
  --ink-100: #a9aeb4;    /* secondary text on void */
  --ink-200: #6c7178;    /* tertiary / disabled text */
  --alert: #ff4b3e;      /* error states only */
  --alert-dim: #2a1512;  /* error surface tint */

  /* ---- Typography ----
     PROMOTED SITEWIDE (Production Master Build §06, LOCKED):
       DISPLAY/BRAND  → Syne          (headlines, cinematic statements)
       BODY/UI        → Manrope       (body copy, nav, buttons, forms)
       TECHNICAL/META → JetBrains Mono (accent only — eyebrows, indices,
                                         labels; never body copy or the
                                         default button/nav typeface)
     No new font family may be introduced without explicit Product Owner
     approval.

     FONT DELIVERY — BLOCKED (dependency, not silently substituted):
     self-hosting Syne/Manrope/JetBrains Mono was attempted across every
     prior phase (fonts.googleapis.com, fonts.gstatic.com, npm's
     @fontsource packages, cdnjs.cloudflare.com, cdn.jsdelivr.net,
     unpkg.com, GitHub raw/codeload) and every path was rejected by this
     sandbox's network policy (documented since v0.1 — ARCHITECTURE.md
     §3.2). The stack below requests "Syne"/"Manrope"/"JetBrains Mono"
     by name sitewide; with no local @font-face supplying those files,
     the browser's own fallback mechanism resolves to the system-ui
     stack that follows each name — an honest, disclosed BLOCKED
     dependency, never a silent substitution of a different family.
     Dropping the font files into src/assets/fonts/ and adding the
     @font-face block is the only change required once network/asset
     access allows it. */
  --font-display: "Syne", ui-sans-serif, system-ui, "Segoe UI", -apple-system, sans-serif;
  --font-body: "Manrope", ui-sans-serif, system-ui, "Segoe UI", -apple-system, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;

  --text-2xs: 0.6875rem;  /* 11px — mono eyebrows */
  --text-xs: 0.8125rem;   /* 13px */
  --text-sm: 0.9375rem;   /* 15px */
  --text-base: 1.0625rem; /* 17px — body */
  --text-md: 1.25rem;     /* 20px */
  --text-lg: 1.75rem;     /* 28px */
  --text-xl: 2.5rem;      /* 40px */
  /* v0.11.2 TYPOGRAPHY QA CORRECTION: --text-2xl was a FIXED 56px with no
     responsive reduction, relying entirely on base.css's h1-h4
     `overflow-wrap: anywhere` rule to avoid narrow-viewport overflow —
     a mechanism that prevents overflow by permitting a mid-word
     character split, which VOIDLINE's typography rule explicitly
     forbids (words must never be split internally). This is the exact
     failure mode docs/HOME_TYPOGRAPHY_GEOMETRY_v0.4.md §5 already
     identified and fixed for Home's own headline classes
     (`.home-headline` etc. in home.css) with a fluid clamp — but every
     OTHER h1 across the site (About/Careers/Contact/Create-a-Game-With-
     Us/FAQ/Games/Legal/404) and Game Detail's `h2.world-hook` still read
     this token directly with no such protection. Once v0.11.1 made
     Game Detail's world-hook a real, variable-length TRANSLATED string
     instead of static English text, this gap was exposed for real
     (confirmed: "Königliche" visibly rendered as "Königli"/"che" across
     two lines on /de/games/royal-ace/ at 320-390px — a genuine, visible
     defect, not a theoretical risk). FIX: apply the exact same clamp
     values already tuned and screenshot-verified for Home's headlines,
     to the shared token itself, so every h1/h2 consumer is fixed in one
     place rather than patched individually — reaches the original 56px
     at ~1024px and holds there through wide desktop, so current desktop
     appearance is unchanged (see base.css's h1-h4 rule for the paired
     `overflow-wrap: anywhere` removal). */
  --text-2xl: clamp(1.85rem, 1.3rem + 3.8vw, 3.5rem); /* 56px desktop, fluid below ~1024px */
  --text-3xl: clamp(3rem, 3rem + 4vw, 6.5rem); /* hero, fluid */

  --lh-tight: 1.05;
  --lh-snug: 1.25;
  --lh-normal: 1.55;

  --tracking-wide: 0.02em;
  --tracking-wider: 0.06em;
  --tracking-widest: 0.14em;

  /* ---- Spacing: irregular section cadence, 4px base unit ---- */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;
  --space-9: 6rem;
  --space-10: 9rem;
  --space-11: 12rem;

  /* ---- Grid ---- */
  --content-max: 84rem;
  --gutter: clamp(1.25rem, 2vw + 0.5rem, 3rem);

  /* ---- Shape: signature cut corner (structural panels / media frames) ---- */
  --cut: 16px;
  --cut-sm: 10px;
  --border-hair: 1px solid var(--void-200);

  /* ---- Shape: interactive-control radius — "ANGULAR ARCHITECTURE +
     ROUNDED INTERACTION" (LOCKED, brief §07). Cut-corner geometry above
     remains reserved for structural/media-frame elements (.panel,
     .viewport, game-card art frames, etc.) sitewide, unchanged. Rounded
     geometry belongs to interactive controls (buttons, the mobile Menu
     control, filter chips) — PROMOTED SITEWIDE this phase (Production
     Master Build §72-B): every page's .btn now uses this radius, not
     only Home's. */
  --radius-control: 14px;

  /* ---- Motion ---- */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-fast: 150ms;
  --dur-base: 220ms;
  --dur-scan: 700ms;

  /* ---- Cinematic Spectral system — PROMOTED SITEWIDE this phase
     (Production Master Build, superseding the three prior Home-only
     phases documented in docs/VISUAL_MOTION_DIRECTION_SPEC_v0.4.md and
     docs/COLOR_UI_DESIGN_SYSTEM_v1.0.md §2/§3). These tokens are now the
     site's ONLY accent system — Signal Lime is retired sitewide (see the
     --signal role-split immediately below). Contrast computed against
     void-000 (#0a0b0c): spectral-violet 4.60:1, spectral-blue 6.03:1,
     spectral-cyan 11.16:1, spectral-magenta 5.97:1 — all pass as
     large-scale atmosphere/fills. White/--ink-000 text ON a spectral-violet
     or spectral-blue FILL fails AA (3.97:1 / 3.03:1) — --spectral-ink
     (near-black) text on those same fills passes (4.60:1 / 6.03:1), so any
     solid spectral-gradient button/fill must use --spectral-ink text.

       --electric-blue: the persistent/decorative "primary atmosphere"
       role (logo mark, signal-line device, panel dots, card borders,
       nav rest-state underline) — carries more weight than cyan
       sitewide, not just on Home.
       --spectral-blue: unchanged, keeps its role as a gradient
       workhorse stop (--grad-corporate-primary etc.).
       --spectral-cyan: reserved sitewide for genuinely restrained,
       interaction-only highlights (keyboard focus rings, nav
       hover/active underline glow, button focus) — never a flat,
       persistent fill. This is the fix the brief is explicit about:
       "do not simply replace lime with flat cyan."
       --spectral-lavender: static/always-on UI labels (section
       eyebrows, indices, filter/legal page eyebrows) — kept off cyan
       everywhere, sitewide, for the same reason.
       --starlight: a named "real light" token, distinct in *intent*
       from --ink-000 — --ink-000 is body-text-on-void, tuned for
       reading; --starlight is a luminous highlight used in glows,
       gradient hot-spots and edge-light, tuned to read as light rather
       than as text.
     Contrast computed against both void-000 (#0a0b0c) and Home's
     graphite surface (#1b1730): electric-blue 7.63:1 / 6.61:1,
     spectral-lavender 9.94:1 / 8.61:1, starlight 18.66:1 / 16.17:1 —
     all comfortably pass at every role they are used in (borders,
     small/mono labels, glows, and --spectral-ink-on-electric-blue
     fills, itself 7.63:1). */
  --spectral-violet: #7c5cff;
  --spectral-blue: #3d8bff;
  --spectral-cyan: #33d4e8;
  --spectral-magenta: #e14fd4;
  --spectral-violet-dim: #5a3fd6;
  --spectral-ink: #08080c;
  --electric-blue: #4fa3ff;
  --spectral-lavender: #cba6ff;
  --starlight: #f4f7ff;

  /* ---- Signal Lime retirement (sitewide) ----
     --signal was the site's one flat accent (#c8ff3d). It now resolves
     to the persistent/decorative role of the spectral role-split
     (electric-blue) rather than to a hardcoded hex, so every existing
     consumer of var(--signal) sitewide (logo mark, signal-line,
     skip-link, focus rings' legacy fallback, panel dots, prose links,
     card hover borders, filter-chip active state, etc.) inherits the
     new color automatically without a per-selector rewrite. Sitewide
     :focus-visible is restated explicitly on --spectral-cyan below
     (components.css / base.css), matching the interaction-only role
     cyan carries — the one --signal consumer deliberately NOT left on
     the electric-blue default. */
  --signal: var(--electric-blue);
  --signal-dim: color-mix(in srgb, var(--electric-blue) 70%, black 30%);
  --signal-ink: var(--spectral-ink);

  --grad-corporate-primary: linear-gradient(135deg, var(--spectral-violet), var(--spectral-blue));
  --grad-corporate-hover: linear-gradient(135deg, var(--spectral-violet), var(--spectral-magenta));
  --grad-corporate-ambient: radial-gradient(circle, var(--spectral-violet) 0%, transparent 70%);
  --grad-corporate-edge: linear-gradient(90deg, var(--spectral-violet), var(--spectral-magenta));
  --grad-text-accent: linear-gradient(135deg, var(--spectral-violet), var(--spectral-cyan));
  /* Luminous variants — corrective phase: brighter, more dimensional
     versions used for hover/glow states, kept distinct from the REST-state
     gradients above (which stay unchanged for every element already using
     them) so no existing rest-state visual moves as a side effect. */
  --grad-luminous-primary: linear-gradient(135deg, var(--spectral-violet) 0%, var(--electric-blue) 55%, var(--spectral-cyan) 100%);
  --glow-violet: color-mix(in srgb, var(--spectral-violet) 55%, transparent);
  --glow-electric: color-mix(in srgb, var(--electric-blue) 55%, transparent);
  --glow-cyan: color-mix(in srgb, var(--spectral-cyan) 55%, transparent);

  --dur-instant: 100ms;
  --dur-shift: 260ms;
  --dur-reveal: 450ms;
  --dur-mask: 600ms;
  --dur-ambient: 24000ms;
  --ease-glow: cubic-bezier(0.4, 0, 0.2, 1);

  /* ---- Z-index ---- */
  --z-nav: 100;
  --z-overlay: 200;

  /* ---- Breakpoints (reference only — used in @media literals) ----
     narrow: <480, mobile: <768, tablet: <1024, laptop: <1280, wide: 1280+ */
}

/* ---- Devanagari (Hindi) typography strategy — i18n pass, brief §7 ----
   Syne/Manrope/JetBrains Mono are Latin-script display/body/mono faces;
   none includes Devanagari glyphs, so a browser already substitutes a
   system Devanagari face per-glyph even without this rule. This block
   makes that substitution EXPLICIT and DELIBERATE rather than
   accidental, and follows brief §7's instruction not to force Syne (the
   Latin display face) onto glyphs it cannot render: under :lang(hi),
   the DISPLAY role is demoted to the same stack as BODY instead of
   requesting "Syne" at all, so Hindi headlines render in a real
   Devanagari text face rather than Syne's own Latin-fallback chain
   applied glyph-by-glyph. The MONO role (used for eyebrows/labels/
   indices, several of which carry translated Devanagari text — e.g.
   breadcrumbs, the "VOIDLINE // MANIFEST" nav label's surrounding UI)
   is similarly demoted; Devanagari has no widely-available true
   monospace face, so trading monospace character for readability here
   is a deliberate, disclosed tradeoff, not an oversight.
   Font delivery for these three system faces follows the exact same
   disclosed BLOCKED self-hosting status documented above for Syne/
   Manrope/JetBrains Mono (sandbox network policy rejected every
   self-hosting path attempted) — but these are OS/browser-supplied
   faces already present on virtually every modern device (Windows:
   Nirmala UI; macOS/iOS: Kohinoor Devanagari; Android/ChromeOS/Linux +
   Chrome's own bundled Noto: Noto Sans Devanagari), so no NEW asset
   dependency is introduced by this rule — it only makes an
   already-happening fallback explicit and correctly prioritized.
   Scoped by the :lang() pseudo-class (matches the closest ancestor's
   lang attribute, i.e. <html lang="hi">) rather than a class, so it
   activates automatically from chrome.mjs's pageShell() <html lang>
   output with no additional markup and cannot drift out of sync with
   the page's actual locale. */
:lang(hi) {
  --font-display: "Nirmala UI", "Kohinoor Devanagari", "Noto Sans Devanagari", ui-sans-serif, system-ui, sans-serif;
  --font-body: "Nirmala UI", "Kohinoor Devanagari", "Noto Sans Devanagari", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "Nirmala UI", "Kohinoor Devanagari", "Noto Sans Devanagari", ui-monospace, ui-sans-serif, system-ui, sans-serif;
}

@media (prefers-color-scheme: light) {
  /* VOIDLINE is deliberately a dark-void brand identity at all times —
     this is a Design DNA decision (§3.3), not an oversight. A future
     light "print/press-kit" mode could live here if ever required. */
}


/* VOIDLINE INTERACTIVE — Base / Reset / Typography */

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

html {
  color-scheme: dark;
  -webkit-text-size-adjust: 100%;
}

body {
  background: var(--void-000);
  color: var(--ink-000);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--lh-normal);
  -webkit-font-smoothing: antialiased;
  min-height: 100svh;
}

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

/* Bespoke icon sprite default size (§ Icon Language). Unsized <svg><use>
   elements fall back to the SVG spec's 300×150 default box, which is the
   one bug this rule exists to prevent everywhere icons.svg is used. */
.icon {
  width: 1em;
  height: 1em;
  display: inline-block;
  flex: none;
  vertical-align: -0.15em;
}

a { color: inherit; text-decoration: none; }

button, input, textarea, select {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
}

ul, ol { list-style: none; padding: 0; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-wide);
  /* PREVIOUS DECISION (v0.1.1, QA-03 — see docs/DEFECT_REGISTER.md):
     overflow-wrap:anywhere (not break-word) was applied here as a
     sitewide safety net, because it's what actually reduces a flex/grid
     item's automatic minimum size — break-word constrains wrapping but
     does not shrink the min-content contribution used for flex/grid
     item sizing, which let a long heading overflow its container
     whenever that container was a flex or grid item.

     REASON FOR CHANGE (v0.11.2 TYPOGRAPHY QA): this "safety net" permits
     an arbitrary MID-WORD character split whenever a heading's complete
     word doesn't fit its container at the current font-size — which
     directly violates VOIDLINE's typography rule that words must never
     be split internally. docs/HOME_TYPOGRAPHY_GEOMETRY_v0.4.md §5 had
     already flagged this exact risk for Home's own headline classes and
     fixed it there with a fluid clamp() instead, but left this sitewide
     rule in place as a backstop for "content this refinement does not
     control" — v0.11.1's real, variable-length translated Game Detail
     copy and every locale's page h1s were exactly that uncontrolled
     content, and the backstop was confirmed actually firing (a real,
     visible mid-word split: "Königliche" -> "Königli"/"che" on
     /de/games/royal-ace/ at 320-390px).

     NEW DECISION: overflow-wrap:anywhere is removed from this sitewide
     rule. The root cause — --text-2xl (h2's font-size, and the font-size
     nearly every page's h1 sets inline) being a FIXED 56px with no
     narrow-viewport reduction — is fixed at the token level instead
     (see tokens.css), the same fluid-clamp technique already proven for
     Home's headlines. Headings now only wrap BETWEEN words (the
     browser's normal text-wrapping behavior); min-width:0 below is
     unrelated to text-wrapping and is kept unchanged (it is the fix for
     a flex/grid CONTAINER's own automatic sizing, not for how text
     wraps inside it).

     AFFECTED AREAS: every h1/h2 sitewide (h3/h4 were audited and found
     not to need the same change — see docs/CHANGELOG.md's v0.11.2
     entry for the full verification record). No visual change at
     desktop/tablet widths (the new clamp reaches the same 56px by
     ~1024px and holds through wide desktop). */
  min-width: 0;
}

h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }
h4 { font-size: var(--text-md); }

p { text-wrap: pretty; }

.u-mono {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
}

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

/* ---- Skip link ---- */
.skip-link {
  position: absolute;
  top: -100%;
  left: var(--space-4);
  z-index: 300;
  background: var(--signal);
  color: var(--signal-ink);
  padding: var(--space-3) var(--space-5);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  transition: top var(--dur-fast) var(--ease-out);
}
.skip-link:focus {
  top: var(--space-4);
}

/* ---- Focus ring: signal-line motif, not a browser default ----
   PROMOTED SITEWIDE (Production Master Build §04): focus rings are the
   canonical interaction-only highlight, so they resolve to
   --spectral-cyan explicitly everywhere — not to --signal (which now
   means "persistent/decorative electric-blue") — matching the role-split
   already proven on Home. */
:focus-visible {
  outline: 2px solid var(--spectral-cyan);
  outline-offset: 3px;
}
a:focus-visible, button:focus-visible {
  outline: 2px solid var(--spectral-cyan);
  outline-offset: 2px;
}

/* ---- Layout primitives ---- */
.shell {
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  /* v0.1.1 fix (QA-03): .shell is sometimes a flex/grid item (e.g. a
     section with an inline display:flex wrapper, as on /careers/) — an
     explicit min-width:0 stops it inheriting the default automatic
     minimum size (~its content's min-content) as a flex/grid item, which
     was letting a large heading inside it push .shell wider than its
     100%-of-parent constraint at narrow viewports. */
  min-width: 0;
}

.grid-12 {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-5);
}

main { display: block; }

.section {
  position: relative;
  padding-block: var(--space-9);
}
.section + .section { border-top: var(--border-hair); }

.section__head {
  grid-column: 1 / -1;
  max-width: 44rem;
  margin-bottom: var(--space-7);
}
@media (min-width: 768px) {
  .section__head--split { grid-column: 1 / span 7; }
}

/* ---- Reduced motion: global switch ---- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* Utility used by JS-enhanced reveal; base state is fully visible so
   content never depends on JavaScript running (§ Browser/device). */
[data-reveal] {
  opacity: 1;
  transform: none;
}
.js-motion [data-reveal] {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity var(--dur-scan) var(--ease-out),
              transform var(--dur-scan) var(--ease-out);
}
.js-motion [data-reveal].is-visible {
  opacity: 1;
  transform: none;
}


/* VOIDLINE INTERACTIVE — Components */

/* =========================================================
   Signal line — the literal brand device (§ Brand Concept)
   ========================================================= */
.signal-line {
  height: 1px;
  background: linear-gradient(90deg,
    transparent 0%, var(--signal) 18%, var(--signal) 82%, transparent 100%);
  opacity: 0.55;
}
.signal-line--solid {
  background: var(--signal);
  opacity: 1;
}

/* =========================================================
   Header / primary navigation
   ========================================================= */
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  background: color-mix(in srgb, var(--void-000) 92%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: var(--border-hair);
}

.site-header__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* VQA-001 fix: `justify-content: space-between` alone collapses to a
     literal 0px gap between the logo and the nav group the instant the
     row is at its tightest fit — this floor guarantees breathing room
     between them even at the desktop-nav breakpoint's exact minimum
     width, rather than only "however much space happens to be left." */
  gap: var(--space-4);
  height: 4.5rem;
}

.logo {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-md);
  letter-spacing: var(--tracking-wide);
}
/* FINAL BRAND IDENTITY INTEGRATION PASS: the generic "◆" text glyph is
   replaced by the user-approved VOIDLINE V mark (an extracted derivative
   of the approved master logo — see docs/BRAND_IDENTITY_INTEGRATION.md).
   `align-items` moved from `baseline` to `center` on `.logo` above: a
   raster mark has no text baseline to align against, and `center` is what
   keeps it vertically balanced next to the wordmark at every breakpoint
   (baseline alignment on an <img> falls back to its bottom edge, which
   visibly sank the mark below the wordmark's optical center). */
.logo__mark {
  display: inline-flex;
  flex-shrink: 0;
  /* Restrained, existing-token glow — not a new effect: same
     drop-shadow mechanism the retired glyph used, same electric-blue
     token, same fast/out timing already used sitewide. */
  filter: drop-shadow(0 0 6px var(--glow-electric));
  transition: filter var(--dur-fast) var(--ease-out);
}
.logo__mark img {
  display: block;
  height: 1.75rem;
  width: auto;
}
.logo:hover .logo__mark,
.logo:focus-visible .logo__mark {
  /* Premium, not animated: a single restrained intensity step on
     hover/focus, no scale/rotate/pulse — see brand-integration brief §7. */
  filter: drop-shadow(0 0 10px var(--glow-electric));
}

.primary-nav {
  display: none;
}
/* VQA-001 fix: the desktop-nav breakpoint was 900px, but the full row
   (wordmark + 5 nav links + CTA + language selector) does not fit at
   900px in ANY locale, including English — measured (forced-nowrap)
   minimum single-line fit using this same compact styling is ~916px
   for EN and ~1016px for ES (the longest locale), before adding any
   breathing room. 1060px was chosen as the new activation width: it
   clears every locale's measured minimum with real margin rather than
   the bare minimum, so the row does not sit at its literal breaking
   point. Below this width the mobile hamburger menu remains active
   (unchanged) instead of ever wrapping. See docs/VISUAL_QA_v0.11.2.md
   VQA-001 for the full measurement method and retest evidence. */
@media (min-width: 1060px) {
  .primary-nav {
    display: flex;
    align-items: center;
    gap: var(--space-6);
  }
  .primary-nav a {
    position: relative;
    font-size: var(--text-sm);
    color: var(--ink-100);
    padding-block: var(--space-2);
    transition: color var(--dur-fast) var(--ease-out);
  }
  .primary-nav a::after {
    content: "";
    position: absolute;
    left: 0; right: 100%;
    bottom: 0;
    height: 1px;
    /* REST state stays the persistent/decorative electric-blue (--signal);
       only HOVER/ACTIVE below become the restrained cyan interaction
       highlight — promoted sitewide from the Home-only corrective phase. */
    background: var(--signal);
    transition: right var(--dur-base) var(--ease-out), background var(--dur-shift) var(--ease-glow), box-shadow var(--dur-shift) var(--ease-glow);
  }
  .primary-nav a:hover,
  .primary-nav a[aria-current="page"] {
    color: var(--ink-000);
    text-shadow: 0 0 14px color-mix(in srgb, var(--spectral-cyan) 40%, transparent);
  }
  .primary-nav a:hover::after,
  .primary-nav a[aria-current="page"]::after {
    right: 0;
    background: linear-gradient(90deg, var(--electric-blue), var(--spectral-cyan));
    box-shadow: 0 0 10px 1px var(--glow-cyan);
  }
}

.header-actions { display: flex; align-items: center; gap: var(--space-4); }

/* Header CTA (Production Master Build §16 — Reference A header CTA
   state): reuses the sitewide .btn primary style at a smaller scale so
   it doesn't dominate the header row. Hidden below the same breakpoint
   as .primary-nav — the mobile overlay carries its own emphasized
   version instead (nav-overlay__item--cta below), so the action is
   never lost, just relocated. */
.nav-cta {
  display: none;
  padding: 0.55em 1.1em;
  font-size: var(--text-xs);
}
@media (min-width: 1060px) {
  .nav-cta { display: inline-flex; }
}

/* ---- Language switcher (i18n pass, brief §3) ----
   Deliberately restrained — a native <details>/<summary> disclosure, the
   same accessible/no-JS-safe pattern as .faq-item below, styled as a
   compact pill matching .filter-chip's restrained secondary-control
   language: void-100 fill, hairline border, --electric-blue on
   hover/open, --spectral-cyan focus ring. NEVER --signal/Signal Lime
   (brief §3 CRITICAL — this file's --signal token is itself already
   permanently aliased to --electric-blue, see tokens.css, but this rule
   deliberately references --electric-blue directly rather than
   --signal, so the switcher can never regress to lime even if --signal
   is ever repointed). Intentionally NOT styled like a generic
   browser-translate globe widget: no globe icon, no flag icons, no
   native <select> chrome — a text language code + the same chevron
   glyph used nowhere else but here, inside VOIDLINE's own pill/border
   language. Sized and weighted below .nav-cta (font-size, padding,
   border only vs. .nav-cta's filled gradient button) so it stays
   visually secondary to the primary CTA, per brief §3. */
.lang-switcher {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.lang-switcher__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.4em 0.75em;
  border: 1px solid var(--void-200);
  border-radius: var(--radius-control);
  background: var(--void-100);
  color: var(--ink-100);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  cursor: pointer;
  list-style: none;
  transition: border-color var(--dur-shift) var(--ease-glow), color var(--dur-fast) var(--ease-out);
}
.lang-switcher__trigger::-webkit-details-marker { display: none; }
.lang-switcher__trigger:hover { border-color: var(--electric-blue); color: var(--ink-000); }
.lang-switcher__trigger:focus-visible { outline: 2px solid var(--spectral-cyan); outline-offset: 2px; }
.lang-switcher[open] .lang-switcher__trigger { border-color: var(--electric-blue); color: var(--ink-000); }
.lang-switcher__chevron { width: 10px; height: 10px; flex: none; transition: transform var(--dur-shift) var(--ease-glow); }
.lang-switcher[open] .lang-switcher__chevron { transform: rotate(180deg); }
.lang-switcher__list {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  z-index: var(--z-overlay);
  min-width: 9rem;
  background: var(--void-100);
  border: 1px solid var(--void-200);
  border-radius: var(--radius-panel, var(--radius-control));
  padding: var(--space-2);
  box-shadow: 0 12px 32px -12px rgba(0,0,0,0.6);
}
.lang-switcher__option {
  display: block;
  padding: 0.5em 0.75em;
  border-radius: var(--radius-control);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--ink-100);
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.lang-switcher__option:hover,
.lang-switcher__option:focus-visible { background: var(--void-150); color: var(--ink-000); }
.lang-switcher__option.is-current { color: var(--electric-blue); font-weight: 600; }

/* Header row can get tight once a 6th (CTA), 7th (language switcher)
   element competes with 5 nav links in the band just above the
   desktop-nav activation width. A modest gap/size reduction in that
   band keeps every element on one line without wrapping or overflow.
   VQA-001 retest (docs/VISUAL_QA_v0.11.2.md): the previous 900-1180px
   band left a real, previously-undiscovered gap — Spanish (the
   longest locale) still wrapped from ~1181px to ~1204px, because the
   full-size (non-compact) styling below activated at 1180px, before
   Spanish's full-size content actually fits (empirically ~1205px+).
   The band's bounds are now tied to the same measurements behind the
   1060px activation width above: 1060px is proven to fit every locale
   using THIS compact styling, and the upper bound is raised past the
   confirmed real full-size fit point for Spanish (empirically ~1240px
   — verified by a fine-grained sweep, not estimated from formula, since
   the shell gutter's clamp() means available width is not a simple
   linear function of viewport width) so there is no width left where
   neither styling fits. */
@media (min-width: 1060px) and (max-width: 1245px) {
  .primary-nav { gap: var(--space-4); }
  .primary-nav a { font-size: var(--text-xs); }
  .lang-switcher__trigger { padding: 0.35em 0.6em; }
}

.nav-overlay__item--cta {
  border-bottom: none;
  padding-top: var(--space-5);
}
.nav-overlay__item--cta .btn {
  width: 100%;
  justify-content: center;
  font-family: var(--font-body);
  font-size: var(--text-sm);
}

/* ---- Mobile manifest toggle: pure CSS checkbox hack, no JS required ---- */
.nav-toggle-input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.nav-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  padding: var(--space-2) var(--space-3);
  border: var(--border-hair);
  border-radius: var(--radius-control);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
}
@media (min-width: 1060px) {
  .nav-toggle { display: none; }
  /* VQA-006 fix: the underlying checkbox has no visual role of its own
     at desktop width once its label (the hamburger button, above) and
     the overlay it drives are both hidden — but being display:block by
     default it still sat in the keyboard tab order as an invisible,
     unreachable-by-purpose stop right after the skip-link, with no way
     to show a focus indicator (its own outline is suppressed by its
     opacity:0). Hiding it at the same breakpoint removes that dead tab
     stop entirely, so desktop keyboard order goes skip-link -> logo. */
  .nav-toggle-input { display: none; }
}
.nav-toggle__icon { width: 14px; height: 14px; flex: none; }

/* VQA-006 fix: below the desktop breakpoint the checkbox is still the
   real, necessary control (no JS, pure CSS checkbox hack) so it must
   stay focusable — but it is `opacity:0`, which makes the sitewide
   `:focus-visible` outline (base.css) resolve on a fully transparent
   box and disappear. Mirror that same focus-visible treatment onto its
   visible proxy, the hamburger label, via the sibling combinator this
   file already uses for `.nav-toggle-input:checked ~ .nav-overlay`. */
.nav-toggle-input:focus-visible ~ .site-header .nav-toggle {
  outline: 2px solid var(--spectral-cyan);
  outline-offset: 2px;
}

.nav-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  background: var(--void-000);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--gutter);
  /* i18n pass: the mobile language list (.nav-overlay__lang below) adds
     content below the nav list; overflow-y:auto (rather than nothing,
     the prior behavior) keeps a short viewport / a locale with longer
     translated nav labels from clipping any item instead of silently
     losing it — the overlay itself was already position:fixed/inset:0,
     so scrolling stays contained to the overlay, never the page behind
     it. */
  overflow-y: auto;
  opacity: 0;
  pointer-events: none;
  transform: translateY(-8px);
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}
@media (min-width: 1060px) { .nav-overlay { display: none; } }
.nav-toggle-input:checked ~ .nav-overlay {
  opacity: 1;
  pointer-events: auto;
  transform: none;
}
.nav-overlay__list { display: flex; flex-direction: column; gap: var(--space-3); }
.nav-overlay__item {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  padding-block: var(--space-3);
  border-bottom: var(--border-hair);
}
.nav-overlay__index {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--signal);
}
.nav-overlay__item a {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-lg);
}
.nav-overlay__close {
  position: absolute;
  top: var(--gutter);
  right: var(--gutter);
  cursor: pointer;
  padding: var(--space-2);
}

/* ---- Mobile language list (i18n pass, brief §3) ----
   Appended below the primary nav list inside the same full-screen
   overlay, rather than duplicating the desktop <details> disclosure at
   mobile widths — a second nested disclosure inside an already-modal
   overlay would be an awkward double-tap interaction. A flat, always-
   visible link list is simpler and matches the overlay's own list-of-
   destinations character. Kept visually distinct from (below, smaller
   than) the primary nav items so it stays secondary. */
.nav-overlay__lang {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: var(--border-hair);
}
.nav-overlay__lang-label {
  color: var(--ink-200);
  font-size: var(--text-2xs);
  margin-bottom: var(--space-3);
}
.nav-overlay__lang-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.nav-overlay__lang-list .lang-switcher__option {
  border: 1px solid var(--void-200);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}
.nav-overlay__lang-list .lang-switcher__option:hover,
.nav-overlay__lang-list .lang-switcher__option:focus-visible { border-color: var(--electric-blue); }
.nav-overlay__lang-list .lang-switcher__option.is-current { border-color: var(--electric-blue); }

/* =========================================================
   Buttons — "ANGULAR ARCHITECTURE + ROUNDED INTERACTION" (brief §07/§08)
   PROMOTED SITEWIDE this phase: this was previously the cut-corner,
   flat Signal-Lime `.btn` with a Home-only `.btn--spectral` override.
   The corrective role-split proved this system on Home first; the
   Production Master Build now makes it the ONE button system for the
   whole site — no more per-page fork. Cut-corner geometry stays
   reserved for structural/media-frame elements (.panel, .viewport)
   sitewide, unchanged; rounded geometry belongs here.

   PRIMARY: violet → electric-blue → cyan gradient fill, spectral-ink
   text (contrast verified in tokens.css), localized luminous glow on
   hover (no bounce/pulse/flashing/giant blur — a controlled edge-light
   + ambient glow at --dur-shift, inside the 180–300ms range).
   States: default / hover / active / focus-visible / disabled / loading. */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.85em 1.4em;
  border-radius: var(--radius-control);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  background: var(--grad-corporate-primary);
  color: var(--spectral-ink);
  border: none;
  transition: background var(--dur-shift) var(--ease-glow), transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-shift) var(--ease-glow);
  cursor: pointer;
}
.btn:hover {
  background: var(--grad-luminous-primary);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--starlight) 45%, transparent),
    0 0 36px -6px var(--glow-violet),
    0 0 18px -4px var(--glow-cyan);
  transform: translateY(-1px);
}
.btn:active {
  transform: translateY(0);
  background: linear-gradient(135deg, var(--spectral-violet-dim), var(--spectral-blue));
}
.btn:focus-visible {
  outline: 2px solid var(--spectral-cyan);
  outline-offset: 2px;
}
.btn .icon { transition: transform var(--dur-fast) var(--ease-out), filter var(--dur-shift) var(--ease-glow); }
.btn:hover .icon {
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--spectral-ink) 35%, transparent));
  transform: translateX(2px);
}

/* SECONDARY: dark interior + spectral border (brief §08). Used for
   less-emphasis actions that still need visible affordance — distinct
   from the quiet ghost variant below. */
.btn--secondary {
  background: var(--void-100);
  color: var(--ink-000);
  border: 1px solid var(--electric-blue);
  box-shadow: none;
}
.btn--secondary:hover {
  background: var(--void-150);
  border-color: var(--spectral-cyan);
  box-shadow: 0 0 22px -8px var(--glow-cyan);
  transform: translateY(-1px);
}
.btn--secondary:active { transform: translateY(0); background: var(--void-100); }

/* GHOST: quiet default, spectral hover. */
.btn--ghost {
  background: transparent;
  color: var(--ink-000);
  border: 1px solid var(--void-200);
  box-shadow: none;
}
.btn--ghost:hover,
.btn--ghost:focus-visible {
  background: var(--void-100);
  border-color: var(--electric-blue);
  box-shadow: 0 0 22px -8px var(--glow-electric);
  transform: translateY(-1px);
}

.btn--disabled,
.btn[disabled] {
  background: var(--void-100);
  color: var(--ink-200);
  border: 1px dashed var(--void-200);
  box-shadow: none;
  cursor: not-allowed;
  pointer-events: none;
  transform: none;
}

/* LOADING: reuses the disabled surface (no motion beyond the sitewide
   reduced-motion-safe opacity pulse) with an accessible busy state —
   markup sets aria-busy="true" and swaps the label; no JS spinner
   dependency required for the state to be legible. */
.btn--loading {
  background: var(--void-100);
  color: var(--ink-200);
  border: 1px solid var(--void-200);
  box-shadow: none;
  cursor: wait;
  pointer-events: none;
}

.btn--block { width: 100%; justify-content: center; }

/* =========================================================
   Panel — cut-corner structural card, reused across content types
   ========================================================= */
.panel {
  position: relative;
  background: var(--void-100);
  border: var(--border-hair);
  clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  height: 100%;
}
.panel::before {
  content: "";
  position: absolute;
  top: 0; right: 0;
  width: var(--cut);
  height: 1px;
  background: var(--void-200);
  transform-origin: 100% 0;
  transform: rotate(45deg) translate(1px, 0);
}
.panel__eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--ink-100);
}
.panel__dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--signal);
  flex: none;
}
.panel__dot--idle { background: var(--ink-200); }
.panel h3 { font-size: var(--text-md); }
/* PREVIOUS DECISION (v0.11.1, i18n correction/completion pass): now that
   Game Detail's translated tagline text (src/locales/<code>/gameDetail.mjs)
   actually renders here instead of always-English fallback text, a long
   unbroken German compound word (e.g. "Abenddämmerung") pushed a narrow
   flex/grid card wider than its container at 320px — verified via
   build/lib/functional-check.mjs's i18n overflow sweep, which caught this
   on /de/ specifically. overflow-wrap:anywhere was applied as the fix.

   REASON FOR CHANGE (v0.11.2 TYPOGRAPHY QA): overflow-wrap:anywhere permits
   an arbitrary MID-WORD character split, which VOIDLINE's typography rule
   forbids (words must never be split internally) — confirmed this was
   actually firing ("Abenddämmerung" splitting across 2-3 lines on the Home
   mosaic at 390/375/320px). NEW DECISION: overflow-wrap:anywhere is removed.
   The real root cause was available width, not text-wrapping — the Home
   mosaic's 2-column layout at <=767px leaves only ~68px of text width per
   card at 320px (132px column - 64px .panel padding), too narrow for any
   real compound word. Fixed at the layout level instead: .home-mosaic
   drops to a single column below 420px (see home.css), giving each card
   ~216px of text width, which fits the longest known tagline word without
   splitting. min-width:0 is unrelated to text-wrapping (it is the fix for
   this flex child's own automatic sizing) and is kept unchanged. */
.panel__body { color: var(--ink-100); font-size: var(--text-sm); flex: 1; min-width: 0; }
.panel__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: var(--space-3);
  border-top: var(--border-hair);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--ink-100);
}

/* PRE-DEPLOYMENT QA FIX (§4, VQA-005 root-caused): .panel-link always
   wraps exactly a .viewport (aspect-ratio-driven media block) followed by
   a .panel (the base rule below gives .panel `height:100%`, intended for
   .panel's OTHER usage as a single, standalone content box — see
   build/pages/legal.mjs, unaffected by this override since it is never
   inside .panel-link). Inside .panel-link specifically, .viewport already
   consumes part of the card's height, so .panel's inherited
   `height:100%` (100% of the *whole* card, on top of the space .viewport
   already occupies) made .panel's own box overflow past .panel-link's
   bottom edge by exactly .viewport's height. Confirmed via
   getBoundingClientRect() (real computed geometry, not a headless/
   compositor artifact): on Game Detail's "MORE FROM VOIDLINE" rail this
   pushed .panel's solid-background box ~250px past the related-games
   grid's own bottom edge, visually overlapping the footer immediately
   below (VQA-005's "footer bleed" symptom) — same shared markup/CSS also
   backs the Games catalogue's gameCard(), so the identical row-overflow
   risk existed there too, not only on Game Detail.
   FIX: .panel-link becomes the flex column (instead of .panel claiming
   100% of a height it doesn't fully control), so .viewport keeps its
   natural aspect-ratio height and .panel grows to fill exactly the
   remaining space — no overflow, no visual change to intended card
   proportions. min-height:0 is required so the flex item can shrink
   below its content's intrinsic height when needed (standard flex fix). */
.panel-link { display: flex; flex-direction: column; height: 100%; }
.panel-link .panel { height: auto; flex: 1; min-height: 0; transition: border-color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); }
/* Cinematic Visual Completion Pass §11 — Related Worlds: when a
   panel-link carries its own --game-accent (Game Detail's "MORE FROM
   VOIDLINE" rail), hover responds with that world's own color instead of
   one flat sitewide accent, echoing the Games catalogue's per-card
   world-specific hover treatment. Falls back to --signal wherever
   --game-accent isn't set (e.g. Home's mosaic/strip, unaffected). */
.panel-link:hover .panel {
  border-color: var(--game-accent, var(--signal));
  transform: translateY(-2px);
  box-shadow: 0 24px 50px -30px color-mix(in srgb, var(--game-accent, var(--spectral-violet)) 55%, transparent);
}

.panel--stub { opacity: 0.72; }
.panel--stub .panel__body { color: var(--ink-200); }

/* =========================================================
   Viewport — the game-world panel. Inner accent is overridden
   per game via --game-accent / --game-accent-2 / --game-bg.
   ========================================================= */
.viewport {
  position: relative;
  background: var(--game-bg, var(--void-100));
  border: 1px solid color-mix(in srgb, var(--game-accent, var(--signal)) 35%, var(--void-200));
  clip-path: polygon(0 0, calc(100% - 28px) 0, 100% 28px, 100% 100%, 0 100%);
  overflow: hidden;
  aspect-ratio: 16 / 10;
  display: flex;
  align-items: flex-end;
}
.viewport--wide { aspect-ratio: 21 / 9; }
.viewport--square { aspect-ratio: 1 / 1; }
/* v0.2 Phase 2 (Game Key Art & Media System): at the fixed 21:9 desktop
   hero ratio, a narrow mobile viewport produces a very short letterbox
   strip — the same relative crop as desktop, just rendered small, which
   leaves each game's signature subject (BLIP / Kestrel-9 / Kalyx) tiny
   and hard to read. Below 768px the hero panel switches to a taller 4:3
   frame instead: with `preserveAspectRatio="xMidYMid slice"` on the
   inner SVG (src/data/game-worlds.mjs), a taller container crops less
   of the composition's vertical extent and effectively zooms in on the
   subject, keeping it legible instead of shrinking with the viewport.
   Each hero composition's subject was repositioned/verified to survive
   this crop — see docs/GAME_KEY_ART_v0.2_PHASE2.md §16 for the
   per-breakpoint verification. Card panels (.viewport without --wide)
   are unaffected — their 16:10 ratio already gives enough height. */
@media (max-width: 767px) {
  .viewport--wide { aspect-ratio: 4 / 3; }
}
/* v0.1.1 fix (QA-02): scoped to the placeholder-art SVG specifically —
   was `.viewport svg`, which also matched (and, via higher specificity,
   overrode the sizing of) the bespoke sprite icons rendered inside
   .viewport__caption below. See src/data/signal-art.mjs for the class. */
.viewport svg.viewport-art { position: absolute; inset: 0; width: 100%; height: 100%; }
/* v0.2 Phase 2B (Web Media & Asset Pipeline v1.0): the same fill/scope
   pattern as the SVG rule above, for the new raster <picture> art
   (apex-signal, chicken-adventure, golden-roar). Each derivative was
   pre-cropped server-side to its exact delivery ratio (see
   src/data/game-media.mjs), so object-fit:cover here is a safety net,
   not the primary cropping mechanism — it should never need to crop
   anything beyond sub-pixel rounding. */
.viewport picture.viewport-art { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }
.viewport picture.viewport-art img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center; }
/* Defensive second layer: even if an unscoped SVG/img is ever nested
   inside a .viewport panel again, any element carrying .icon stays
   icon-sized. */
.viewport .icon { position: static; inset: auto; width: 1em; height: 1em; }
.viewport__caption {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--ink-000);
  /* v0.2 Phase 1 self-check fix: the original two-stop scrim went fully
     transparent right at the caption row's own top edge, so busier art
     compositions (e.g. Circuit Stray's courier trail, which reads all
     the way to the bottom of the frame) could cross that seam with no
     contrast protection. A third stop keeps meaningful opacity closer
     to the top of the row before fading into the artwork above it. */
  background: linear-gradient(
    0deg,
    color-mix(in srgb, var(--void-000) 78%, transparent) 0%,
    color-mix(in srgb, var(--void-000) 45%, transparent) 65%,
    transparent 100%
  );
}
.viewport__badge {
  padding: 0.3em 0.6em;
  background: color-mix(in srgb, var(--void-000) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--game-accent, var(--signal)) 50%, transparent);
}

/* =========================================================
   Tags / genre badges
   ========================================================= */
.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 0.35em 0.7em;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  border: 1px solid var(--void-200);
  color: var(--ink-100);
}
.tag--accent {
  border-color: var(--game-accent, var(--signal));
  color: var(--game-accent, var(--signal));
}

/* =========================================================
   Hero — off-center, poster composition (not centered+2 buttons)
   ========================================================= */
.hero {
  position: relative;
  padding-top: var(--space-8);
  padding-bottom: var(--space-9);
  overflow: hidden;
}
.hero__grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-5);
  align-items: end;
  min-height: 60vh;
}
.hero__eyebrow {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--ink-100);
  margin-bottom: var(--space-6);
}
.hero__title {
  grid-column: 1 / -1;
  max-width: 20ch;
}
@media (min-width: 768px) {
  .hero__title { grid-column: 1 / span 9; }
}
.hero__lede {
  grid-column: 1 / -1;
  margin-top: var(--space-5);
  max-width: 34rem;
  color: var(--ink-100);
  font-size: var(--text-md);
}
@media (min-width: 768px) {
  .hero__lede { grid-column: 1 / span 6; }
}
.hero__actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-top: var(--space-6);
}

/* =========================================================
   Games grid / filter
   ========================================================= */
.filter-bar {
  display: none;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-6);
}
.js-enhanced .filter-bar { display: flex; }
.filter-chip {
  padding: 0.5em 1.1em;
  border-radius: 999px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  border: 1px solid var(--void-200);
  color: var(--ink-100);
  cursor: pointer;
  background: var(--void-100);
  transition: background var(--dur-shift) var(--ease-glow), border-color var(--dur-shift) var(--ease-glow), color var(--dur-fast) var(--ease-out), box-shadow var(--dur-shift) var(--ease-glow);
}
.filter-chip:hover { border-color: var(--electric-blue); }
.filter-chip:focus-visible { outline: 2px solid var(--spectral-cyan); outline-offset: 2px; }
.filter-chip[aria-pressed="true"] {
  border-color: transparent;
  color: var(--spectral-ink);
  background: var(--grad-corporate-primary);
  box-shadow: 0 0 18px -6px var(--glow-violet);
}

.games-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
}
@media (min-width: 640px) {
  .games-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .games-grid { grid-template-columns: repeat(3, 1fr); }
}

.approach-grid { grid-template-columns: 1fr; }
@media (min-width: 700px) {
  .approach-grid { grid-template-columns: repeat(3, 1fr); }
}

.world-grid { grid-template-columns: 1fr; }
@media (min-width: 700px) {
  .world-grid { grid-template-columns: repeat(2, 1fr); }
}

/* Game Detail's per-game "hook" heading. border-left/padding-left were
   previously set as inline styles in build/pages/gameDetail.mjs; moved
   here so a narrow-viewport override can actually apply (an inline
   style would out-specificity a plain class rule). --world-accent is
   set as a custom property on the ancestor <section> and inherits down
   to this element, so no inline value is needed here. */
.world-hook {
  border-left: 2px solid var(--world-accent);
  padding-left: var(--space-5);
}
/* v0.11.2 TYPOGRAPHY QA CORRECTION: v0.11.1 made this heading real,
   variable-length TRANSLATED copy (src/locales/<code>/gameDetail.mjs
   `hook` field) instead of static English placeholder text. h2's
   default font-size (--text-2xl, a sitewide fluid clamp) is tuned for
   the general case, but several German compound words in this specific
   field (e.g. book-of-anubis's "Entdeckungsgetriebene", 21 characters,
   the worst case measured) are wider than this heading's available
   column even at --text-2xl's reduced narrow-viewport value — a real,
   measured overflow (up to ~189px on /de/games/book-of-anubis/ at
   320px), not a split (base.css's h1-h4 rule no longer permits
   mid-word splitting — see that file's note). FIX: a dedicated,
   SCOPED override for this heading alone (not a further reduction of
   the shared --text-2xl token, which would needlessly shrink every
   other h1/h2 sitewide) — a lower fluid-clamp minimum tuned to the
   measured worst-case word, plus reclaiming this heading's own
   border/padding indent at the same narrow widths (~26px of extra
   text width). Values were derived from canvas-measured word widths
   (Playwright + ctx.measureText at the actual computed font) against
   the actual available column width per viewport, then verified with
   a zero-margin-of-error re-measurement across all 12 games x 6
   locales at 320/375/390px. Only applies below 480px — 768px+ already
   fits the worst-case word at --text-2xl's normal value, so tablet/
   desktop appearance is unchanged.
   OVERNIGHT MASTER PRODUCTION-READINESS PASS (§5): the narrow-viewport
   `[data-family="monumental"] .world-hook` suppression that originally
   lived in this block has been moved down to immediately after the
   Tier A Layer B rule it suppresses (this file's later
   `[data-family="monumental"] .world-hook { border-right: ...}` block).
   ROOT CAUSE (found via rendered + computed-style inspection at 390px
   on /games/royal-ace/): both rules share the exact selector
   `[data-family="monumental"] .world-hook` (identical specificity), and
   the Tier A rule was appended to this file AFTER this correction —
   so, tie broken by source order, the later unconditional rule was
   winning the cascade at every width, including below 480px, silently
   reinstating the "stray clipped line" this suppression exists to
   prevent. Moving the suppression to after the rule it must beat (same
   content, new position — see below) restores the intended behavior
   without touching either rule's selector or specificity. */
@media (max-width: 480px) {
  .world-hook {
    font-size: clamp(1rem, 6vw, 1.85rem);
    border-left-width: 1px;
    padding-left: var(--space-3);
  }
}

/* =========================================================
   Full-bleed editorial catalogue card (Production Master Build §27-29)
   Anatomy: index / genre / world-tag / title / hook / VIEW WORLD →,
   all integrated over full-bleed key art via a gradient scrim — no
   separate grey panel. Cut-corner geometry (structural/media-frame
   Design DNA) is kept for this card's outer frame, matching .panel and
   .viewport sitewide; ROUNDED geometry stays reserved for interactive
   controls (buttons, filter chips) per §07 — this card is a navigation
   surface, not a button, so it keeps the angular frame.
   ========================================================= */
.game-card-fb {
  position: relative;
  display: block;
  overflow: hidden;
  aspect-ratio: 8 / 5;
  border: 1px solid color-mix(in srgb, var(--game-accent, var(--electric-blue)) 30%, var(--void-200));
  clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
  color: inherit;
  transition: transform var(--dur-shift) var(--ease-glow), border-color var(--dur-shift) var(--ease-glow), box-shadow var(--dur-shift) var(--ease-glow);
}
.game-card-fb__art,
.game-card-fb__picture {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.game-card-fb__picture img,
.game-card-fb__procedural svg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transition: transform var(--dur-mask) var(--ease-glow);
}
/* Scrim: darkens toward the bottom (title/hook/CTA legibility) with a
   lighter top pass (index/genre legibility) — controlled darkening, not
   a flat overlay, so the art itself still reads through. */
.game-card-fb__scrim {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(0deg, color-mix(in srgb, var(--void-000) 92%, transparent) 0%, color-mix(in srgb, var(--void-000) 55%, transparent) 42%, transparent 68%),
    linear-gradient(180deg, color-mix(in srgb, var(--void-000) 55%, transparent) 0%, transparent 30%);
}
.game-card-fb__content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 100%;
  padding: var(--space-5);
}
.game-card-fb__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  color: var(--ink-000);
}
.game-card-fb__index {
  padding: 0.3em 0.6em;
  background: color-mix(in srgb, var(--void-000) 55%, transparent);
  color: var(--spectral-lavender);
}
.game-card-fb__genre {
  padding: 0.3em 0.7em;
  border: 1px solid color-mix(in srgb, var(--starlight) 35%, transparent);
  background: color-mix(in srgb, var(--void-000) 45%, transparent);
  color: var(--ink-000);
}
.game-card-fb__worldtag { align-self: flex-start; background: color-mix(in srgb, var(--void-000) 45%, transparent); }
.game-card-fb__title {
  margin-top: var(--space-3);
  font-size: var(--text-lg);
  color: var(--ink-000);
}
.game-card-fb__hook {
  margin-top: var(--space-2);
  max-width: 32ch;
  color: var(--ink-100);
  font-size: var(--text-sm);
  /* v0.11.1 applied overflow-wrap:anywhere here defensively (same
     reasoning as .panel__body above), but this card is single-column
     up to 640px width (.games-grid) — its content padding (--space-5,
     24px per side) still leaves ~230px+ of text width at 320px, which
     empirically fits every locale's card tagline without splitting.
     REMOVED (v0.11.2 TYPOGRAPHY QA): overflow-wrap:anywhere permits an
     arbitrary mid-word split, which VOIDLINE's typography rule forbids;
     it was not actually needed here — verified via a full-locale/
     viewport sweep with the property removed (zero overflow, zero
     splits). min-width:0 is unrelated to text-wrapping and is kept. */
  min-width: 0;
}
.game-card-fb__cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-000);
}
.game-card-fb__cta .icon { transition: transform var(--dur-fast) var(--ease-out); }

/* Hover/focus — restrained ~1.02 scale, per-game accent edge response
   (never a uniform purple glow: --game-accent is set per card by
   gameCardFullBleed()), arrow movement. No 3D rotation, no large zoom,
   no glass blur. */
.game-card-fb:hover,
.game-card-fb:focus-visible {
  transform: translateY(-2px) scale(1.02);
  border-color: var(--game-accent, var(--electric-blue));
  box-shadow: 0 28px 60px -30px color-mix(in srgb, var(--game-accent, var(--spectral-violet)) 70%, transparent);
}
.game-card-fb:hover .game-card-fb__picture img,
.game-card-fb:focus-visible .game-card-fb__picture img {
  transform: scale(1.05);
}
.game-card-fb:hover .game-card-fb__cta .icon,
.game-card-fb:focus-visible .game-card-fb__cta .icon {
  transform: translateX(3px);
}
.game-card-fb:focus-visible {
  outline: 2px solid var(--spectral-cyan);
  outline-offset: 3px;
}

/* Cinematic Visual Completion Pass §10 — world-specific hover atmosphere:
   a soft radial wash in the card's own accent, layered between the scrim
   and the text content (content keeps z-index:1 above it), so each card's
   hover state feels like its world rather than one uniform highlight. */
.game-card-fb::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 25% 105%, color-mix(in srgb, var(--game-accent, var(--electric-blue)) 38%, transparent) 0%, transparent 62%);
  opacity: 0;
  transition: opacity var(--dur-shift) var(--ease-glow);
  pointer-events: none;
}
.game-card-fb:hover::after,
.game-card-fb:focus-visible::after {
  opacity: 1;
}

.games-grid-fb {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
}
@media (min-width: 640px) {
  .games-grid-fb { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1200px) {
  .games-grid-fb { grid-template-columns: repeat(3, 1fr); }
  /* §10 — occasional width-span variation: two cards in the twelve break
     the uniform 3-up rhythm at wide desktop only (2-col/mobile stay
     perfectly uniform for scanability — no chaotic masonry). Selection is
     positional (first card, and the card opening the grid's second half)
     so it survives any future portfolio reordering without special-casing
     a specific game. */
  .game-card-fb--featured { grid-column: span 2; aspect-ratio: 16 / 7; }
  .game-card-fb--featured .game-card-fb__title { font-size: var(--text-xl); }
  .game-card-fb--featured .game-card-fb__hook { max-width: 44ch; font-size: var(--text-md); }
}

/* =========================================================
   Commercial bridge — "HAVE A WORLD IN MIND?" (brief §27/§34-36)
   Reused after the Games grid and on About; a raised, lit atmosphere
   beat (matches Home's section-rhythm language) rather than a flat CTA
   strip.
   ========================================================= */
.commercial-bridge {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  text-align: center;
}
.commercial-bridge::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(ellipse 60% 80% at 50% 30%, color-mix(in srgb, var(--spectral-violet) 18%, transparent), transparent 72%);
}
.commercial-bridge__inner { max-width: 34rem; margin-inline: auto; }
.commercial-bridge__headline { font-size: var(--text-xl); }
.commercial-bridge__body { margin-top: var(--space-4); color: var(--ink-100); }
.commercial-bridge .btn { margin-top: var(--space-6); }

/* =========================================================
   Forms
   ========================================================= */
.grid-2col {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 var(--space-4);
}
@media (min-width: 480px) {
  .grid-2col { grid-template-columns: 1fr 1fr; }
}
.form-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
}
/* Release Candidate pass — honeypot spam-deterrent field (honeypotField(),
   components.mjs): visually off-screen (not display:none, which some
   bots skip) rather than hidden by layout, and already out of the tab
   order / accessibility tree via tabindex="-1" + aria-hidden on its
   wrapper — this rule only removes it from sighted layout. */
.form-field--honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
  margin: 0;
}
.form-field label {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--ink-100);
}
.form-field input,
.form-field textarea,
.form-field select {
  background: var(--void-100);
  border: var(--border-hair);
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-base);
}
.form-field select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-100) 50%), linear-gradient(135deg, var(--ink-100) 50%, transparent 50%);
  background-position: calc(100% - 1.3em) center, calc(100% - 1em) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: var(--space-7);
}
.form-field input:focus,
.form-field textarea:focus,
.form-field select:focus {
  border-color: var(--electric-blue);
}
.form-field textarea { resize: vertical; min-height: 8rem; }
.form-field__hint { font-size: var(--text-xs); color: var(--ink-200); }
.form-field__error {
  font-size: var(--text-xs);
  color: var(--alert);
  display: none;
}
.form-field.has-error input,
.form-field.has-error textarea,
.form-field.has-error select { border-color: var(--alert); }
.form-field.has-error .form-field__error { display: block; }

/* Consent checkbox (Project Inquiry form, brief §38) */
.form-field--checkbox { flex-direction: row; }
.form-field__checkbox-label {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--ink-100);
  text-transform: none;
  letter-spacing: normal;
  cursor: pointer;
}
.form-field__checkbox-label input[type="checkbox"] {
  appearance: none;
  width: 20px;
  height: 20px;
  flex: none;
  margin-top: 0.1em;
  border: 1px solid var(--void-200);
  background: var(--void-100);
  border-radius: 4px;
  cursor: pointer;
  position: relative;
  transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.form-field__checkbox-label input[type="checkbox"]:checked {
  background: var(--grad-corporate-primary);
  border-color: transparent;
}
.form-field__checkbox-label input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  left: 6px; top: 2px;
  width: 5px; height: 10px;
  border: solid var(--spectral-ink);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.form-field__checkbox-label input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--spectral-cyan);
  outline-offset: 2px;
}
.form-field__checkbox-label a { color: var(--electric-blue); text-decoration: underline; text-underline-offset: 2px; }
.form-field--checkbox.has-error .form-field__checkbox-label input[type="checkbox"] { border-color: var(--alert); }

.form-status {
  padding: var(--space-4);
  border: 1px solid var(--void-200);
  font-size: var(--text-sm);
  margin-top: var(--space-5);
  display: none;
}
.form-status.is-visible { display: block; }
.form-status--error { border-color: var(--alert); background: var(--alert-dim); }
.form-status--pending { color: var(--ink-100); }

/* =========================================================
   Footer
   ========================================================= */
.site-footer {
  border-top: var(--border-hair);
  padding-block: var(--space-8) var(--space-6);
}
.footer-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-7);
}
@media (min-width: 768px) {
  .footer-grid { grid-template-columns: 2fr repeat(3, 1fr); }
}
/* v0.1.1 fix (QA-05 — see docs/DEFECT_REGISTER.md): footer column titles
   are a styled non-heading element, not an <h4>, so the footer landmark no
   longer skips heading levels on pages whose in-page content is only an h1
   (e.g. /careers/, /contact/). Selector renamed from `.footer-col h4` to
   match the .footer-col__title class on the <p> in chrome.mjs. */
.footer-col__title {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-100);
  font-weight: 600;
  margin-bottom: var(--space-4);
}
.footer-col ul { display: flex; flex-direction: column; gap: var(--space-3); }
.footer-col a {
  font-size: var(--text-sm);
  color: var(--ink-100);
  transition: color var(--dur-base) var(--ease-glow);
}
/* Cinematic Visual Completion Pass §13: footer refinement only — flat
   ink-000 hover replaced with the same spectral interaction language
   used sitewide (nav links, buttons), not a flat white swap. Spacing/
   hierarchy/grid are otherwise untouched, per the brief's explicit
   "acceptable as-is, do not overdesign" instruction for this section. */
.footer-col a:hover,
.footer-col a:focus-visible {
  color: var(--electric-blue);
}
.footer-bottom {
  margin-top: var(--space-8);
  padding-top: var(--space-5);
  border-top: var(--border-hair);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  color: var(--ink-200);
}
/* Production Master Build §43/§47: legal business-identity disclosure,
   distinct row from the version/positioning line above — plain body
   type (not mono/uppercase), since this is a factual legal statement
   the visitor should be able to read comfortably, not a decorative
   studio-branding label. */
.footer-legal-line {
  margin-top: var(--space-4);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  color: var(--ink-200);
  max-width: 48rem;
}

/* =========================================================
   Prose (legal / news article body copy)
   ========================================================= */
.prose { max-width: 42rem; color: var(--ink-100); }
.prose h2 { color: var(--ink-000); font-size: var(--text-lg); margin-top: var(--space-8); margin-bottom: var(--space-3); }
.prose p { margin-bottom: var(--space-4); }
.prose a { color: var(--signal); text-decoration: underline; text-underline-offset: 3px; }
.prose ul { list-style: disc; padding-left: 1.4em; margin-bottom: var(--space-4); }
.prose li { margin-bottom: var(--space-2); }

/* =========================================================
   FAQ accordion (Production Master Build §41)
   Native <details>/<summary> — accessible and functional without JS.
   ========================================================= */
.faq-item {
  border-bottom: var(--border-hair);
  padding-block: var(--space-5);
}
.faq-item:first-child { border-top: var(--border-hair); }
.faq-item__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-md);
}
.faq-item__q::-webkit-details-marker { display: none; }
.faq-item__q:focus-visible { outline: 2px solid var(--spectral-cyan); outline-offset: 4px; }
.faq-item__icon {
  position: relative;
  width: 18px; height: 18px;
  flex: none;
}
.faq-item__icon::before,
.faq-item__icon::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  background: var(--electric-blue);
  transform: translate(-50%, -50%);
  transition: transform var(--dur-fast) var(--ease-out);
}
.faq-item__icon::before { width: 12px; height: 2px; }
.faq-item__icon::after { width: 2px; height: 12px; }
.faq-item[open] .faq-item__icon::after { transform: translate(-50%, -50%) rotate(90deg) scaleY(0); }
.faq-item__a {
  margin-top: var(--space-4);
  color: var(--ink-100);
  max-width: 40rem;
}

/* =========================================================
   404
   ========================================================= */
.error-page {
  min-height: 70vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* =========================================================
   Development-placeholder badge (art direction disclosure)
   ========================================================= */
.dev-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  /* v0.2 Phase 1 fix: this label sits directly on the .viewport__caption
     scrim (unlike .viewport__badge, which has its own opaque pill
     background) with no independent backing. An independent QA pass
     measured that --ink-200 (#6c7178, "tertiary/disabled text") caps
     out at roughly a 4.0:1 contrast ratio against this scrim at its
     maximum practical opacity — structurally below the 4.5:1 AA
     minimum for this text size, regardless of any artwork behind it.
     --ink-100 (verified 8.82:1 against opaque --void-000 in
     build/lib/verify.mjs) carries enough headroom to clear AA even
     under the translucent scrim. See docs/QA_RETEST_02.md §4a and
     docs/GAME_VISUAL_DIRECTION_v0.2.md §12 for the full history. */
  color: var(--ink-100);
}

/* =========================================================
   v0.2 Phase 1 — Game World visual system
   (Game Visual & Motion Engineering — see
   docs/GAME_VISUAL_DIRECTION_v0.2.md for the full design reasoning
   behind each game's palette, shape language and motion character.)

   Motion is scoped under .js-motion (progressive enhancement, same
   convention as [data-reveal] in base.css: no motion at all without
   JavaScript) and is already neutralized under
   @media (prefers-reduced-motion: reduce) by the sitewide rule in
   base.css, which zeroes every animation/transition duration — nothing
   game-world-specific is needed to satisfy that guardrail as long as
   motion stays CSS `animation`, never JS-driven per-frame updates.
   ========================================================= */

/* ---- Circuit Stray: fast, kinetic, directional ----
   v0.2 Phase 2B: the arcade slot's live game is now Chicken Adventure,
   rendered from raster key art (src/data/game-media.mjs), not this
   procedural SVG world — so `.world-circuit-stray` no longer appears in
   any live render path and these rules are currently unused. Left intact,
   not deleted, per change control: circuitStray.hero()/card() in
   game-worlds.mjs remain the documented fallback if this raster entry
   were ever removed from the registry (see game-media.mjs
   chicken-adventure.fallback), and these rules would apply again if that
   fallback ever renders. ---- */
.js-motion .world-circuit-stray .ws-trail {
  animation: ws-dash-flow 1.1s linear infinite;
}
.js-motion .world-circuit-stray .ws-eye-glow {
  animation: ws-pulse-fast 1.4s ease-in-out infinite alternate;
  transform-origin: center;
}
@keyframes ws-dash-flow {
  to { stroke-dashoffset: -24; }
}
@keyframes ws-pulse-fast {
  from { opacity: 0.75; transform: scale(0.94); }
  to { opacity: 1; transform: scale(1.06); }
}

/* ---- Apex Signal: slow atmospheric drift + rare cinematic flash ---- */
.js-motion .world-apex-signal .ws-storm {
  animation: ws-storm-drift 7s ease-in-out infinite alternate;
}
.js-motion .world-apex-signal .ws-bolt {
  animation: ws-bolt-flicker 6s ease-in-out infinite;
}
.js-motion .world-apex-signal .ws-trail {
  animation: ws-glow-breathe 3.5s ease-in-out infinite alternate;
}
@keyframes ws-storm-drift {
  from { opacity: 0.75; }
  to { opacity: 1; }
}
@keyframes ws-bolt-flicker {
  0%, 88%, 92%, 100% { opacity: 0; }
  89%, 91% { opacity: 0.9; }
  90% { opacity: 0.3; }
}
@keyframes ws-glow-breathe {
  from { opacity: 0.45; }
  to { opacity: 0.85; }
}

/* ---- Ash Throne: slow, monumental, occasional cinematic glow ---- */
.js-motion .world-ash-throne .ws-ember {
  animation: ws-ember-drift 5s ease-in infinite;
}
.js-motion .world-ash-throne .ws-ember:nth-of-type(2) { animation-delay: -1.6s; }
.js-motion .world-ash-throne .ws-ember:nth-of-type(3) { animation-delay: -3.1s; }
.js-motion .world-ash-throne .ws-ember:nth-of-type(4) { animation-delay: -0.6s; }
.js-motion .world-ash-throne .ws-shard {
  animation: ws-shard-glow 3s ease-in-out infinite alternate;
  transform-origin: center;
}
@keyframes ws-ember-drift {
  0% { transform: translateY(0); opacity: 0; }
  15% { opacity: 0.6; }
  100% { transform: translateY(-28px); opacity: 0; }
}
@keyframes ws-shard-glow {
  from { opacity: 0.8; transform: scale(0.96); }
  to { opacity: 1; transform: scale(1.08); }
}

/* ---- Game Detail: per-world background wash behind the corporate
   section frame. Cinematic Visual Completion Pass §11: previously this
   was five hand-authored `.world-<slug>` rules covering only 4 of the 12
   live games (the other 8 got no wash at all — part of why the
   experience "dropped" after the hero). Replaced with one generic,
   accent-driven pair of rules keyed off `--world-accent`/`--world-accent-2`
   custom properties set inline per game from its OWN category accent
   (gameDetail.mjs) — every game gets a wash automatically, colored from
   already-approved category tokens (categories.mjs), no new hex values
   invented. `.game-detail-atmosphere--hero` and `--continue` are two
   strengths of the same treatment: full behind the hero, softer behind
   "THE WORLD" section so the color carries past the hero instead of
   cutting off at its bottom edge. Kept low-opacity and edge-faded so it
   tints the world without competing with the studio's own section
   rhythm (.section + .section border, grid, typography) — corporate
   layer stays the frame, game layer stays the color underneath it. ---- */
.game-detail-atmosphere--hero {
  background: radial-gradient(ellipse 65% 70% at 70% 15%, color-mix(in srgb, var(--world-accent, var(--electric-blue)) 14%, transparent), transparent 70%);
}
.game-detail-atmosphere--continue {
  background: radial-gradient(ellipse 55% 60% at 15% 0%, color-mix(in srgb, var(--world-accent, var(--electric-blue)) 8%, transparent), transparent 72%);
}

/* =========================================================
   VISUAL POLISH IMPLEMENTATION / TIER A — Game Detail family system
   (implementation brief layers A + B). Approved 3-family architecture:
   KINETIC (arcade/flight/mythic/confection), MONUMENTAL (table/prism/
   expedition/royal), ATMOSPHERIC (aquatic/spectacle/social/abyssal) —
   see src/data/categories.mjs `family` field (INTERNAL ONLY, never
   visitor-facing). Every rule below is keyed off the existing
   `[data-family]` attribute set once on #world (gameDetail.mjs) and
   reuses ONLY existing tokens (--world-accent/--world-accent-2, motion
   durations/easings) and the existing .world-hook/.game-detail-
   atmosphere--continue selectors — no new component system, per the
   brief's explicit instruction. Motion is restricted this pass to
   REVEAL, GLOW, SHIFT and AMBIENT (docs/VISUAL_MOTION_DIRECTION_SPEC_v0.4.md):
   every family's motion below is one of those four, respects
   prefers-reduced-motion (via the sitewide .js-motion gate — main.js
   only adds that class when the media query does NOT match — and the
   global forced 0.001ms animation/transition-duration override in
   base.css), and never touches the hero (LCP-safe, untouched by this
   pass) or the .world-hook/.game-detail-atmosphere--continue selectors'
   layout, only their decorative treatment.
   ========================================================= */

/* Layer A — Family Transition (the hero -> "THE WORLD" seam).
   Replaces the generic `.section + .section` hairline for this specific
   seam only — every other section-to-section border sitewide keeps that
   rule unchanged. `.family-seam` is a real DOM element (not a
   pseudo-element) so it can carry `data-reveal` and be picked up by the
   existing, unmodified sitewide IntersectionObserver with zero JS
   changes. */
#world[data-family] { border-top: none; }

.family-seam {
  grid-column: 1/-1;
  position: relative;
  overflow: hidden;
}

/* KINETIC — thin directional streak. Allowed: "thin directional streak,
   controlled SHIFT, asymmetric crop placement"; avoid: "constant fast
   animation, large motion, speed-line cliché." Base state already shows
   the full streak (progressive enhancement — see [data-reveal]'s base
   rule in base.css); .js-motion animates it in ONCE via a SHIFT-style
   gradient-position change, triggered by the same one-shot REVEAL
   IntersectionObserver as every other [data-reveal] element — never a
   continuous/looping animation. */
[data-family="kinetic"] .family-seam {
  height: 2px;
  margin-bottom: var(--space-8);
  background: linear-gradient(90deg, transparent, var(--world-accent) 45%, var(--world-accent-2) 65%, transparent);
  background-size: 220% 100%;
  background-position: 0% 0;
}
.js-motion [data-family="kinetic"] .family-seam[data-reveal] {
  background-position: 100% 0;
  opacity: 0.4;
  transition: background-position var(--dur-reveal) var(--ease-out), opacity var(--dur-reveal) var(--ease-out);
}
.js-motion [data-family="kinetic"] .family-seam[data-reveal].is-visible {
  background-position: 0% 0;
  opacity: 1;
}

/* MONUMENTAL — architectural double-rule. Allowed: "double-rule,
   stronger architectural framing, static or near-static composition,
   controlled GLOW"; avoid: "generic gold luxury treatment, overly heavy
   borders, ornamental overload." Static double border by default; GLOW
   appears once (opacity/box-shadow ramp on reveal) and HOLDS — never a
   looping pulse, matching this project's GLOW definition sitewide. */
[data-family="monumental"] .family-seam {
  height: 6px;
  margin-bottom: var(--space-8);
  border-top: 1px solid color-mix(in srgb, var(--world-accent) 55%, var(--void-200));
  border-bottom: 1px solid color-mix(in srgb, var(--world-accent) 28%, var(--void-200));
}
.js-motion [data-family="monumental"] .family-seam[data-reveal] {
  box-shadow: none;
  transition: box-shadow var(--dur-reveal) var(--ease-glow);
}
.js-motion [data-family="monumental"] .family-seam[data-reveal].is-visible {
  box-shadow: 0 0 22px -4px color-mix(in srgb, var(--world-accent) 50%, transparent);
}

/* ATMOSPHERIC — soft gradient fade. Allowed: "gradient fade, soft
   environmental crop, slow AMBIENT drift"; avoid: "generic fog overlay,
   blur-heavy composition, large animated gradients." A wide, low-
   contrast vertical wash (not a hard line like the other two families)
   with a slow (--dur-ambient, 24s) opacity drift of ~8% amplitude —
   inside the "≤10% amplitude" AMBIENT ceiling — gated by .js-motion,
   which is itself never added when prefers-reduced-motion matches
   (main.js), so reduced-motion users get the static wash with the drift
   fully excluded, not merely slowed. */
[data-family="atmospheric"] .family-seam {
  height: 44px;
  margin-bottom: calc(var(--space-8) - 40px);
  background: linear-gradient(180deg, color-mix(in srgb, var(--world-accent) 16%, transparent), transparent);
}
.js-motion [data-family="atmospheric"] .family-seam {
  animation: family-seam-ambient var(--dur-ambient) var(--ease-glow) infinite alternate;
}
@keyframes family-seam-ambient {
  from { opacity: 0.92; }
  to { opacity: 1; }
}

/* Layer B — World Section Treatment. Restrained family-specific
   expression of "THE WORLD" section using ONLY --world-accent/
   --world-accent-2, the existing .game-detail-atmosphere--continue wash
   and .world-hook border — no new component. */
[data-family="kinetic"].game-detail-atmosphere--continue {
  /* Asymmetric, off-axis wash — echoes the family's "asymmetric crop
     placement" allowance without touching layout. */
  background: radial-gradient(ellipse 60% 65% at 8% 0%, color-mix(in srgb, var(--world-accent) 10%, transparent), transparent 68%);
}
[data-family="monumental"].game-detail-atmosphere--continue {
  /* Centered, symmetric wash — architectural/static reading. */
  background: radial-gradient(ellipse 48% 55% at 50% 0%, color-mix(in srgb, var(--world-accent) 9%, transparent), transparent 74%);
}
[data-family="monumental"] .world-hook {
  /* Second, fainter rule on the opposite edge — echoes the seam's own
     double-rule language on the heading itself; still just a border
     color drawn from --world-accent, no new border language invented. */
  border-right: 1px solid color-mix(in srgb, var(--world-accent) 22%, transparent);
  padding-right: var(--space-5);
}
[data-family="atmospheric"].game-detail-atmosphere--continue {
  /* Wide, shallow, calm wash — reads as horizon/ambient rather than a
     directional light source. */
  background: radial-gradient(ellipse 85% 45% at 30% 8%, color-mix(in srgb, var(--world-accent) 7%, transparent), transparent 80%);
}
[data-family="atmospheric"] .world-hook {
  border-left-width: 3px;
}

/* VISUAL POLISH IMPLEMENTATION / TIER A — MONUMENTAL's second
   (right-side) rule reads as a deliberate architectural frame at
   tablet/desktop column widths, but at this narrow a measure it sits
   close enough to the viewport edge to read as a stray clipped line
   rather than a considered double-rule. Suppressed here only; the left
   border (this game's one universal, sitewide world-hook treatment) is
   untouched. Positioned after the rule above so it reliably wins the
   cascade at this selector's identical specificity — see the moved-rule
   comment on the other `@media (max-width: 480px) { .world-hook {...} }`
   block earlier in this file (overnight pass §5 root-cause note). */
@media (max-width: 480px) {
  [data-family="monumental"] .world-hook {
    border-right: none;
    padding-right: 0;
  }
}

/* FINAL STABILIZATION & v1.0 RC PASS (§4): the "Layer C — World-Specific
   Visual Fragment" rule block that lived here (.world-fragment and its
   monumental/atmospheric family variants, plus a >=768px
   .section__head--split column override) has been removed as part of
   reverting the world-fragment system — see components.mjs's own
   removal comment for the full rationale. The base .section__head--split
   rule (src/styles/base.css) is untouched and still applies; "THE
   WORLD" section's typography now simply occupies its 7-column span
   with the remaining columns unused in row 1, exactly like the
   Wishlist section immediately below it already does. */

/* =========================================================
   Page atmosphere — Production Polish §03/§115
   A restrained decorative glow used behind secondary-page headers
   (About, Careers, Contact, FAQ, 404) so they read as part of the same
   Cinematic Spectral system as Home/Games rather than plain utility
   pages, without building each page its own bespoke hero composition.
   Purely decorative (aria-hidden, absolutely positioned, pointer-events
   none) — never a layout dependency, never the LCP candidate content.
   `--calm` is a lower-opacity variant reserved for the legal pages
   (Privacy/Terms/Cookies), which stay closer to the site's plain
   register per Design DNA (§34: "legal pages must remain calmer than
   marketing pages" — readability/trust over atmosphere).
   ========================================================= */
.page-atmosphere {
  position: absolute;
  inset: -8% -10% auto -10%;
  height: 34rem;
  max-height: 55vh;
  pointer-events: none;
  z-index: 0;
  overflow: hidden;
}
.page-atmosphere::before,
.page-atmosphere::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  filter: blur(70px);
}
.page-atmosphere::before {
  width: 30rem;
  height: 30rem;
  top: -12rem;
  right: -4rem;
  background: var(--grad-corporate-ambient);
  opacity: 0.32;
}
.page-atmosphere::after {
  width: 22rem;
  height: 22rem;
  top: 6rem;
  left: -6rem;
  background: radial-gradient(circle, var(--electric-blue) 0%, transparent 70%);
  opacity: 0.2;
}
.page-atmosphere--calm::before {
  opacity: 0.14;
  filter: blur(90px);
}
.page-atmosphere--calm::after {
  opacity: 0.08;
  filter: blur(90px);
}
.page-head--elevated {
  position: relative;
  z-index: 1;
}


/* VOIDLINE INTERACTIVE — Home v0.4 ("HOME EXPERIENCE")
   Every rule in this file is scoped by unique `home-*` class names that
   exist ONLY in build/pages/home.mjs's markup (plus the `.home` body
   class from pageShell({bodyClass:"home"})) — nothing here can visually
   affect any other page. See docs/VISUAL_MOTION_DIRECTION_SPEC_v0.4.md
   (approved with amendments) for the design-system decisions this file
   implements, and tokens.css's "Cinematic Spectral system" block for the
   new tokens used throughout.

   This file does not redefine or remove any token, selector or rule that
   base.css / components.css / tokens.css already own. Where a deeper
   void base is needed for Home's atmosphere, it is scoped locally to
   `.home` as a custom-property override (cascades to descendants of
   <body class="home"> only) rather than editing --void-000 etc. in
   tokens.css directly — every other page keeps the original values. */

.home {
  /* PRODUCTION MASTER BUILD — SUPERSEDED NOTE: the color role-split
     (--signal → electric-blue, cyan reserved for interaction, focus on
     cyan) and the Syne/Manrope/JetBrains Mono typography stack that used
     to be defined ONLY here were promoted sitewide in tokens.css and
     base.css this phase (see those files' "PROMOTED SITEWIDE" comments).
     They are no longer redeclared in this block — the root values now
     already match what Home always rendered, so removing the duplicate
     avoids two sources of truth for the same tokens. This preserves the
     prior phases' record rather than erasing it: see
     docs/COLOR_UI_DESIGN_SYSTEM_v1.0.md §2/§3 for the full disapproved →
     corrective → sitewide-promoted history.

     What remains genuinely Home-specific is the richer, violet-tinted
     "graphite" void surface below — tuned and screenshot-verified for
     Home's own dense, atmosphere-heavy composition (hero portal, mosaic,
     manifesto glow, featured-world cards). Other pages keep the calmer
     neutral --void-* foundation from tokens.css for text readability
     (documented assumption, brief §29/IMPORTANT — see tokens.css). */
  --void-000: var(--spectral-ink);
  --void-100: #1b1730;
  --void-150: #221c3c;
  --void-200: #322a54;
}

/* =========================================================
   Shared: eyebrow / headline / gradient-text-accent
   ========================================================= */
.home-eyebrow {
  /* Corrective phase: moved off flat --spectral-cyan (the exact
     "cyan-as-new-lime" pattern the Product Owner disapproved — every one
     of Home's 8 section heads used this same static colour). Section
     eyebrows are a static, always-visible label, not an interactive
     highlight, so they take the soft-lavender secondary accent instead;
     cyan is now reserved for interaction/glow states elsewhere on the
     page (nav hover/active, focus rings, button focus). */
  color: var(--spectral-lavender);
  margin-bottom: var(--space-3);
}
.home-section__head {
  max-width: 44rem;
  margin-bottom: var(--space-7);
}
.home-section__head--row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-5);
  max-width: none;
}
/* v0.4 Typography & Interactive Geometry Refinement — word-breaking
   correction (see docs/HOME_TYPOGRAPHY_GEOMETRY_v0.4.md §5). The three
   section-headline classes below previously shared a single FIXED
   font-size (var(--text-2xl), 56px) with no responsive reduction. This
   fluid clamp is deliberate, controlled sizing tuned and screenshot-
   verified at 1440/1024/768/390/375/320px against every named Home
   headline string. Reaches the original 56px at ~1024px and holds there
   through wide desktop, unchanged from the previously-approved desktop
   appearance.

   v0.11.2 note: --text-2xl (tokens.css) is itself now this same fluid
   clamp value sitewide, and base.css's h1-h4 rule no longer carries an
   overflow-wrap:anywhere backstop (removed — it permitted arbitrary
   mid-word splitting, which VOIDLINE's typography rule forbids). This
   class-level clamp is kept as-is; it already matched --text-2xl's new
   value, so this is a no-op for Home's own appearance. */
.home-headline,
.home-manifesto__statement,
.home-final-cta__headline {
  font-size: clamp(1.85rem, 1.3rem + 3.8vw, 3.5rem);
}
.home-headline {
  max-width: 20ch;
}
/* Color-as-event (§14): background-clip text accent, used on at most one
   line per headline sitewide on this page — never a large flat gradient
   surface. */
.home-gradient-text {
  background: var(--grad-text-accent);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* =========================================================
   01 — Hero
   ========================================================= */
.home-hero {
  isolation: isolate;
}
.home-hero__atmosphere {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  /* Radial ambient fields only — never a large flat gradient fill
     (§14). Rendered with the page in the very first frame: no image
     request, no JS dependency, nothing here gates the hero text below.

     CORRECTIVE PHASE — brightened and re-layered per the "much higher
     brightness target" / "cinematic environmental hero" requirement,
     while staying a pure CSS composition (no new raster asset: this
     project still has no generic "studio hero" master art and no
     image-generation tool available this session — see the hard LCP
     rule at the top of build/pages/home.mjs, unchanged). Violet and
     electric-blue now carry most of the field (bigger, brighter ellipses
     = the "primary atmosphere" per the brief); one restrained cyan
     highlight is layered near the title as a localized glow rather than
     a wash; a starlight core-light sits behind the eyebrow/title
     intersection to read as an actual light source, not just colour. */
  background:
    radial-gradient(ellipse 70% 60% at 80% 12%, color-mix(in srgb, var(--spectral-violet) 34%, transparent), transparent 72%),
    radial-gradient(ellipse 60% 70% at 98% 58%, color-mix(in srgb, var(--electric-blue) 26%, transparent), transparent 72%),
    radial-gradient(ellipse 30% 30% at 62% 22%, color-mix(in srgb, var(--spectral-cyan) 20%, transparent), transparent 75%),
    radial-gradient(ellipse 45% 40% at 58% 80%, color-mix(in srgb, var(--spectral-magenta) 14%, transparent), transparent 70%),
    radial-gradient(ellipse 22% 24% at 18% 30%, color-mix(in srgb, var(--starlight) 10%, transparent), transparent 80%);
}
.home-hero__portal {
  position: absolute;
  top: 0; right: 0;
  width: 100%; height: 100%;
  opacity: 0.85;
}
/* Cinematic Visual Completion Pass §3/§16 — "spectral portal" system.
   Depth-planed: far arcs (recessed, dim, largest radius) → rim arcs
   (mid-plane, brighter, asymmetric partial paths, never a full circle)
   → core glow (nearest, brightest) → rays/particles (foreground detail).
   Each plane drifts at a different amplitude/speed under .js-motion so
   the parallax itself reads as depth, not just as individual shapes
   moving — still opacity/transform only (compositor-friendly, §19),
   still fully neutralized by the sitewide prefers-reduced-motion rule. */
.hh-arc {
  fill: none;
  stroke-width: 1;
  stroke: color-mix(in srgb, var(--spectral-violet) 30%, transparent);
}
.hh-arc-far-b { stroke: color-mix(in srgb, var(--electric-blue) 24%, transparent); }
.hh-core { opacity: 0.9; }
.hh-rim {
  fill: none;
  stroke-width: 1.25;
  stroke-linecap: round;
}
.hh-rim-a { stroke: color-mix(in srgb, var(--electric-blue) 70%, transparent); filter: drop-shadow(0 0 5px var(--glow-electric)); }
.hh-rim-b { stroke: color-mix(in srgb, var(--spectral-cyan) 55%, transparent); filter: drop-shadow(0 0 6px var(--glow-cyan)); }
.hh-rim-c { stroke: color-mix(in srgb, var(--spectral-violet) 55%, transparent); }
.hh-rays {
  stroke: color-mix(in srgb, var(--starlight) 30%, transparent);
  stroke-width: 1;
}
.hh-struts { fill: none; stroke: color-mix(in srgb, var(--ink-200) 40%, transparent); stroke-width: 1; }
.hh-stars-dots { fill: var(--starlight); }
.hh-stars-dots circle { filter: drop-shadow(0 0 3px color-mix(in srgb, var(--starlight) 60%, transparent)); }

/* AMBIENT tier (§17): 24s barely-noticeable drift per depth plane,
   excluded entirely without JS and neutralized by base.css's
   prefers-reduced-motion rule (it zeroes animation-duration globally).
   Decorative layer only — the hero text above it never depends on this
   running. */
.js-motion .hh-arc-far-a { animation: home-arc-drift var(--dur-ambient) var(--ease-glow) infinite alternate; }
.js-motion .hh-arc-far-b { animation: home-arc-drift calc(var(--dur-ambient) * 1.15) var(--ease-glow) infinite alternate-reverse; }
.js-motion .hh-core { animation: home-core-pulse calc(var(--dur-ambient) * 0.5) var(--ease-glow) infinite alternate; }
.js-motion .hh-rim-a { animation: home-ring-drift var(--dur-ambient) var(--ease-glow) infinite alternate; }
.js-motion .hh-rim-b { animation: home-ring-drift calc(var(--dur-ambient) * 0.8) var(--ease-glow) infinite alternate-reverse; }
.js-motion .hh-rim-c { animation: home-ring-drift calc(var(--dur-ambient) * 0.6) var(--ease-glow) infinite alternate; }
@keyframes home-ring-drift {
  from { transform: scale(1) rotate(0deg); opacity: 0.7; }
  to { transform: scale(1.04) rotate(2deg); opacity: 1; }
}
@keyframes home-arc-drift {
  from { transform: scale(1) rotate(0deg); opacity: 0.5; }
  to { transform: scale(1.02) rotate(-1.5deg); opacity: 0.75; }
}
@keyframes home-core-pulse {
  from { opacity: 0.75; transform: scale(1); }
  to { opacity: 1; transform: scale(1.05); }
}

.home-hero__title {
  /* v0.4 responsive art-direction fix (found this phase during 320-390px
     visual QA, not present in the original brief): the sitewide h1 fluid
     clamp (--text-3xl, tokens.css) sized "INTERACTIVE" — this title's
     longest single word, and the studio's own wordmark — wide enough to
     force a mid-word break at narrow widths even with the deliberate
     <br> already splitting VOIDLINE/INTERACTIVE onto separate lines.
     >=1024px keeps the sitewide --text-3xl value unchanged (verified
     correct at 1440px). Below that, a Home-scoped size (tuned and
     re-screenshotted at 320/375/390px — see final report §H) keeps
     "INTERACTIVE" on one line at every required breakpoint.

     v0.11.2 note: base.css's h1-h4 rule no longer carries an
     overflow-wrap:anywhere backstop (removed sitewide — it permitted
     arbitrary mid-word splitting). This override is the sole mechanism
     keeping "INTERACTIVE" unsplit below 1024px; it has been re-verified
     clean (no overflow, no split) at 1440/1024/768/390/375/320px with
     that backstop gone. */
  font-size: var(--text-3xl);
}
@media (max-width: 1023px) {
  .home-hero__title {
    font-size: clamp(1.9rem, 8.5vw, 3.5rem);
  }
}
.home-hero__message {
  font-size: var(--text-lg);
  color: var(--ink-000);
  font-family: var(--font-display);
  font-weight: 700;
  max-width: 28rem;
}
.home-hero__supporting {
  grid-column: 1 / -1;
  margin-top: var(--space-3);
  max-width: 34rem;
  color: var(--ink-100);
}
@media (min-width: 768px) {
  .home-hero__supporting { grid-column: 1 / span 6; }
}

/* =========================================================
   Buttons, mobile Menu control, nav hover/active, logo glow
   ========================================================= */
/* PRODUCTION MASTER BUILD — SUPERSEDED NOTE: buttons (rounded, spectral
   gradient primary, secondary, ghost, all states), the mobile Menu
   control's rounded shape, the logo mark's glow, and the primary-nav
   luminous hover/active underline were all promoted from Home-only
   overrides to sitewide rules in components.css this phase. `.btn` and
   `.btn--spectral` are now visually identical sitewide (the gradient
   fill lives on `.btn` itself); `.btn--spectral` is kept as a harmless
   alias on Home's two markup call sites (build/pages/home.mjs) rather
   than removed, avoiding an unnecessary markup churn for zero visual
   change. No rules remain to duplicate here. */
.btn--spectral { /* alias — see components.css `.btn` for the full spec */ }

/* =========================================================
   02 — World Transition: featured-3 (locked)
   ========================================================= */
/* CORRECTIVE PHASE — dark/light section rhythm (brief §15/§17): this is
   the page's first "beat" after the hero and its own showcase moment for
   the three flagship worlds, so it gets a raised, lit atmosphere instead
   of sitting flush on the same flat void-000 as the sections around it.
   Purely a full-bleed radial wash behind existing content — no markup
   change, no effect on the featured-grid's own layout, IA or the locked
   featured-3 content. */
.home-worlds {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.home-worlds::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(ellipse 80% 60% at 50% 0%, color-mix(in srgb, var(--spectral-violet) 16%, transparent), transparent 72%),
    radial-gradient(ellipse 60% 50% at 90% 100%, color-mix(in srgb, var(--electric-blue) 12%, transparent), transparent 70%);
}
.home-worlds__supporting {
  margin-top: var(--space-4);
  color: var(--ink-100);
}
.home-featured-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
}
@media (min-width: 900px) {
  .home-featured-grid { grid-template-columns: repeat(3, 1fr); }
}
.home-featured {
  position: relative;
  display: block;
  color: inherit;
  min-width: 0; /* same fix as .home-mosaic__item above */
}
.home-featured__index {
  position: absolute;
  top: var(--space-4);
  left: var(--space-4);
  z-index: 2;
  color: var(--ink-000);
  background: color-mix(in srgb, var(--void-000) 55%, transparent);
  padding: 0.3em 0.6em;
  border: 1px solid color-mix(in srgb, var(--game-accent, var(--spectral-cyan)) 60%, transparent);
}
.home-featured__art { position: relative; }
.home-featured__art .viewport { transition: border-color var(--dur-shift) var(--ease-glow); }
.home-featured__art .viewport-art img,
.home-featured__art .viewport-art svg {
  transition: transform var(--dur-shift) var(--ease-glow);
}
.home-featured__meta {
  position: relative;
  margin-top: -1px;
  background: var(--void-100);
  border: var(--border-hair);
  border-top: none;
  clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
  padding: var(--space-5);
  transition: border-color var(--dur-shift) var(--ease-glow), transform var(--dur-fast) var(--ease-out);
}
.home-featured__class {
  color: var(--game-accent, var(--spectral-cyan));
}
.home-featured__line {
  margin-top: var(--space-3);
  font-size: var(--text-md);
}
.home-featured__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  color: var(--ink-100);
  /* v0.4 cyber/terminal reduction (§21): this is actionable UI link
     text, not a numeric index or metadata micro-label, so it moved out
     of `.u-mono` (JetBrains Mono, brief §7's "NOT the default... nav
     typeface") into Manrope — the eyebrow/index/classification labels
     elsewhere on this card correctly stay mono accents and are
     untouched. */
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}
.home-featured__link .icon { transition: transform var(--dur-fast) var(--ease-out); }

.home-featured:hover .home-featured__art .viewport,
.home-featured:focus-visible .home-featured__art .viewport {
  border-color: var(--game-accent, var(--spectral-cyan));
}
.home-featured:hover .home-featured__art .viewport-art img,
.home-featured:hover .home-featured__art .viewport-art svg,
.home-featured:focus-visible .home-featured__art .viewport-art img,
.home-featured:focus-visible .home-featured__art .viewport-art svg {
  transform: scale(1.04);
}
.home-featured:hover .home-featured__meta,
.home-featured:focus-visible .home-featured__meta {
  border-color: var(--game-accent, var(--spectral-cyan));
  transform: translateY(-2px);
  box-shadow: 0 24px 60px -32px var(--game-accent, var(--spectral-violet));
}
.home-featured:hover .home-featured__link .icon,
.home-featured:focus-visible .home-featured__link .icon {
  transform: translateX(3px);
}
.home-featured:focus-visible {
  outline: 2px solid var(--spectral-cyan);
  outline-offset: 3px;
}

/* CINEMATIC tier (§17): the featured-world reveal is one of the rare
   high-value motion moments. Below-fold content only (this grid is never
   the first-screen LCP candidate — see the hard rule at the top of
   home.mjs) so a controlled mask/translate entrance is authorized here
   per §18. Base state (no JS, or JS present before intersection) is
   fully visible — [data-reveal-home] never hides content on its own. */
[data-reveal-home] { opacity: 1; transform: none; }
.js-motion [data-reveal-home="cinematic"] {
  opacity: 0;
  transform: translateY(18px) scale(0.99);
  transition: opacity var(--dur-mask) var(--ease-glow), transform var(--dur-mask) var(--ease-glow);
}
.js-motion [data-reveal-home="cinematic"].is-visible-home {
  opacity: 1;
  transform: none;
}
.js-motion [data-reveal-home="cinematic"] .home-featured {
  transition: opacity var(--dur-reveal) var(--ease-glow), transform var(--dur-reveal) var(--ease-glow);
}
.js-motion [data-reveal-home="cinematic"]:not(.is-visible-home) .home-featured {
  opacity: 0;
  transform: translateY(10px);
}
.js-motion [data-reveal-home="cinematic"] .home-featured:nth-child(2) { transition-delay: 80ms; }
.js-motion [data-reveal-home="cinematic"] .home-featured:nth-child(3) { transition-delay: 160ms; }

/* =========================================================
   03 — Explore the Worlds: editorial mosaic
   ========================================================= */
.home-mosaic {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-auto-rows: minmax(9rem, auto);
  grid-auto-flow: dense;
  gap: var(--space-4);
}
/* min-width:0 fix (same bug class as the QA-03 fix in base.css/components.css
   — see docs/DEFECT_REGISTER.md): a grid item's automatic minimum size
   defaults to its content's min-content size, and a nested <img> with a
   real intrinsic (pixel) width bubbles that size up through the `1fr`
   track unless the item's own min-width is reset to 0. Found this phase
   via node build/lib/functional-check.mjs's existing narrow-viewport
   overflow sweep (QA-03) after introducing the mosaic's 2-col mobile
   layout — see final report §K. */
.home-mosaic__item { grid-column: span 3; grid-row: span 1; min-width: 0; }
.home-mosaic__item--large { grid-column: span 4; grid-row: span 2; }
.home-mosaic__item--vertical { grid-column: span 2; grid-row: span 2; }
.home-mosaic__item--wide { grid-column: span 6; grid-row: span 1; }
.home-mosaic__item--medium { grid-column: span 3; grid-row: span 1; }
.home-mosaic__item--small { grid-column: span 3; grid-row: span 1; }
.home-mosaic__item .panel-link,
.home-mosaic__item .panel { height: 100%; }
.home-mosaic__item .viewport { transition: transform var(--dur-shift) var(--ease-glow); }
.home-mosaic__item .viewport-art img,
.home-mosaic__item .viewport-art svg { transition: transform var(--dur-shift) var(--ease-glow); }
.home-mosaic__item .panel-link:hover .viewport-art img,
.home-mosaic__item .panel-link:hover .viewport-art svg,
.home-mosaic__item .panel-link:focus-visible .viewport-art img,
.home-mosaic__item .panel-link:focus-visible .viewport-art svg {
  transform: scale(1.03);
}
/* Every mosaic item must remain independently discoverable/keyboard-
   reachable regardless of visual size — the irregular grid never removes
   tab order or the underlying <a>. */
@media (max-width: 767px) {
  .home-mosaic { grid-template-columns: repeat(2, 1fr); grid-auto-rows: auto; }
  .home-mosaic__item,
  .home-mosaic__item--large,
  .home-mosaic__item--vertical,
  .home-mosaic__item--wide,
  .home-mosaic__item--medium,
  .home-mosaic__item--small {
    grid-column: span 1;
    grid-row: span 1;
  }
  .home-mosaic__item--wide { grid-column: span 2; }
}
/* v0.11.2 TYPOGRAPHY QA CORRECTION: the 2-column mobile layout above
   leaves each card only ~132px wide, and .panel's padding (--space-6,
   32px per side) reduces that to ~68px of actual text width — too
   narrow for a real translated tagline word (e.g. German
   "Abenddämmerung", 14 chars) to fit on one line at any reasonable
   font-size. components.css's .panel__body previously papered over this
   with overflow-wrap:anywhere, which let the word split mid-character —
   a violation of VOIDLINE's typography rule (words must never be split
   internally). ROOT-CAUSE FIX: drop to a single column below 420px,
   giving each card ~216px of text width (280px shell content - 64px
   panel padding), which fits the longest known tagline word without
   splitting or overflowing (verified: zero overflow, zero splits across
   all 6 locales at 320/375/390px). This is a narrower breakpoint than
   the existing 767px 2-column step, so tablet/large-phone layouts are
   unaffected. */
@media (max-width: 420px) {
  .home-mosaic { grid-template-columns: 1fr; }
  .home-mosaic__item,
  .home-mosaic__item--large,
  .home-mosaic__item--vertical,
  .home-mosaic__item--wide,
  .home-mosaic__item--medium,
  .home-mosaic__item--small {
    grid-column: span 1;
  }
}

/* =========================================================
   Manifesto
   ========================================================= */
.home-manifesto {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: var(--space-9);
}
/* CORRECTIVE PHASE — "manifesto panel" luminous glow (Reference A v2.0
   shows the manifesto as a lit panel, not flat text on void). A single
   soft radial anchored behind the statement — no new markup, sits under
   the existing clip-path-driven reveal (home-manifesto__statement is
   inside the `.shell` the mask animates, so the glow itself is part of
   the section background and is unaffected by that animation). */
.home-manifesto::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(ellipse 55% 65% at 22% 40%, color-mix(in srgb, var(--spectral-lavender) 16%, transparent), transparent 72%);
}
.home-manifesto__statement {
  max-width: 18ch;
}
.home-manifesto__body {
  margin-top: var(--space-5);
  max-width: 30rem;
  color: var(--ink-100);
  font-size: var(--text-md);
}
/* Overlay-retreat MASK family (§17/§18): distinct from the cinematic
   translate/scale family above and from the line-draw family below —
   below-fold only, base state fully visible without JS.

   DEFECT FOUND AND FIXED this phase (v0.4 Typography & Interactive
   Geometry Refinement — discovered while screenshot-verifying the
   manifesto headline's word-breaking at every required breakpoint; see
   docs/HOME_TYPOGRAPHY_GEOMETRY_v0.4.md §H for the full diagnostic).
   ROOT CAUSE (VERIFIED via an instrumented IntersectionObserver probe):
   applying `clip-path: inset(0 0 100% 0)` directly to the OBSERVED
   element (the <section data-reveal-home="mask"> itself, per
   build/pages/home.mjs) collapses that element's rendered/clipped box
   to zero visible height. Chromium computes IntersectionObserver's
   intersection rectangle against that clipped box, not the element's
   full layout box, so `entry.isIntersecting` was always false — even
   with the section scrolled fully into view (boundingClientRect
   correctly centered in the viewport; intersectionRect measured
   {0,0,0,0}, ratio 0) — so src/scripts/main.js's reveal-home observer
   never added `.is-visible-home`, so the mask never opened. This
   self-defeating combination affected only this one family (the other
   three — cinematic/line/stagger — hide via opacity/transform, which do
   not suppress intersection computation) and meant the manifesto
   section's headline and body were permanently invisible to any visitor
   with JavaScript enabled and motion not reduced — a real, previously
   undetected defect in the already-shipped v0.4 baseline, not something
   this refinement introduced.
   FIX: the clip-path now targets the section's inner `.shell` wrapper
   instead of the observed element itself. The <section> stays fully
   unclipped (so IntersectionObserver measures its real, nonzero
   intersection area and fires normally); the visual collapse/reveal
   still happens, one level down, exactly as before. No JS or markup
   change was required. Base (no-JS) state is unaffected: `.shell` has
   no clip-path at all outside `.js-motion`, so it renders fully visible
   by default, matching every other reveal family's no-JS contract. */
.js-motion [data-reveal-home="mask"] > .shell {
  clip-path: inset(0 0 100% 0);
  transition: clip-path var(--dur-mask) var(--ease-glow);
}
.js-motion [data-reveal-home="mask"].is-visible-home > .shell {
  clip-path: inset(0 0 0% 0);
}

/* =========================================================
   How We Build Worlds — principles
   ========================================================= */
.home-principles__list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  border-top: var(--border-hair);
}
@media (min-width: 700px) {
  .home-principles__list { grid-template-columns: repeat(3, 1fr); }
}
.home-principle {
  position: relative;
  padding: var(--space-6) var(--space-5);
  border-bottom: var(--border-hair);
}
@media (min-width: 700px) {
  .home-principle { border-right: var(--border-hair); border-bottom: none; }
  .home-principle:last-child { border-right: none; }
}
.home-principle__index {
  /* Corrective phase: same static-label fix as .home-eyebrow above —
     moved off flat cyan to soft-lavender; cyan stays reserved for
     interaction/focus states only. */
  color: var(--spectral-lavender);
}
.home-principle__icon {
  display: block;
  width: 28px; height: 28px;
  margin-block: var(--space-4);
  color: var(--ink-000);
}
.home-principle__icon .icon { width: 100%; height: 100%; }
.home-principle__title {
  font-size: var(--text-md);
}
.home-principle__body {
  margin-top: var(--space-2);
  color: var(--ink-100);
  font-size: var(--text-sm);
}
/* Typography/LINE-DRAW reveal family: a hairline draws in above each
   principle before the text settles — a third distinct treatment from
   MASK (manifesto) and CINEMATIC (featured worlds), per §18's
   instruction to vary reveal treatment across sections. */
.home-principle::before {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 0;
  height: 1px;
  background: var(--grad-corporate-edge);
}
.js-motion .home-principle { opacity: 0; transform: translateY(8px); transition: opacity var(--dur-reveal) var(--ease-glow), transform var(--dur-reveal) var(--ease-glow); }
.js-motion .home-principle.is-visible-home { opacity: 1; transform: none; }
.js-motion .home-principle.is-visible-home::before {
  width: 100%;
  transition: width var(--dur-reveal) var(--ease-glow);
}

/* =========================================================
   All Worlds — secondary strip navigation
   ========================================================= */
.home-strip-section .shell { margin-bottom: var(--space-5); }
.home-strip {
  display: flex;
  gap: var(--space-4);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding-inline: var(--gutter);
  padding-bottom: var(--space-3);
  /* Native horizontal touch scrolling — never dependent on hover, never
     scroll-jacked (§8/§19: this list never hijacks the page's own
     vertical scroll). */
  -webkit-overflow-scrolling: touch;
}
.home-strip__item {
  position: relative;
  flex: 0 0 11rem;
  scroll-snap-align: start;
  color: inherit;
}
.home-strip__item .viewport { aspect-ratio: 4 / 5; }
.home-strip__item .viewport-art img,
.home-strip__item .viewport-art svg { transition: transform var(--dur-shift) var(--ease-glow); }
.home-strip__item:hover .viewport,
.home-strip__item:focus-visible .viewport {
  border-color: var(--game-accent, var(--spectral-cyan));
}
.home-strip__item:hover .viewport-art img,
.home-strip__item:hover .viewport-art svg,
.home-strip__item:focus-visible .viewport-art img,
.home-strip__item:focus-visible .viewport-art svg {
  transform: scale(1.05);
}
.home-strip__index {
  position: absolute;
  top: var(--space-3);
  left: var(--space-3);
  z-index: 2;
  color: var(--ink-000);
}
.home-strip__name {
  display: block;
  margin-top: var(--space-3);
  /* v0.4 cyber/terminal reduction (§21): a game's own name is product
     content, not technical metadata — moved from JetBrains Mono to
     Manrope, dropping the heavy uppercase/tracking HUD treatment. The
     numeric index above it (.home-strip__index) is a legitimate mono
     accent per the brief and is untouched. */
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink-100);
}
.home-strip__item:hover .home-strip__name,
.home-strip__item:focus-visible .home-strip__name {
  color: var(--ink-000);
}
.home-strip__item:focus-visible {
  outline: 2px solid var(--spectral-cyan);
  outline-offset: 3px;
}

/* =========================================================
   Studio — corporate identity returns (no game key art here)
   ========================================================= */
.home-studio {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.home-studio__atmosphere {
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Corrective phase: raised from 0.35 toward the brief's higher
     brightness target — still clearly secondary to the hero/final-CTA
     "beats", but no longer reading as nearly-invisible. */
  opacity: 0.5;
}
.home-studio__monolith { position: absolute; inset: 0; width: 100%; height: 100%; }
/* Corrective phase: duo-tone fill (violet + electric-blue, was violet-only)
   with a starlight rim-light stroke on the tallest block, so the
   monolith reads as lit from one side rather than as a flat silhouette —
   matching Reference A v2.0's "monumental, lit architecture" language
   for this section without introducing any new asset. */
.hs-block {
  fill: color-mix(in srgb, var(--spectral-violet) 16%, transparent);
  stroke: color-mix(in srgb, var(--electric-blue) 45%, transparent);
  stroke-width: 1;
}
.hs-block-c { stroke: var(--starlight); stroke-width: 1.25; filter: drop-shadow(0 0 6px var(--glow-electric)); }
.hs-ground { stroke: var(--void-200); stroke-width: 1; }
.js-motion .hs-block-a { animation: home-block-drift var(--dur-ambient) var(--ease-glow) infinite alternate; }
.js-motion .hs-block-b { animation: home-block-drift calc(var(--dur-ambient) * 0.7) var(--ease-glow) infinite alternate-reverse; }
.js-motion .hs-block-c { animation: home-block-drift calc(var(--dur-ambient) * 0.9) var(--ease-glow) infinite alternate; }
@keyframes home-block-drift {
  from { transform: translateY(0); opacity: 0.85; }
  to { transform: translateY(-6px); opacity: 1; }
}
/* FINAL BRAND IDENTITY INTEGRATION PASS: .home-studio__inner was
   previously the section's only direct child of .shell; it is now one of
   two children of the new .home-studio__layout row (text column + the
   full-emblem brand-signature column added alongside it — see
   build/pages/home.mjs). max-width is unchanged so the text column's own
   measure/line-length is identical to before this pass. */
.home-studio__layout {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-8);
}
.home-studio__inner { max-width: 40rem; }
.home-studio__body {
  margin-top: var(--space-5);
  color: var(--ink-100);
  font-size: var(--text-md);
  max-width: 34rem;
}
.home-studio__inner .btn { margin-top: var(--space-6); }

.home-studio__emblem {
  flex-shrink: 0;
  width: min(22rem, 32vw);
  /* Restrained ambient glow, consistent with the header mark's treatment
     and the emblem's own spectral rim-light — not a new visual effect. */
  filter: drop-shadow(0 0 32px color-mix(in srgb, var(--spectral-violet) 22%, transparent));
}
.home-studio__emblem img { display: block; width: 100%; height: auto; }
/* Brief §11: below 768px there is no longer enough width for the emblem
   at a size where its own embedded wordmark/tagline stays legible
   (32vw of a 767px viewport is ~245px, where "WORLDS BEYOND PLAY"
   already reads as noise) — hidden rather than forced small; the
   section's heading/body text carries the studio identity on mobile. */
@media (max-width: 767px) {
  .home-studio__layout { flex-direction: column; align-items: flex-start; }
  .home-studio__emblem { display: none; }
}

/* =========================================================
   REMOVE LEGACY NEWS FROM PUBLIC RELEASE pass: the "Transmissions"
   section and its .home-transmission-card / [data-reveal-home="stagger"]
   rules previously here were scoped exclusively to that now-removed
   Home section (data-reveal-home="stagger" was never used anywhere
   else on this page) — removed as genuinely dead CSS, not touching any
   rule shared with another section. See CHANGELOG.md /
   PRODUCTION_READINESS.md §38.
   ========================================================= */

/* =========================================================
   Final CTA
   ========================================================= */
.home-final-cta {
  position: relative;
  padding-block: var(--space-9);
  border-top: var(--border-hair);
  overflow: hidden;
  isolation: isolate;
}
.home-final-cta::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Corrective phase: brighter closing beat, third stop adds a restrained
     starlight glint near the CTA itself so the page's final moment reads
     as the brightest, most luminous point — consistent with ending on
     the highest-energy state rather than fading out. */
  background:
    radial-gradient(ellipse 70% 80% at 30% 30%, color-mix(in srgb, var(--spectral-violet) 30%, transparent), transparent 70%),
    radial-gradient(ellipse 60% 70% at 80% 70%, color-mix(in srgb, var(--spectral-magenta) 20%, transparent), transparent 70%),
    radial-gradient(ellipse 30% 30% at 15% 75%, color-mix(in srgb, var(--starlight) 10%, transparent), transparent 75%);
}
.home-final-cta__inner { max-width: 34rem; }
.home-final-cta__headline {
  max-width: 16ch;
}
.home-final-cta__supporting {
  margin-top: var(--space-4);
  color: var(--ink-100);
}
.home-final-cta .btn { margin-top: var(--space-6); }
.js-motion [data-reveal-home="cinematic"].home-final-cta {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--dur-mask) var(--ease-glow), transform var(--dur-mask) var(--ease-glow);
}
.js-motion [data-reveal-home="cinematic"].home-final-cta.is-visible-home {
  opacity: 1;
  transform: none;
}


/* VOIDLINE INTERACTIVE — Secondary-page visual compositions
   Cinematic Visual Completion Pass. Each block below is scoped to one
   page's own body class (set via pageShell({bodyClass}) — same pattern
   home.css already uses for `.home`), so none of this affects any other
   page's rendered output. Motion here follows the same rules as
   home.css: opacity/transform only, gated under `.js-motion`, and fully
   neutralized by base.css's sitewide prefers-reduced-motion rule. */

/* =========================================================
   Shared: split-hero layout (About / Create a Game With Us / Contact)
   ========================================================= */
.split-hero {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-7);
  align-items: center;
  min-height: 32rem;
}
@media (min-width: 900px) {
  .split-hero {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr);
    gap: var(--space-8);
  }
}
/* base.css's .section__head defaults to `grid-column: 1 / -1` for use
   inside .grid-12 layouts — inside .split-hero's own 2-column grid that
   would force it to span both columns and push the visual onto its own
   row below. Reset it back to normal auto-placement here so the two
   children actually sit side by side. */
.split-hero > .section__head {
  grid-column: auto;
  margin-bottom: 0;
  max-width: none;
}
.split-hero__visual {
  position: relative;
  min-height: 20rem;
  overflow: hidden;
}
.split-hero__visual svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
@media (max-width: 899px) {
  .split-hero__visual { min-height: 14rem; opacity: 0.8; order: -1; }
}

/* =========================================================
   About — monolith / architecture
   ========================================================= */
.am-beam { opacity: 0.7; }
.am-block {
  fill: color-mix(in srgb, var(--void-100) 70%, var(--spectral-violet) 8%);
  stroke: color-mix(in srgb, var(--electric-blue) 35%, transparent);
  stroke-width: 1;
}
.am-block-c { stroke: color-mix(in srgb, var(--spectral-cyan) 45%, transparent); filter: drop-shadow(0 0 8px var(--glow-cyan)); }
.am-ground { stroke: var(--void-200); stroke-width: 1; }
.am-stars { fill: var(--starlight); }
.js-motion .am-block-a { animation: about-block-drift var(--dur-ambient) var(--ease-glow) infinite alternate; }
.js-motion .am-block-c { animation: about-block-drift calc(var(--dur-ambient) * 0.8) var(--ease-glow) infinite alternate-reverse; }
.js-motion .am-beam { animation: about-beam-pulse calc(var(--dur-ambient) * 0.5) var(--ease-glow) infinite alternate; }
@keyframes about-block-drift { from { transform: translateY(0); } to { transform: translateY(-6px); } }
@keyframes about-beam-pulse { from { opacity: 0.5; } to { opacity: 0.85; } }

.about-page .section__head { position: relative; }
.about-page .about-statement {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2rem, 1.3rem + 3vw, var(--text-xl));
  max-width: 22ch;
  text-align: center;
  margin: 0 auto;
  padding: var(--space-9) 0;
}
.about-page .about-alt {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
  max-width: var(--content-max);
}
@media (min-width: 820px) {
  .about-page .about-alt { grid-template-columns: repeat(2, 1fr); gap: var(--space-8); }
  .about-page .about-alt__block:nth-child(even) { margin-top: var(--space-8); }
}
.about-page .about-alt__block h2 {
  font-size: var(--text-md);
  margin-bottom: var(--space-3);
}
.about-page .about-alt__block p { color: var(--ink-100); }

/* VISUAL POLISH IMPLEMENTATION / TIER A — the wordless "world passage"
   between the brand statement and the narrative sections (see
   worldPassage()'s own comment in components.mjs). Deliberately reuses
   the exact three motion techniques Game Detail's family system uses
   this same phase (components.css's [data-family] rules): a SHIFT-style
   gradient-position reveal for the kinetic streaks, a GLOW appear-and-
   hold for the monumental columns, and a slow AMBIENT drift for the
   atmospheric glow — so the same visual grammar recurs across the site
   without either page naming it. */
/* OVERNIGHT MASTER PRODUCTION-READINESS PASS (§9): the Product Owner's
   review found the passage read "too much like a self-contained UI
   banner" — a rounded rectangular card on desktop that collapsed to a
   tiny decorative strip on mobile, losing its purpose. Three changes
   address that without inventing a new component system:
   1. More surrounding negative space (padding-block raised one step)
      so the passage feels like an interval the page moves through
      rather than a tightly wedged intermission between two blocks of
      copy.
   2. The generic `.section + .section` hairline is suppressed on both
      sides of `.about-passage` (same technique Game Detail's family
      system already uses for `#world[data-family]` in components.css)
      so no ruled line boxes the passage off from the sections above and
      below — it bleeds into them instead.
   3. The frame itself no longer clips to a bordered, rounded rectangle
      (see the mask-fade rule below) and now ships two art-directed
      compositions — wide/tall — toggled by media query, rather than one
      wide scene squeezed into every width. */
.about-page.about-passage { padding-block: var(--space-9); border-top: none; }
.about-passage + .section { border-top: none; }
.world-passage-frame {
  position: relative;
  overflow: hidden;
  /* Soft top/bottom fade instead of a hard rectangular clip — the scene
     dissolves into the surrounding void rather than looking like a
     photo dropped in a box. Sides stay unfaded so the composition still
     reads as reaching toward the full width of the page. */
  mask-image: linear-gradient(to bottom, transparent, black 18%, black 82%, transparent);
  -webkit-mask-image: linear-gradient(to bottom, transparent, black 18%, black 82%, transparent);
}
.world-passage-frame svg { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Wide (landscape, 10:3) composition: legible at tablet/desktop widths
   where its full horizontal sweep has room to read. Hidden below the
   mobile breakpoint in favor of the portrait composition. */
.world-passage-frame--wide { aspect-ratio: 10/3; }
/* Tall (portrait) composition: hidden above the mobile breakpoint. See
   worldPassageVertical()'s own comment in components.mjs for why this
   exists as its own authored scene rather than a cropped reflow of the
   wide one. 4/5 keeps it a deliberate, legible passage — not a thumbnail
   strip — without inflating it into a second hero. */
.world-passage-frame--tall { display: none; aspect-ratio: 4/5; }
@media (max-width: 640px) {
  .world-passage-frame--wide { display: none; }
  .world-passage-frame--tall { display: block; }
}

.wp-mote { fill: var(--starlight); opacity: 0.7; }

/* KINETIC zone — streaks are visible at rest (progressive enhancement);
   .js-motion shifts their gradient position once on reveal, matching
   the family-seam's own kinetic treatment. Never a continuous animation. */
.wp-streak { opacity: 0.85; }
.js-motion .world-passage-frame[data-reveal] .wp-streak {
  transition: opacity var(--dur-reveal) var(--ease-out);
  opacity: 0.4;
}
.js-motion .world-passage-frame[data-reveal].is-visible .wp-streak {
  opacity: 0.85;
}

/* MONUMENTAL zone — static/near-static columns; GLOW appears once on
   reveal and holds (never loops), matching the family-seam's own
   monumental treatment. */
.wp-column {
  fill: color-mix(in srgb, var(--void-100) 60%, var(--spectral-violet) 10%);
  stroke: color-mix(in srgb, var(--electric-blue) 40%, transparent);
  stroke-width: 1;
}
.js-motion .world-passage-frame[data-reveal] .wp-column {
  filter: none;
  transition: filter var(--dur-reveal) var(--ease-glow);
}
.js-motion .world-passage-frame[data-reveal].is-visible .wp-column {
  filter: drop-shadow(0 0 10px var(--glow-electric));
}

/* ATMOSPHERIC zone — soft horizon waves, static; the glow blob alone
   gets the slow (--dur-ambient) AMBIENT opacity drift, ~8% amplitude —
   gated by .js-motion, which main.js never adds when prefers-reduced-
   motion matches, so this is fully excluded (not merely slowed) under
   reduced motion, matching the family-seam's own atmospheric treatment. */
.wp-wave { fill: none; stroke: color-mix(in srgb, var(--spectral-cyan) 35%, transparent); stroke-width: 1.5; }
.js-motion .wp-drift {
  animation: world-passage-drift var(--dur-ambient) var(--ease-glow) infinite alternate;
}
@keyframes world-passage-drift {
  from { opacity: 0.82; }
  to { opacity: 1; }
}

/* =========================================================
   Create a Game With Us — forming world
   ========================================================= */
.fw-core { opacity: 0.9; }
.fw-ring { fill: none; stroke-width: 1; }
.fw-ring-a { stroke: color-mix(in srgb, var(--electric-blue) 55%, transparent); }
.fw-ring-b { stroke: color-mix(in srgb, var(--spectral-violet) 40%, transparent); }
.fw-frag-a { fill: var(--starlight); }
.fw-frag-b { fill: var(--electric-blue); }
.fw-frag-c { fill: var(--spectral-cyan); }
.js-motion .fw-core { animation: fw-pulse calc(var(--dur-ambient) * 0.5) var(--ease-glow) infinite alternate; }
.js-motion .fw-ring-a { animation: fw-spin var(--dur-ambient) linear infinite; transform-origin: 400px 380px; }
.js-motion .fw-ring-b { animation: fw-spin calc(var(--dur-ambient) * 1.3) linear infinite reverse; transform-origin: 400px 380px; }
.js-motion .fw-fragments circle { animation: fw-drift var(--dur-ambient) var(--ease-glow) infinite alternate; }
@keyframes fw-pulse { from { opacity: 0.7; transform: scale(1); } to { opacity: 1; transform: scale(1.06); } }
@keyframes fw-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes fw-drift { from { transform: translateY(0); opacity: 0.6; } to { transform: translateY(-8px); opacity: 1; } }

.create-page .form-panel {
  background: color-mix(in srgb, var(--void-100) 60%, transparent);
  border: var(--border-hair);
  padding: var(--space-7);
}
@media (max-width: 640px) {
  .create-page .form-panel { padding: var(--space-5); }
}

.create-page__unavailable-heading { font-size: var(--text-xl); }
/* v0.11.2 TYPOGRAPHY QA CORRECTION: this heading previously set
   font-size:var(--text-xl) as an inline style (a FIXED 40px value —
   --text-xl has no fluid/responsive reduction, unlike --text-2xl/-3xl).
   German's unavailableHeading string
   (src/locales/de/createGame.mjs) contains "Online-Einreichungen" — a
   hyphenated compound word that DOES wrap correctly at its hyphen (no
   mid-word split), but the resulting second half, "Einreichungen"
   alone, measures ~323px at 40px font-size — wider than this heading's
   ~280px available column at the 320px viewport, causing a real,
   measured ~43px overflow (confirmed via canvas text measurement and
   scrollWidth/clientWidth comparison; 375px+ already fits, since the
   crossover point is ~363px of viewport width). FIX: moved the
   font-size out of an inline style (so a media-query override can
   actually apply) and added a narrow, tightly-scoped reduction only
   below 340px — comfortably covering the failing 320px case with
   margin, without touching this heading's appearance at 375px+ or any
   other page's --text-xl usage (h3's sitewide default, and other
   inline uses of --text-xl, were unaffected in the full sweep and are
   left untouched — this is a single-instance fix for a single-instance
   defect, not a token-level change). */
@media (max-width: 340px) {
  .create-page__unavailable-heading { font-size: 2rem; }
}

/* =========================================================
   Contact — signal / planetary horizon
   ========================================================= */
.sh-horizon { fill: none; stroke: color-mix(in srgb, var(--electric-blue) 55%, transparent); stroke-width: 1.5; filter: drop-shadow(0 0 6px var(--glow-electric)); }
.sh-glow { opacity: 0.6; }
.sh-wave { fill: none; stroke-width: 1; }
.sh-wave-a { stroke: color-mix(in srgb, var(--spectral-cyan) 45%, transparent); }
.sh-wave-b { stroke: color-mix(in srgb, var(--spectral-violet) 35%, transparent); }
.sh-stars { fill: var(--starlight); }
.js-motion .sh-wave-a { animation: sh-rise var(--dur-ambient) var(--ease-glow) infinite alternate; }
.js-motion .sh-wave-b { animation: sh-rise calc(var(--dur-ambient) * 1.2) var(--ease-glow) infinite alternate-reverse; }
@keyframes sh-rise { from { opacity: 0.4; transform: translateY(0); } to { opacity: 0.9; transform: translateY(-10px); } }

.contact-page .contact-email-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--ink-000);
  padding: var(--space-3) 0;
  border-bottom: 1px solid color-mix(in srgb, var(--electric-blue) 40%, transparent);
  transition: color var(--dur-base) var(--ease-glow), border-color var(--dur-base) var(--ease-glow);
}
/* OVERNIGHT MASTER PRODUCTION-READINESS PASS (§11): real, measured
   horizontal overflow found on /contact/ below ~411px — the email
   address is one unbroken token (no space/hyphen for `white-space:
   normal` to wrap at), so at --text-lg's fixed 28px (this token is not
   a fluid clamp — see tokens.css) its intrinsic width (~371px,
   independent of viewport) simply exceeds the available column
   (viewport minus the 20px+20px shell gutter) once the viewport drops
   below ~411px — confirmed via getBoundingClientRect() at 320/340/360/
   375/390px, all overflowing by 16-71px, worst at 320px. Per this
   project's established pattern for a long unbreakable token (see the
   /world-hook German-compound-word correction elsewhere in this
   codebase), the fix is a scoped, measured font-size reduction — not
   overflow-wrap/word-break, which this project's production standards
   keep at zero active visitor-facing usage. 420px covers the measured
   411px crossover with margin; 1.2rem (19.2px) was verified via
   rendered-width probing to fit the 320px worst case with ~11px of
   headroom to spare. Only this element is affected — --text-lg itself
   is untouched, so no other usage sitewide changes. */
@media (max-width: 420px) {
  .contact-page .contact-email-link { font-size: 1.2rem; }
}
.contact-page .contact-email-link:hover {
  color: var(--electric-blue);
  border-color: var(--electric-blue);
}

/* =========================================================
   Careers — creative field
   ========================================================= */
.cf-links { stroke: color-mix(in srgb, var(--electric-blue) 25%, transparent); stroke-width: 1; }
.cf-node-a { fill: var(--electric-blue); filter: drop-shadow(0 0 5px var(--glow-electric)); }
.cf-node-b { fill: var(--spectral-cyan); filter: drop-shadow(0 0 6px var(--glow-cyan)); }
.cf-node-c { fill: var(--spectral-violet); filter: drop-shadow(0 0 5px var(--glow-violet)); }
.js-motion .cf-nodes circle { animation: cf-glow var(--dur-ambient) var(--ease-glow) infinite alternate; }
@keyframes cf-glow { from { opacity: 0.75; } to { opacity: 1; } }

/* Careers identity strip: brief explicitly warns against solving visual
   design with more UI ("avoid unnecessary... pills") — so this is large
   typography and negative space, not a row of bordered pill badges. Each
   label is a big display word separated by a hairline rule; only color
   moves on hover, no background/border/radius chrome added. */
.careers-page .identity-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-5);
  margin-top: var(--space-8);
  padding-top: var(--space-7);
  border-top: var(--border-hair);
}
.careers-page .identity-chip {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.1rem, 0.8rem + 1.4vw, var(--text-lg));
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--ink-100);
  transition: color var(--dur-base) var(--ease-glow);
}
.careers-page .identity-chip:hover {
  color: var(--electric-blue);
}
.careers-page .identity-chip:not(:last-child)::after {
  content: "/";
  display: inline-block;
  margin-left: var(--space-5);
  color: var(--void-200);
  font-weight: 400;
}

/* VISUAL POLISH IMPLEMENTATION / TIER A — intentional no-open-roles
   status treatment. A single restrained horizontal strip: one small idle
   status dot (reusing .panel__dot--idle's own muted --ink-200 language,
   not a new color) plus one short mono status line — deliberately quiet,
   with no border/background/pill chrome, matching the identity row's own
   "large typography and negative space, not UI chrome" instruction. */
.careers-page .status-strip {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-6);
}
.careers-page .status-strip__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ink-200);
  flex: none;
}
.careers-page .status-strip__label {
  color: var(--ink-200);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-widest);
}

/* =========================================================
   FAQ — knowledge structure (restrained)
   ========================================================= */
.ks-grid { stroke: color-mix(in srgb, var(--void-200) 90%, var(--electric-blue) 10%); stroke-width: 1; }
.ks-node { fill: var(--spectral-cyan); filter: drop-shadow(0 0 4px var(--glow-cyan)); }
/* OVERNIGHT MASTER PRODUCTION-READINESS PASS (§12): this previously ran
   an unconditional `infinite alternate` opacity blink (0.5<->1, a 100%
   relative swing — several times larger than this site's other AMBIENT
   motion, e.g. the world-passage drift's ~8-18% amplitude) for as long
   as the FAQ page stayed open. Re-classified as GLOW rather than
   AMBIENT: the nodes now appear dim, brighten once when the knowledge-
   structure graphic scrolls into view (reusing the existing sitewide
   [data-reveal] IntersectionObserver — the `data-reveal` attribute was
   added to .ks-nodes in components.mjs, zero new JS), and HOLD at full
   glow — matching the same appear-and-hold GLOW language already used
   for Game Detail's MONUMENTAL family-seam. Progressive enhancement:
   base.css's [data-reveal] rule keeps nodes at full opacity/glow with
   no JS or when prefers-reduced-motion is set (.js-motion is never
   added in that case), so nothing is lost without motion. */
.js-motion .ks-nodes[data-reveal] .ks-node {
  opacity: 0.45;
  filter: drop-shadow(0 0 0 transparent);
  transition: opacity var(--dur-reveal) var(--ease-glow), filter var(--dur-reveal) var(--ease-glow);
}
.js-motion .ks-nodes[data-reveal].is-visible .ks-node {
  opacity: 1;
  filter: drop-shadow(0 0 4px var(--glow-cyan));
}

/* =========================================================
   404 — lost transmission
   ========================================================= */
.lt-arc { fill: none; stroke-width: 1; stroke-dasharray: 6 10; }
.lt-arc-a { stroke: color-mix(in srgb, var(--electric-blue) 45%, transparent); }
.lt-arc-b { stroke: color-mix(in srgb, var(--spectral-magenta) 35%, transparent); }
.lt-signal { fill: none; stroke: color-mix(in srgb, var(--spectral-cyan) 50%, transparent); stroke-width: 1.5; filter: drop-shadow(0 0 5px var(--glow-cyan)); }
.lt-stars { fill: var(--starlight); opacity: 0.6; }
.js-motion .lt-arc-a { animation: lt-flicker calc(var(--dur-ambient) * 0.4) steps(2) infinite; }
.js-motion .lt-signal { animation: lt-flicker calc(var(--dur-ambient) * 0.3) steps(3) infinite alternate; }
@keyframes lt-flicker { from { opacity: 0.4; } to { opacity: 0.85; } }
