/* ============================================================
   DIORAMA ENGINE — base.css
   Structural layout + design-token layer.

   T204: TILE GRID BOARD MODE statics (schema 5.19, grid-nav.js
   V1.2). Three additions, all in the grid family. NOTE on (1): the
   elevation carries !important because card-panel.js writes its
   container z INLINE at init — see the rule's own comment.
   (1) body.grid-persistent — the BOARD state class, granted by
       grid-nav alongside body.grid-open when installation.grid.mode
       is "board". It elevates #card-panel from z:50 to z:69, one
       above the grid surface, so a selected POI card comes FORWARD
       over the tiles while the board stays on screen underneath.
       card-panel.js is DELIBERATELY UNTOUCHED by this build and
       never learns the board exists: its z lives here, so the
       elevation belongs here too. Mirrors journey-rail's V1.4
       elevate-over-card mechanism, in the opposite direction.
   (2) .gn-backdrop — the authored veil.image layer, between the
       scrim and the tiles. Renderer-owned child, same contract as
       the existing .gn-tint; the three --grid-backdrop-* properties
       are the levers. Absent image = element never built.
   (3) Centered tile defaults — --gn-label-align now falls back to
       center (schema 5.19 default flip) and thumbnails center
       within their cell box. Affects only the fallback; an authored
       label_align still wins.

   T203: GRID NAV groundwork — #grid-nav static hidden base
   (LOCK-063; interactive surface, so the .gn-visible grant flips
   pointer-events, same as #lobby-wall and unlike the pointer-cold
   entry cover) with the veil carried on the element via
   --grid-veil-* custom properties (renderer-set from
   installation.grid; defaults here are the zero-authoring look),
   plus body.grid-open static chrome defense for the known phantom
   family (hotspots pointer-cold, float arrows + transport
   suppressed; the .mf-utility-btn ?/gear deliberately NOT touched
   — it survives grid per LOCK-067 grammar, and it is the
   affordance that reverses the state). Grid is NOT a nav mode: it
   is an invoked surface that COEXISTS with whatever mode the
   DEC-019 resolver returned, so nothing here suppresses a nav
   renderer — the grid simply sits above it at z:68. Behavior
   arrives with grid-nav.js + guest-panel.js in the same T203
   sequence; these statics are first-paint / version-drift proof
   per LOCK-063.

   T197: DEC-030 Amendment C groundwork — #lobby-wall static
   hidden base (LOCK-063; interactive surface, so the .lw-visible
   grant flips pointer-events, unlike the pointer-cold entry
   cover) with the veil carried on the element via --lobby-veil-*
   custom properties (renderer-set from installation.lobby_rest;
   defaults here are the zero-authoring look), plus body.lobby-rest
   static defense rules for the known phantom family (hotspot
   pointer-cold, float arrows + transport suppressed; the
   .mf-utility-btn ?/gear deliberately NOT touched — it survives
   lobby rest per LOCK-067 grammar). Behavior arrives with
   lobby-wall.js + app.js in the same Amendment C sequence; these
   statics are first-paint / version-drift proof per LOCK-063.

   T174: mobile corner-triangle suppression scoped to non-lobby.
   The .gp-triangle display:none rule for phone/tablet now applies
   only when body lacks .hold-screen-active — the genuine desktop
   triangles (SVG fill/stroke/shadow, manifest presets, z:210,
   wired to the same help/guest panels) serve as the LOBBY ?/gear
   affordance on mobile, visible against any background. Post
   hold:dismiss the rule re-applies and mobile-footer's top-corner
   round buttons take over (V2.7). guest-panel.js untouched.
   KNOWN EDGE (pre-existing family): _restoreHoldScreen() does not
   re-add body.hold-screen-active, so ALL hold-scoped suppressions
   — including this one — do not re-apply on license-engagement
   restore. Registered as minor debt at T174.

   T180: mobile-footer float prev/next arrows (.mf-float-arrow)
   statically suppressed while body.hold-screen-active — mirror of
   the T179 .mf-utility-btn rule, same root cause (MobileFooter
   init-order _holdActive lie, DEBT-038), same LOCK-063 static
   pattern. Trigger: mobile-footer V2.10 (Fork A) decoupled float
   visibility from bar visibility, giving floats the same predicate
   as the utility buttons — and thereby the same lobby phantom
   (found on device T180). Third static shim for the DEBT-038 flag
   lie (coverflow T176, utility T179, floats T180); all three
   remain correct defense-in-depth after the structural cure
   (body class static in index.html, hold-screen V2.3 rider).
   Same T174 KNOWN-EDGE family: does not re-apply on license-
   engagement restore until DEBT-038 lands. mobile-footer.js
   untouched.

   T179: mobile-footer utility ?/gear (.mf-utility-btn) statically
   suppressed while body.hold-screen-active — the inverse of the
   T174 triangle rule. Root cause: MobileFooter.init() (app.js
   L1877) runs before HoldScreen.init() adds the body class, so
   V2.9's DOM-truth read sees no class, flips _holdActive false,
   and grants .utility-visible during the lobby (phantom top-corner
   ?/gear over the hold screen, found on device T179). Static rule
   here is init-order-proof per LOCK-063. Same T174 KNOWN-EDGE
   family: does not re-apply on license-engagement restore until
   DEBT-038 lands (hold-screen V2.3). mobile-footer.js untouched.

   T160 (P1): added the --diorama-* design-token layer below
   (default theme + .diorama-assistive high-contrast theme).
   Module colors migrate onto these tokens progressively in
   P1 build-steps 2+. Until a module consumes var(--diorama-*),
   these tokens are INERT — this delivery is purely additive and
   is zero visual change. Default token values are the current
   card-panel.js palette, extracted verbatim.

   T163 (P1(B) — batched triplet/bridge step): added --diorama-*-rgb
   raw-triplet tokens (bg / text / accent) alongside the existing
   full-value tokens, plus two assistive opacity-floor override
   rules. These exist because three surfaces carry AUTHORED opacity
   (per-POI card background_opacity, per-block ticker label opacity)
   that must be preserved exactly at rest — the full-value tokens
   (fixed alpha) can't be swapped in directly without silently
   overwriting authored values. Mechanism, decided fresh from code
   this thread:
     REST STATE   → consumer composes rgba(var(--diorama-*-rgb),
                     authoredAlpha) in JS/CSS. Triplet matches the
                     literal channel exactly (10,10,14 / 255,255,255
                     / 232,236,9) — zero visual change.
     ASSISTIVE     → composition is NOT reused. A separate descendant
                     override (below, !important) replaces the whole
                     property with the flat opaque/high-contrast
                     full-value token. Composing authored alpha under
                     .diorama-assistive would leave surfaces
                     translucent, defeating the WCAG-opaque intent
                     this file already documents below. The two
                     mechanisms are not alternatives — the floor
                     override is required regardless of the triplet
                     layer.
   Consumers migrated this step: card-panel.js (card bg, chevron
   triplet — accent hue only, no floor needed since hue is identical
   in both themes), narrative-ticker.js (label color). Still INERT —
   nothing sets .diorama-assistive yet (P3).
   ============================================================ */

/* ------------------------------------------------------------
   LOCAL FONTS — Kanit (SIL OFL)
   Bundled at public/fonts/kanit/. Loaded here so font is
   available before any JS module runs. Covers all weights
   used by card-panel.js typography system (300–700).
   Italic variants are on disk but not declared — add if needed.
   ------------------------------------------------------------ */
@font-face { font-family: 'Kanit'; font-style: normal; font-weight: 300; font-display: swap; src: url('fonts/kanit/Kanit-Light.ttf') format('truetype'); }
@font-face { font-family: 'Kanit'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/kanit/Kanit-Regular.ttf') format('truetype'); }
@font-face { font-family: 'Kanit'; font-style: normal; font-weight: 500; font-display: swap; src: url('fonts/kanit/Kanit-Medium.ttf') format('truetype'); }
@font-face { font-family: 'Kanit'; font-style: normal; font-weight: 600; font-display: swap; src: url('fonts/kanit/Kanit-SemiBold.ttf') format('truetype'); }
@font-face { font-family: 'Kanit'; font-style: normal; font-weight: 700; font-display: swap; src: url('fonts/kanit/Kanit-Bold.ttf') format('truetype'); }

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

/* ------------------------------------------------------------
   DESIGN TOKEN LAYER (T160 — P1 / Component B)
   -----------------------------------------------------------
   Role-based --diorama-* tokens. Two value sets, one vocabulary:

     :root                 DEFAULT theme — current appearance,
                           values extracted verbatim from the
                           card-panel.js _injectStyles() palette.
                           Migrating a module onto these tokens is
                           a zero-visual-change operation.

     .diorama-assistive    HIGH-CONTRAST theme — authored to WCAG
                           1.4.6 (AAA, 7:1) with OPAQUE surfaces.
                           Opacity-based card translucency is
                           removed here because a translucent card
                           has indeterminate contrast (its ratio
                           depends on the panorama behind it).

   INVOCATION: the .diorama-assistive class is toggled on #app by
   the assistive-mode control (P3 — not built here). The prefers-
   contrast / prefers-reduced-motion auto-detect pairing is also
   P3. In P1 this class is defined but nothing sets it.

   CANVAS NOTE: CSS custom properties cascade only to CSS-styled
   DOM. Canvas draws (hotspot markers, canvas text) cannot var()
   — those read tokens via getComputedStyle in JS and are handled
   in P4, not here.
   ------------------------------------------------------------ */
:root {
  /* Surfaces */
  --diorama-bg:                    rgba(10, 10, 14, 1);
  --diorama-card-bg:               rgba(10, 10, 14, 0.75);
  --diorama-card-radius:           10px;

  /* Text */
  --diorama-text:                  rgba(255, 255, 255, 0.92);
  --diorama-text-muted:            rgba(255, 255, 255, 0.60);
  --diorama-text-on-accent:        rgba(10, 10, 14, 1);

  /* Accent (chartreuse #E8EC09) */
  --diorama-accent:                rgba(232, 236, 9, 0.85);

  /* Structure */
  --diorama-border:                rgba(255, 255, 255, 0.15);
  --diorama-focus-ring:            rgba(255, 255, 255, 0.90);

  /* Scrollbar (standing visible-scrollbar rule) */
  --diorama-scrollbar-thumb:       rgba(255, 255, 255, 0.28);
  --diorama-scrollbar-thumb-hover: rgba(255, 255, 255, 0.42);

  /* Raw-triplet tokens (T163) — for consumers that compose their own
     authored alpha via rgba(var(--diorama-*-rgb), authoredAlpha).
     Channels match the full-value tokens above exactly. */
  --diorama-bg-rgb:                10, 10, 14;
  --diorama-text-rgb:              255, 255, 255;
  --diorama-accent-rgb:            232, 236, 9;
}

.diorama-assistive {
  /* Surfaces — OPAQUE (removes translucency indeterminacy) */
  --diorama-bg:                    #000000;
  --diorama-card-bg:               #000000;
  --diorama-card-radius:           10px;

  /* Text — white on black = 21:1; muted #d4d4d4 ≈ 14:1 */
  --diorama-text:                  #ffffff;
  --diorama-text-muted:            #d4d4d4;
  --diorama-text-on-accent:        #000000;

  /* Accent — chartreuse, full opacity (≈ 16:1 on black) */
  --diorama-accent:                rgba(232, 236, 9, 1);

  /* Structure — border ≈ 7:1 (well above 3:1 non-text) */
  --diorama-border:                rgba(255, 255, 255, 0.60);
  --diorama-focus-ring:            #e8ec09;

  /* Scrollbar — raised visibility */
  --diorama-scrollbar-thumb:       rgba(255, 255, 255, 0.70);
  --diorama-scrollbar-thumb-hover: rgba(255, 255, 255, 0.90);

  /* Raw-triplet tokens (T163) — accent hue is unchanged between
     themes, so this is the only one a composed consumer (chevrons)
     will actually see change here (nothing — same values, by design).
     bg/text triplets are defined for vocabulary completeness; the
     surfaces that use them (card bg, ticker label) do NOT compose
     under this theme — see the opacity-floor overrides below. */
  --diorama-bg-rgb:                0, 0, 0;
  --diorama-text-rgb:              255, 255, 255;
  --diorama-accent-rgb:            232, 236, 9;
}

/* ------------------------------------------------------------
   ASSISTIVE OPACITY-FLOOR OVERRIDES (T163 — P1(B))
   Authored per-card / per-block opacity is composed at rest via
   the -rgb triplets above (in card-panel.js / narrative-ticker.js).
   Under .diorama-assistive that composition is bypassed: these
   rules replace the whole property with the token's own opaque /
   high-contrast value, !important. A class-scoped !important
   author rule overrides a non-important inline value on the same
   element regardless of load order — this holds even though the
   consuming modules set these properties via inline JS.
   Selector is a plain descendant selector, not #app-anchored, so
   it keeps working once P3 lands .diorama-assistive on <html>/
   <body> per DEC-028 Component C (not yet built — nothing sets
   the class today; these rules are inert until then).
   ------------------------------------------------------------ */
.diorama-assistive .diorama-card {
  background: var(--diorama-card-bg) !important;
}
.diorama-assistive #narrative-ticker-label {
  color: var(--diorama-text) !important;
}

/* ------------------------------------------------------------
   ROOT
   ------------------------------------------------------------ */
html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000;
  font-family: sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* ------------------------------------------------------------
   APP CONTAINER
   All layers are positioned absolutely within this.
   ------------------------------------------------------------ */
#app {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

/* ------------------------------------------------------------
   VIDEO LAYER — z-index: 0
   Full-bleed. object-fit: cover preserves aspect ratio.
   src set at runtime by video-controller.js.
   ------------------------------------------------------------ */
#main-video {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
  display: block;
}

/* ------------------------------------------------------------
   NIGHT OVERLAY — z-index: 5
   Full-bleed CSS gradient div. Above panorama, below alive-layer.
   Managed by night-layer.js. Subscribes to atmo:update.
   Background set inline by JS — starts transparent.
   T112: new — atmosphere consumer (Phase 4).
   ------------------------------------------------------------ */
#night-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 5;
  pointer-events: none;
  background: transparent;
}

/* ------------------------------------------------------------
   SCENE CONTAINER — T168 (stage gesture build, pinch V2 §6)
   The pinch-zoom transform target. Wraps the scene scale group:
   main-video, main-image, night-overlay, alive-layer,
   caustic-layer, fx-layer, lightning-flash, hotspot-layer.
   Chrome never scales — everything else lives outside.

   z-index: 10 — below the mist curtain (30). T168 re-stack note:
   hotspot markers (z:40 INSIDE this container) previously drew
   above the mist; they now sit under the curtain during
   transitions, which is the intended reading (mist covers the
   scene, markers included).

   transform is written ONLY by stage-interaction.js
   (translate + scale, origin 0 0). A CSS transform creates a
   containing block — position:fixed chrome must never be placed
   inside this element (the mobile-footer backdrop-filter lesson).

   touch-action: none — stage gestures are engine-owned; browser
   pan/pinch must not compete on stage terrain.
   ------------------------------------------------------------ */
#scene-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 10;
  will-change: transform;
  touch-action: none;
}

/* ------------------------------------------------------------
   CANVAS LAYERS
   All transparent. All full-bleed over video.
   pointer-events: none by default — hotspot-layer overrides.
   JS is responsible for setting canvas width/height in pixels
   to match actual rendered dimensions (devicePixelRatio aware).
   T168: scene-group members are children of #scene-container;
   their z-indexes order them WITHIN the container only.
   ------------------------------------------------------------ */
canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  display: block;
  background: transparent;
}

#alive-layer   { z-index: 10; }

/* ------------------------------------------------------------
   CAUSTIC LAYER — z-index: 15
   Full-bleed container for animated CSS caustic light bands.
   Above alive-layer, below fx-layer canvas.
   Managed by fx-layer.js. Band children created at runtime.
   Clipped to caustic zone via clipPath set inline by JS.
   T112: new — atmosphere consumer (Phase 4).
   ------------------------------------------------------------ */
#caustic-layer {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 15;
  pointer-events: none;
  overflow: hidden;
  opacity: 0;
}

#caustic-layer .caustic-band {
  position: absolute;
  left: -30%;
  width: 160%;
  pointer-events: none;
}

#fx-layer      { z-index: 20; }

/* ------------------------------------------------------------
   LIGHTNING FLASH — z-index: 25
   Full-bleed white flash overlay for lightning strikes.
   Above fx-layer canvas, below mist-layer.
   Managed by fx-layer.js. Opacity animated inline by JS.
   T112: new — atmosphere consumer (Phase 4).
   ------------------------------------------------------------ */
#lightning-flash {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 25;
  pointer-events: none;
}

/* Mist — z:30, VIEWPORT curtain outside the scene container (T168).
   Above the container (z:10), so it covers the whole scene — hotspot
   markers included — during transitions. */
#mist-layer    { z-index: 30; }

/* Hotspot layer — z:40 INSIDE #scene-container (T168): orders it at
   the top of the scene group; it rides the pinch transform. */
#hotspot-layer {
  z-index: 40;
  pointer-events: auto; /* receives touch/click for POI interaction */
  touch-action: none;   /* T168 — stage gestures are engine-owned */
}

/* ------------------------------------------------------------
   STAGE BLUR OVERLAY — z-index: 42
   Full-bleed backdrop-filter layer. Inactive by default.
   blur-stage-active class applied by card-panel.js per-POI.
   pointer-events: none always — interaction handled by layers above.
   ------------------------------------------------------------ */
#stage-blur-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 42;
  pointer-events: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background: transparent;
}

#stage-blur-overlay.blur-stage-active {
  backdrop-filter: blur(var(--stage-blur-radius, 8px));
  -webkit-backdrop-filter: blur(var(--stage-blur-radius, 8px));
}

/* ------------------------------------------------------------
   DOM OVERLAY BASE CLASS
   All DOM overlay panels share this base.
   Individual panels set their own z-index.
   ------------------------------------------------------------ */
.overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none; /* children opt in via pointer-events: auto */
}

#narrative-ticker { z-index: 45; }
#card-panel       { z-index: 50; }
#dial-controller  {
  z-index: 60;
  /* Required for CSS rotateX perspective tilt applied by dial-controller.js.
     preserve-3d ensures the canvas child renders correctly in 3D space
     rather than being flattened back to the stacking context. */
  transform-style: preserve-3d;
  /* T168 — the dial canvas is full-viewport stage terrain on desktop;
     stage gestures (scrub/pinch) are engine-owned there. */
  touch-action: none;
}
#admin-panel      { z-index: 70; }
#authoring-mode   { z-index: 80; }

/* ------------------------------------------------------------
   T176 — COVERFLOW STATIC HIDDEN BASE (phantom-strip fix)
   The container's hidden state previously lived ONLY in the
   module-injected stylesheet (coverflow-nav-styles), which does
   not exist until Phase-2 init and is removed/reinjected at
   runtime (viewport:class, manifest:loaded). The T176 in-engine
   trap caught the container at computed opacity 1 with only its
   static "overlay" class — the injected rule absent at paint
   time. Hidden-by-default now lives HERE, present from page
   zero, immune to injection timing and module version drift.
   The module's .cfn-visible show path is unchanged; its injected
   sheet still owns the transition and all deck styling.
   ------------------------------------------------------------ */
#coverflow-nav {
  opacity: 0;
  pointer-events: none;
}
#coverflow-nav.cfn-visible {
  opacity: 1;
  pointer-events: auto;
}

/* ------------------------------------------------------------
   T181 — ENTRY COVER (DEC-031) — STATIC HIDDEN BASE
   Pre-manifest journey-entry veil. Hidden state lives HERE from
   page zero per LOCK-063 (boot-suppressed overlay), and
   pointer-events stays none in ALL states — the veil is never
   interactive (mist-curtain precedent; entirely outside the T180
   hidden-chrome input-theft class). entry-cover.js toggles
   .entry-cover-active and sets transition-duration inline per
   phase (rise/fall profile levers; reduced-motion aware).
   z-index: 90 — above mist (30) and all nav/UI chrome, below the
   hold screen (200) so the hold fade reveals the risen veil, and
   below gate (400) / license (500), which are pre/post-entry
   surfaces. Visual: mist-white radial veil (mist rhyme — handoff
   continuity with an authored lobby_exit needs no coupling).
   --entry-cover-color is the optional profile tint lever.
   ------------------------------------------------------------ */
#entry-cover {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 90;
  opacity: 0;
  pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% 42%,
      rgba(255, 255, 255, 1) 0%,
      rgba(244, 246, 248, 1) 55%,
      rgba(226, 230, 234, 1) 100%),
    var(--entry-cover-color, #eef0f2);
  transition-property: opacity;
  transition-timing-function: ease;
  transition-duration: 400ms; /* overwritten inline per phase */
  will-change: opacity;
}
#entry-cover.entry-cover-active {
  opacity: 1;
}

/* ------------------------------------------------------------
   T197 — LOBBY WALL (DEC-030 AMENDMENT C) — STATIC HIDDEN BASE
   The lobby REST-STATE surface: veil over the paused home-journey
   frame + centered play + poster wall. Hidden state lives HERE
   from page zero per LOCK-063 (boot-suppressed overlay). UNLIKE
   the entry cover this surface is INTERACTIVE — the .lw-visible
   grant flips pointer-events (module lobby-wall.js is the only
   granter). z-index: 85 — above every journey chrome layer
   (transport 65, dial 60, cards 50) so rest-state content owns
   the viewport; below the entry cover (90) so a fresh DEC-030
   entry veils it on play; far below the mist return boost
   (inline 300), so the Amendment B cover reveals it at settle.
   VEIL: carried on this element as a scrim; the three
   --lobby-veil-* properties are the L2 authoring levers
   (installation.lobby_rest.veil — lobby-wall.js sets them from
   the manifest; absent = these zero-authoring defaults, tuned
   to rhyme with the entry cover's read without copying it).
   backdrop-filter blur is 0 by default: blur is opt-in.
   ------------------------------------------------------------ */
#lobby-wall {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 85;
  opacity: 0;
  pointer-events: none;
  background: rgba(var(--diorama-bg-rgb, 10, 10, 14),
                   var(--lobby-veil-opacity, 0.45));
  backdrop-filter: blur(var(--lobby-veil-blur, 0px));
  -webkit-backdrop-filter: blur(var(--lobby-veil-blur, 0px));
  transition-property: opacity;
  transition-timing-function: ease;
  transition-duration: 400ms; /* overwritten inline by lobby-wall.js
                                 (reduced-motion aware) */
  will-change: opacity;
}
#lobby-wall.lw-visible {
  opacity: 1;
  pointer-events: auto;
}
/* Optional authored tint rides ABOVE the base scrim — the
   renderer sets --lobby-veil-tint (any CSS color, default
   transparent) on a dedicated child it owns; no rule needed
   here beyond the variable convention being on record. */

/* ── body.lobby-rest — static chrome defense (T197) ──────────
   Mirrors the hold-screen-active family (T174/T179/T180): the
   AUTHORITATIVE suppression is mode-authority intent in app.js
   (LOCK-065 cl.1, Amendment C delivery 3); these statics are
   defense-in-depth against the init-order / stale-module phantom
   class, and they cure the visitor-visible symptoms (live
   hotspots, arrows, transport) even before delivery 3 lands.
   .mf-utility-btn is DELIBERATELY absent from these rules — the
   ?/gear survives lobby rest (LOCK-067 grammar: the gear is not
   nav chrome and must remain to reverse any state). */
body.lobby-rest #hotspot-layer {
  pointer-events: none !important;
}
body.lobby-rest .mf-float-arrow {
  opacity: 0 !important;
  pointer-events: none !important;
}
body.lobby-rest #transport-bar {
  opacity: 0 !important;
  pointer-events: none !important;
}

/* ------------------------------------------------------------
   T203 — GRID NAV — STATIC HIDDEN BASE
   The thumbnail BROWSE surface: veil over the running journey +
   a scrolling grid of POI cards. Hidden state lives HERE from
   page zero per LOCK-063 (boot-suppressed overlay). Like
   #lobby-wall and unlike #entry-cover this surface is
   INTERACTIVE — the .gn-visible grant flips pointer-events
   (module grid-nav.js is the only granter).

   NOT A NAV MODE. Grid is an INVOKED SURFACE that coexists with
   whatever the DEC-019 V2 resolver returned — dial, coverflow,
   filmstrip, chiclets or none. Nothing in this file suppresses a
   nav renderer on grid's account, and grid never appears in
   NAV_MODES or the fitness walk. Invoked from the guest-panel
   gear (one affordance on every tier: bottom corner triangles on
   desktop, top corner round buttons on phone/tablet), dismissed
   by its own close, Escape, or any nav:goto.

   z-index: 68 — above every JOURNEY chrome layer (transport 65,
   dial 60, cards 50) so the browse surface owns the viewport;
   BELOW admin (70), authoring (80), lobby-wall (85), entry cover
   (90) and hold (200). Grid is journey chrome, not a system
   surface: it must never cover an author's panel, a lobby rest
   state, or an entry veil.

   VEIL: carried on this element as a scrim; the three
   --grid-veil-* properties are the authoring levers
   (installation.grid.veil — grid-nav.js sets them from the
   manifest; absent = these zero-authoring defaults, tuned to
   rhyme with the lobby wall's read since the two are siblings in
   grammar). backdrop-filter blur is 0 by default: blur is opt-in.
   ------------------------------------------------------------ */
/* T204 — centered tile defaults. --gn-label-align's fallback
   follows the schema 5.19 default flip (left -> center); the
   renderer still overrides it from an authored label_align. The
   thumbnail centering rule lives in grid-nav's injected sheet
   with the rest of the cell box; only the token default is set
   here, where the other grid tokens live.                        */
#grid-nav {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 68;
  opacity: 0;
  pointer-events: none;
  background: rgba(var(--diorama-bg-rgb, 10, 10, 14),
                   var(--grid-veil-opacity, 0.82));
  backdrop-filter: blur(var(--grid-veil-blur, 0px));
  -webkit-backdrop-filter: blur(var(--grid-veil-blur, 0px));
  transition-property: opacity;
  transition-timing-function: ease;
  transition-duration: 320ms; /* overwritten inline by grid-nav.js
                                 (reduced-motion aware) */
  will-change: opacity;
}
#grid-nav.gn-visible {
  opacity: 1;
  pointer-events: auto;
}
/* Optional authored tint rides ABOVE the base scrim — the
   renderer sets --grid-veil-tint (any CSS color, default
   transparent) on a dedicated child it owns; no rule needed here
   beyond the variable convention being on record. Same contract
   as --lobby-veil-tint. */

/* ── body.grid-open — static chrome defense (T203) ───────────
   Mirrors the body.lobby-rest family directly above. The grid
   veil is opaque enough that stage chrome underneath is not
   VISIBLE, but visible is not the same as unreachable: hotspots,
   float arrows and the transport bar all sit below z:68 and would
   still take taps through any gap the veil does not cover
   (letterboxed stages, safe-area insets). Pointer-cold is the
   defense — the T180 invisible-thumb input-theft class, which is
   half of what LOCK-065 cl.2 exists for.
   .mf-utility-btn is DELIBERATELY absent from these rules — the
   ?/gear survives grid (LOCK-067 grammar: the gear is not nav
   chrome and must remain to reverse any state), and here it is
   load-bearing rather than merely permitted, because the gear is
   the affordance that OPENED the grid.
   Nav renderers are likewise deliberately absent: grid coexists
   with them by design, and they are simply underneath. */
body.grid-open #hotspot-layer {
  pointer-events: none !important;
}
body.grid-open .mf-float-arrow {
  opacity: 0 !important;
  pointer-events: none !important;
}
body.grid-open #transport-bar {
  opacity: 0 !important;
  pointer-events: none !important;
}

/* ── T204 — AUTHORED BACKDROP (installation.grid.veil.image) ──
   Layered ABOVE the element's own scrim and BELOW the tiles, so
   the composite reads: running journey -> veil.opacity scrim ->
   optional tint -> backdrop image -> tiles. This is what lets a
   board look like a table, a felt, a cabinet or a map rather than
   a menu over a dimmed stage.
   The renderer builds .gn-backdrop ONLY when veil.image.src is
   authored, and resolves the path through
   ManifestLoader.contentBase() so it carries the app:// prefix
   under Electron. Nothing here hardcodes a path.
   fit 'tile' switches background-size to auto + repeat for
   material textures; cover/contain use no-repeat. The renderer
   sets the three properties below; these fallbacks are the
   never-authored case and are inert without .gn-backdrop.        */
#grid-nav .gn-backdrop {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: var(--grid-backdrop-opacity, 1);
  background-image: var(--grid-backdrop-image, none);
  background-position: var(--grid-backdrop-position, center);
  background-size: var(--grid-backdrop-size, cover);
  background-repeat: var(--grid-backdrop-repeat, no-repeat);
}

/* ── T204 — BOARD MODE (body.grid-persistent) ────────────────
   Granted by grid-nav.js ONLY under installation.grid.mode
   "board", always alongside body.grid-open — so every rule in the
   chrome-defense block above still applies. A persistent board
   with live hotspots underneath is the LOCK-065 cl.2 input-theft
   class, which is why the defense is NOT relaxed here.
   THE CARD COMES FORWARD. #card-panel is z:50 by the overlay
   table above and the grid surface is z:68; under board mode the
   card must sit ABOVE the tiles it was launched from, so it takes
   69 — one step, not a leap, and still below admin (70),
   authoring (80), lobby-wall (85), entry cover (90) and hold
   (200). Selection is an event WITHIN the surface: the board does
   not close, the tiles stay put, and backing out of the card
   leaves the visitor exactly where they were.
   ONE CARD AT A TIME. This rule elevates the single card-panel
   container; it does not create per-card z-slots. Multiple
   simultaneous cards are DEC-035 (pinned card lifecycle) and are
   deliberately not in play here.
   attract-mode V2.8 tests .grid-open:not(.grid-persistent) — a
   surface that stays open would otherwise block the idle timer
   forever, which is correct for a launcher and wrong for
   furniture.                                                     */
body.grid-persistent #card-panel {
  /* ⚠ !important IS LOAD-BEARING — DO NOT REMOVE. T204 device finding.
     card-panel.js writes this._container.style.zIndex = '62' INLINE at
     init (V3.65 line ~1656). An inline declaration outranks any normal
     author rule no matter how specific, so the first cut of this rule
     lost silently and the card opened BEHIND the tiles. Author
     !important outranks a normal inline declaration, which is what
     makes the elevation actually land. Same family as the T20 locked
     pattern (CSS z-index alone is insufficient against a JS-written
     stacking value) — different mechanism, identical lesson.
     THE INLINE 62, NOT THE TABLE'S 50, IS THE REAL RESTING VALUE of
     #card-panel. The 50 above is dead for this element. */
  z-index: 69 !important;
}

/* ── T204b — THE MOBILE HALF OF THE SAME RULE ────────────────
   DEVICE FINDING (phone + tablet, board mode): the rule above
   CANNOT work on those tiers, and not because of specificity.
   The mobile block further down sets
     html.diorama-phone/-tablet #card-panel { position: static; }
   deliberately, so the card's position:fixed children can escape
   the container. A STATIC ELEMENT IGNORES z-index ENTIRELY — the
   container has no stacking position to raise, so elevating it is
   a no-op and the card stayed under the tiles.
   On those tiers the card element itself is the thing in the
   stack: .diorama-card-mobile is position:fixed at z:65 in
   card-panel's injected sheet, which loses to #grid-nav at 68.
   So the elevation is applied to the CARD, not its container.
   Scoped to body.grid-persistent, so nothing changes for any
   other surface or for launcher mode.
   !important for the same reason as above — it must outrank the
   injected sheet, and card-panel.js is not touched by this build.
   DESKTOP IS UNAFFECTED: there the container is positioned and
   already elevated, and a child at 69 inside a 69 container is
   simply the card's own painting order.                          */
body.grid-persistent .diorama-card,
body.grid-persistent .diorama-card.diorama-card-mobile {
  z-index: 69 !important;
}

/* ------------------------------------------------------------
   UTILITY — HIDDEN STATE
   Applied by JS to suppress panels until ready.
   ------------------------------------------------------------ */
.hidden {
  display: none !important;
  visibility: hidden;
  pointer-events: none;
}

/* ------------------------------------------------------------
   KIOSK HARDENING (Phase 9 — active now as a defensive baseline)
   Prevents text selection and context menus on touch/kiosk displays.
   ------------------------------------------------------------ */
#app {
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* ------------------------------------------------------------
   MOBILE — HALF-SHEET CARD LAYOUT (Phase 3 — T98)
   Viewport-class CSS classes set by app.js _detectViewport().
   phone: diorama-phone   (<768px shortest dimension)
   tablet: diorama-tablet  (768–1023px shortest dimension)
   Both inherit half-sheet behavior. Tablet may be promoted to
   desktop behavior later if needed.
   ------------------------------------------------------------ */

/* Card panel: on mobile, needs to allow fixed children to escape */
html.diorama-phone #card-panel,
html.diorama-tablet #card-panel {
  position: static;
  z-index: 50;
}

/* Hide dial on phone and tablet — replaced by mobile footer filmstrip.
   T125: extended to tablet (was phone-only). Tablet was rendering both
   dial and footer simultaneously — wasted resources, overlapping UI.
   Future: manifest-driven toggle (installation.navigation.mobile_mode)
   will replace this CSS rule for per-deployment control. */
html.diorama-phone #dial-controller,
html.diorama-tablet #dial-controller {
  display: none;
}

/* Hide corner triangles on mobile — absorbed by mobile footer.
   T174: scoped to non-lobby. While body.hold-screen-active is set,
   the desktop triangles show at bottom corners as the mobile lobby
   ?/gear affordance (styling identical to desktop — visible against
   any background). Re-hidden on hold:dismiss. */
html.diorama-phone body:not(.hold-screen-active) .gp-triangle,
html.diorama-tablet body:not(.hold-screen-active) .gp-triangle {
  display: none !important;
}

/* Mobile-footer utility ?/gear — journey-only affordance. T179:
   statically suppressed during the lobby (init-order-proof; see
   header). Triangles serve the lobby; these take over on
   hold:dismiss per V2.7 choreography. */
body.hold-screen-active .mf-utility-btn {
  opacity: 0 !important;
  pointer-events: none !important;
}

/* Mobile-footer float prev/next arrows — journey-only affordance.
   T180: statically suppressed during the lobby, mirror of the
   .mf-utility-btn rule above (same DEBT-038 init-order flag lie,
   same LOCK-063 static pattern; see header). */
body.hold-screen-active .mf-float-arrow {
  opacity: 0 !important;
  pointer-events: none !important;
}

/* Narrative ticker — reposition above footer on mobile */
html.diorama-phone #narrative-ticker,
html.diorama-tablet #narrative-ticker {
  bottom: 56px; /* above mobile footer bar */
}

/* Touch-friendly hotspot hit area on mobile.
   T168: touch-action changed manipulation → none. The stage gesture
   layer (stage-interaction.js) owns pan/pinch on stage terrain;
   'manipulation' would let the browser compete for pinch. */
html.diorama-phone #hotspot-layer,
html.diorama-tablet #hotspot-layer {
  touch-action: none;
}

/* Prevent body bounce / overscroll on iOS */
html.diorama-phone,
html.diorama-phone body,
html.diorama-tablet,
html.diorama-tablet body {
  overscroll-behavior: none;
  touch-action: manipulation;
}

/* ------------------------------------------------------------
   MOBILE — PHASE 6: RESPONSIVE CSS (T99)

   Typography scaling — reduce card and ticker font sizes on
   phone viewports. Tablet inherits phone rules for now; can
   be split later if needed.

   Touch targets — minimum 44px on interactive elements.

   Transport bar — hidden on phone (replaced by mobile footer
   prev/next and filmstrip). Retained on tablet.

   Narrative ticker — repositioned above footer, smaller font,
   reduced letter-spacing for narrower screens.
   ------------------------------------------------------------ */

/* ── Typography scaling ──────────────────────────────────── */

/* Card title: desktop uses manifest value (typically 28-36px).
   On phone, cap at a readable size that doesn't dominate the
   half-sheet / side-sheet card. */
html.diorama-phone .diorama-card .card-block-title {
  font-size: clamp(18px, 4.5vw, 24px) !important;
  letter-spacing: 0.06em;
}
html.diorama-phone .diorama-card .card-block-title p {
  font-size: inherit !important;
}

/* Card body text */
html.diorama-phone .diorama-card .card-block-body {
  font-size: clamp(14px, 3.5vw, 18px) !important;
  line-height: 1.45;
}
html.diorama-phone .diorama-card .card-block-body p {
  font-size: inherit !important;
}

/* Tablet — slightly larger than phone, smaller than desktop */
html.diorama-tablet .diorama-card .card-block-title {
  font-size: clamp(20px, 3.2vw, 28px) !important;
}
html.diorama-tablet .diorama-card .card-block-title p {
  font-size: inherit !important;
}
html.diorama-tablet .diorama-card .card-block-body {
  font-size: clamp(15px, 2.4vw, 20px) !important;
  line-height: 1.45;
}
html.diorama-tablet .diorama-card .card-block-body p {
  font-size: inherit !important;
}

/* ── Narrative ticker — mobile sizing ────────────────────── */

html.diorama-phone #narrative-ticker {
  padding-bottom: 2vh;
}
html.diorama-phone #narrative-ticker-label {
  font-size: clamp(10px, 2.5vw, 14px);
  letter-spacing: 0.12em;
}
html.diorama-tablet #narrative-ticker-label {
  font-size: clamp(11px, 1.6vw, 16px);
  letter-spacing: 0.14em;
}

/* ── Transport bar — hidden on phone ─────────────────────── */

html.diorama-phone #transport-bar {
  display: none !important;
}

/* ── Touch targets — minimum 44px on phone ───────────────── */

html.diorama-phone .mf-corner-btn,
html.diorama-phone .mf-arrow,
html.diorama-phone .mf-float-arrow {
  min-width: 44px;
  min-height: 44px;
}

/* ── Journey rail — static hidden base (LOCK-063) ───────────
   #journey-rail is a boot-suppressed overlay: invisible and
   pointer-cold from page zero, shown only when journey-rail.js
   grants .jr-visible. The suppression lives HERE, not solely in
   the module-injected sheet (journey-rail-styles) — injection
   races first paint, and a stale or absent module version would
   silently remove the suppression (the T176 coverflow phantom
   class). DEC-034 §3a build, T190. */

#journey-rail {
  opacity: 0;
  pointer-events: none;
}
#journey-rail.jr-visible {
  opacity: 1;
  pointer-events: auto;
}

/* ── Journeys glyph — static hidden base (LOCK-063) ─────────
   #journey-glyph is the DEC-034 §3e.4 universal summon affordance
   (top-center, all tiers), created and shown only by
   journey-rail.js (.jrg-visible). Same boot-suppression rationale
   as #journey-rail directly above: hidden and pointer-cold from
   page zero, immune to injection timing and module version drift.
   rail V1.1 build, T193. */

#journey-glyph {
  opacity: 0;
  pointer-events: none;
}
#journey-glyph.jrg-visible {
  opacity: 1;
  pointer-events: auto;
}

/* @version T204b — 2026-08-19 — DEVICE FIX (mobile half): on phone/tablet #card-panel is position:static BY DESIGN (so fixed children escape), and a static element ignores z-index — so the T204a container elevation was a no-op there. The elevation is applied to the CARD ELEMENT itself (.diorama-card / .diorama-card-mobile, z:65 in card-panel's injected sheet) under body.grid-persistent. Desktop unaffected. */
/* @version T204a — 2026-08-19 — DEVICE FIX: the board card elevation needed !important. card-panel.js writes its container z-index INLINE ('62') at init, which outranks any normal author rule; the T204 cut lost silently and the card opened BEHIND the tiles. Author !important beats a normal inline declaration. No other change. */
/* @version T204 — 2026-08-19 — TILE GRID BOARD MODE statics (schema 5.19 / grid-nav V1.2): body.grid-persistent elevates #card-panel to z:69 so a selected card comes forward OVER the persistent tiles (card-panel.js untouched — its z lives here); .gn-backdrop layer for the authored veil.image (scrim -> tint -> backdrop -> tiles); --gn-label-align fallback flipped left -> center per the schema default change. Board keeps the full body.grid-open chrome defense — persistent furniture over live hotspots is the LOCK-065 cl.2 class. */
/* @version T203 — 2026-08-18 — GRID NAV groundwork: #grid-nav static hidden base + --grid-veil-* levers (LOCK-063; interactive grant .gn-visible, z:68 above journey chrome / below admin+authoring+lobby-wall+entry-cover) + body.grid-open static chrome defense (hotspots pointer-cold, floats + transport suppressed; utility ?/gear survives per LOCK-067 — load-bearing here, it is the invoker). Grid is an invoked SURFACE, not a nav mode: no renderer suppression, no NAV_MODES entry. */
/* @version T197 — 2026-08-11 — DEC-030 Amendment C groundwork: #lobby-wall static hidden base + --lobby-veil-* levers (LOCK-063; interactive grant .lw-visible) + body.lobby-rest static chrome defense (hotspots pointer-cold, floats + transport suppressed; utility ?/gear survives per LOCK-067) */
/* @version T193 — 2026-08-10 — #journey-glyph static hidden base (DEC-034 §3e.4 glyph, rail V1.1 build; LOCK-063 pattern) */
/* @version T190 — 2026-08-08 — #journey-rail static hidden base (DEC-034 §3a rail build; LOCK-063 pattern) */
/* @version T181 — 2026-07-31 — #entry-cover static hidden base + veil (DEC-031 build; LOCK-063 pattern, pointer-cold all states) */
/* @version T180 — 2026-07-31 — static lobby suppression for .mf-float-arrow (mirror of T179 utility rule; mobile-footer V2.10 Fork A follow-on) */
/* @version T179 — 2026-07-31 — static lobby suppression for .mf-utility-btn (mobile-footer init-order phantom) */
/* @version T176 — 2026-07-28 — static hidden base for #coverflow-nav (phantom fix); stamp defect corrected (file carried T174 content under a T168 stamp) */
/* @version T174 — 2026-07-19 — mobile corner-triangle suppression scoped to non-lobby */
/* @version T168 — 2026-07-16 — scene container (stage gesture build) */
/* @version T163 — 2026-07-15 */
/* @version T160 — 2026-07-05 */
