html, body {
  margin: 0;
  height: 100%;
  background: #0a0a0c;
  overflow: hidden;
  font-family: 'Courier New', monospace;
}

* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

/* -----------------------------------------------------------------
   LAYOUT — two "floors", stacked, on every device: laptop and mobile
   alike. Nothing here is ever laid on top of the cube with absolute
   positioning, so nothing can overlap it.
     1. #topbar  — legend (left) + status/search (right)
     2. #scene   — the cube, filling all the remaining space
   (the grayed-out scrolling activity log that used to sit below the
   cube has been removed - live events now only show as a color pulse
   directly on the cube and in the panel's "Recent events" section.)
----------------------------------------------------------------- */
#app {
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
  gap: 10px;
  overflow: hidden;
}

#topbar {
  flex: none;
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "tagline status"
    "hud     search";
  align-items: start;
  column-gap: 12px;
  row-gap: 6px;
  padding: 10px 12px 0;
}

#matrix-tagline {
  grid-area: tagline;
  justify-self: start;
}

#hud {
  grid-area: hud;
  justify-self: start;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 16px;
  color: #ddd;
  font-size: 12px;
  /* No frame on purpose: the legend now sits under the title as plain
     text, not inside its own boxed widget. */
  background: none;
  padding: 0;
  border-radius: 0;
  border: none;
}

#hud span {
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}

/* Matrix-style pixel tagline, directly under the legend band.
   Reuses the site's own neon green (#39ff8f, already the accent on
   the search button and the block's halo) so it reads as part of the
   same "web3 terminal" language instead of a bolted-on effect. */
#matrix-tagline {
  font-family: 'Courier New', monospace;
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #39ff8f;
  background: rgba(57, 255, 143, 0.06);
  border: 1px solid rgba(57, 255, 143, 0.2);
  border-radius: 4px;
  padding: 5px 9px;
  text-shadow: 0 0 4px rgba(57, 255, 143, 0.75), 0 0 12px rgba(57, 255, 143, 0.3);
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

.matrix-cursor {
  display: inline-block;
  margin-left: 1px;
  animation: matrix-caret 1s step-end infinite;
}

@keyframes matrix-caret {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

/* The "3" in "Undead3" (Undead cubed, echoing the 3D block itself),
   set as a true exponent and blinking to draw the eye without
   competing with the cursor's own blink timing. */
.exp-cube {
  font-size: 0.7em;
  line-height: 1;
  margin-left: 1px;
  animation: exp-cube-blink 1.4s ease-in-out infinite;
}

@keyframes exp-cube-blink {
  0%, 45% { opacity: 1; }
  50%, 95% { opacity: 0.15; }
  100% { opacity: 1; }
}

.dot {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  display: inline-block;
  flex: none;
}

/* "sales 24h" badge — always-on pink treatment: same chip shape and
   pulsing glow throughout, since it's never toggled off. */
#sales-24h-badge {
  background: rgba(224, 71, 159, 0.28);
  border: 1px solid rgba(224, 71, 159, 0.5);
  border-radius: 4px;
  padding: 3px 9px;
  animation: sales-24h-pulse 1.6s ease-in-out infinite;
}

@keyframes sales-24h-pulse {
  0%, 100% { box-shadow: 0 0 6px rgba(224, 71, 159, 0.35); }
  50% { box-shadow: 0 0 14px rgba(224, 71, 159, 0.85); }
}

/* "last sale" badge — the single most recent sale, always on, same
   chip/pulse treatment as "sales 24h" right before it, just green.
   Inline next to it on laptop (small margin-left for a bit of
   breathing room); forced onto its own line right below it on mobile
   (flex-basis:100% in the touch media query further down). */
#last-sale-badge {
  background: rgba(57, 255, 143, 0.28);
  border: 1px solid rgba(57, 255, 143, 0.5);
  border-radius: 4px;
  padding: 3px 9px;
  margin-left: 10px;
  animation: last-sale-pulse 1.6s ease-in-out infinite;
}

@keyframes last-sale-pulse {
  0%, 100% { box-shadow: 0 0 6px rgba(57, 255, 143, 0.35); }
  50% { box-shadow: 0 0 14px rgba(57, 255, 143, 0.85); }
}

#status {
  grid-area: status;
  justify-self: end;
  flex: none;
  color: #666;
  font-size: 11px;
  background: rgba(255, 255, 255, 0.05);
  padding: 6px 10px;
  border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  white-space: nowrap;
}

#status.live { color: #3fbf6b; }
#status.offline { color: #e0a030; }

/* Search form, the green cross (right after the arrow - not grouped
   with the sound button) and the sound toggle, all in one cluster so
   the whole group can be repositioned as a unit per breakpoint. Sits
   in a shared column with the credit link (#origin-site) right below
   it - see #search-column - so the two always stay close together
   instead of the link ending up far below everything else. */
#search-column {
  grid-area: search;
  justify-self: end;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
}

#search-cluster {
  display: flex;
  align-items: center;
  gap: 8px;
}

#search-gu {
  display: flex;
  align-items: center;
  gap: 8px;
  background: rgba(255, 255, 255, 0.05);
  padding: 8px 10px;
  border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.1);
}

#search-gu label {
  color: #888;
  font-size: 11px;
  white-space: nowrap;
}

#search-gu input {
  width: 64px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 4px;
  color: #eee;
  font-family: 'Courier New', monospace;
  font-size: 12px;
  padding: 5px 6px;
  outline: none;
  transition: border-color 0.15s ease;
}

#search-gu input:focus {
  border-color: #39ff8f;
}

#search-gu input.invalid {
  border-color: #e0503f;
  animation: search-shake 0.35s ease;
}

@keyframes search-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}

#search-gu button,
#search-gu-reset,
#sound-toggle {
  background: rgba(57, 255, 143, 0.12);
  border: 1px solid rgba(57, 255, 143, 0.35);
  color: #39ff8f;
  border-radius: 4px;
  width: 26px;
  height: 26px;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

#search-gu button:hover,
#search-gu-reset:hover,
#sound-toggle:hover {
  background: rgba(57, 255, 143, 0.2);
}

#search-gu button:active,
#search-gu-reset:active,
#sound-toggle:active {
  background: rgba(57, 255, 143, 0.3);
}

#search-gu-reset {
  font-size: 12px;
}

/* Muted state: same chip, dimmed to grey so it's obvious at a glance
   the background track is currently off. */
#sound-toggle.muted {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.18);
  color: #777;
}

#sound-toggle.muted:hover {
  background: rgba(255, 255, 255, 0.09);
}

/* -----------------------------------------------------------------
   RESET POSITION — a normal flex child of #app, directly under #scene
   on laptop (see the HTML - the button sits right after the cube in
   the DOM), never a fixed overlay there. #app's own `gap` (see the
   LAYOUT rule near the top of this file) already provides the "small
   space" between the cube and this button for free. On touch devices
   it goes back to a small, discreet, fixed corner badge instead (see
   the touch media query further down) - always visible, never in the
   way of the search controls. Pulses continuously either way.
----------------------------------------------------------------- */
#reset-camera {
  flex: none;
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  background: none;
  border: none;
  padding: 4px;
  cursor: pointer;
}

#reset-camera .reset-square {
  width: 12px;
  height: 12px;
  border-radius: 2px;
  background: #39ff8f;
  box-shadow: 0 0 6px rgba(57, 255, 143, 0.8), 0 0 14px rgba(57, 255, 143, 0.4);
  animation: reset-pulse 1.6s ease-in-out infinite;
}

#reset-camera .reset-label {
  font-family: 'Courier New', monospace;
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(57, 255, 143, 0.75);
  white-space: nowrap;
}

#reset-camera:hover .reset-square {
  box-shadow: 0 0 9px rgba(57, 255, 143, 1), 0 0 20px rgba(57, 255, 143, 0.6);
}

@keyframes reset-pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.35); opacity: 0.55; }
}

/* -----------------------------------------------------------------
   CODE SCROLL — the real, verified Genuine Undead contract source
   (see contract-source.js) endlessly scrolling in a frameless strip.
   Purely decorative: pointer-events stay off so it never steals a
   drag/click meant for the cube underneath it, and it never recolors
   or resizes on click/hover.

   Laptop: a tall fixed band down the left edge, running from a gap
   below the topbar (below the "sales 24h" badge) to near the bottom
   of the screen - reuses --panel-top-offset (already measured live
   off the topbar's real height in scene.js for the detail panel) so
   the gap never drifts out of sync with the topbar's own height.
   Touch devices override this entirely below (full-width strip under
   the cube instead - see the touch media query further down).
----------------------------------------------------------------- */
#code-scroll {
  position: fixed;
  left: max(10px, env(safe-area-inset-left));
  top: calc(var(--panel-top-offset, 84px) + 20px);
  bottom: max(20px, env(safe-area-inset-bottom));
  width: 380px;
  overflow: hidden;
  pointer-events: none;
  z-index: 3;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
}

.code-scroll-track {
  animation-name: code-scroll-loop;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.code-scroll-line {
  font-family: 'Courier New', monospace;
  font-size: 10.5px;
  line-height: 1.6em;
  white-space: pre;
  color: #39ff8f;
  opacity: 0.4;
  text-shadow: 0 0 4px rgba(57, 255, 143, 0.5);
}

@keyframes code-scroll-loop {
  from { transform: translateY(0); }
  to { transform: translateY(-50%); }
}

/* -----------------------------------------------------------------
   ORIGIN SITE LINK — credits the source community site. Green, same
   neon accent as the rest of the topbar/search/reset controls (not
   the pink "sale" color, since this isn't a live-data element).
   Laptop: fixed bottom-right, roughly the same line as "reset
   position" below the cube. Mobile: pulled out of fixed positioning
   entirely - it becomes a normal, centered, small in-flow line right
   under the topbar (see the touch media query further down).
----------------------------------------------------------------- */
#origin-site {
  position: fixed;
  right: max(14px, env(safe-area-inset-right));
  bottom: max(14px, env(safe-area-inset-bottom));
  z-index: 30;
  font-family: 'Courier New', monospace;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: #39ff8f;
  text-decoration: none;
  text-shadow: 0 0 6px rgba(57, 255, 143, 0.5);
  opacity: 0.85;
  transition: opacity 0.15s ease;
}

#origin-site:hover {
  opacity: 1;
  text-decoration: underline;
}

#scene {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
  border-radius: 8px;
}

#scene canvas {
  display: block;
  touch-action: none;
}

/* -----------------------------------------------------------------
   DETAIL PANEL — a floating, closable, draggable window that pops
   up over everything when a fragment is clicked or a GU# is
   searched. Free to move and reposition on both mouse and touch
   devices; the titlebar is the drag handle.
----------------------------------------------------------------- */
#panel {
  position: fixed;
  /* --panel-top-offset is set by scene.js from #topbar's own actual
     rendered height (plus a small margin) - a fixed 84px guess used
     to sit right on top of the sound/search-reset row once that row
     was added under the search box, so the panel opened overlapping
     it. Measuring the real height means this stays correct even if
     the legend/tagline's own wrapping changes how tall the topbar is. */
  top: var(--panel-top-offset, 84px);
  right: 20px;
  width: min(320px, calc(100vw - 24px));
  /* Was capped at a flat 560px regardless of screen size, which left
     a lot of dead space (and cut content short) on most laptop
     displays. Deriving it from the viewport itself - minus the
     panel's own top offset and a matching bottom margin - means it
     always fills the available height down near the bottom of the
     screen and scales automatically with any laptop resolution. */
  max-height: calc(100vh - var(--panel-top-offset, 84px) - 24px);
  display: flex;
  flex-direction: column;
  background: rgba(14, 14, 17, 0.96);
  backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 10px;
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5);
  color: #eee;
  z-index: 50;
  opacity: 0;
  transform: translateY(-8px) scale(0.98);
  pointer-events: none;
  transition: opacity 0.16s ease, transform 0.16s ease;
}

#panel.open {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}

#panel.dragging {
  transition: none;
}

#panel-titlebar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 14px 10px 14px 18px;
  cursor: grab;
  touch-action: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  user-select: none;
}

#panel.dragging #panel-titlebar {
  cursor: grabbing;
}

#panel h2 {
  font-size: 15px;
  margin: 0;
  font-weight: normal;
}

#panel .close {
  flex: none;
  cursor: pointer;
  color: #999;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 6px;
  width: 28px;
  height: 28px;
  font-size: 16px;
  line-height: 1;
}

#panel .close:hover {
  color: #eee;
  background: rgba(255, 255, 255, 0.12);
}

#panel-body {
  overflow-y: auto;
  padding: 18px;
}

#panel .thumb {
  width: 100%;
  aspect-ratio: 1;
  background: #1c1c20;
  border-radius: 6px;
  margin-bottom: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #555;
  font-size: 11px;
  overflow: hidden;
}

#panel .thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Owner wallet — sits directly under the NFT image, above the traits
   section. Deliberately plain (no card/border) so it reads as a
   caption on the artwork rather than another data block. */
.panel-owner {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 11px;
  color: #888;
  margin-top: -6px;
  margin-bottom: 4px;
  overflow-wrap: anywhere;
}

.owner-label {
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 9.5px;
  color: #777;
  white-space: nowrap;
}

.owner-address {
  color: #6fa8e0;
  text-decoration: none;
  font-size: 11.5px;
}

.owner-address:hover {
  text-decoration: underline;
}

.owner-address.owner-pending {
  color: #666;
  pointer-events: none;
}

.panel-section {
  margin-top: 18px;
}

.panel-section-title {
  margin: 0 0 10px;
  font-size: 11px;
  font-weight: normal;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #888;
}

.empty-note {
  font-size: 11px;
  color: #666;
  padding: 10px 0;
}

.traits-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
}

.trait-card {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 6px;
  padding: 10px 12px;
}

.trait-label {
  font-size: 9.5px;
  letter-spacing: 0.05em;
  color: #777;
  text-transform: uppercase;
  margin-bottom: 4px;
}

.trait-value {
  font-size: 12px;
  color: #eee;
  margin-bottom: 8px;
  overflow-wrap: anywhere;
}

.trait-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 10.5px;
}

.trait-rarity {
  color: #6fa8e0;
  background: rgba(63, 143, 224, 0.12);
  border: 1px solid rgba(63, 143, 224, 0.3);
  border-radius: 4px;
  padding: 2px 6px;
}

.trait-floor {
  color: #aaa;
  white-space: nowrap;
}

/* Backdrop behind the floating panel. Transparent/inert on desktop
   (the panel is small and docked, nothing needs dimming); on touch
   devices it dims the cube and makes clear the panel is a modal
   layer, and tapping it closes the panel - see mobile block below. */
#panel-scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: transparent;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.16s ease;
}

/* ===================================================================
   TOUCH DEVICES (phones / tablets)
   Same three-floor structure; just tighter spacing and a panel sized
   to actually fit a small screen instead of a laptop sidebar.
=================================================================== */
@media (pointer: coarse) {
  #app {
    gap: 8px;
  }

  #topbar {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "tagline tagline"
      "hud     search";
    row-gap: 8px;
    padding: calc(8px + env(safe-area-inset-top)) 10px 0;
  }

  #hud {
    font-size: 10.5px;
    padding: 8px 10px;
    gap: 6px 10px;
    flex: 1 1 auto;
  }

  /* Forced onto its own line right below "sales 24h" (flex-basis:100%
     in a wrapping flex container pushes an item onto a fresh line,
     regardless of how much space would otherwise be left on the
     current one) - laptop keeps them side by side (see the shared
     rule above). */
  #last-sale-badge {
    flex-basis: 100%;
    margin-left: 0;
  }

  /* Now spans the full topbar width (see grid-template-areas above) -
     #status no longer actually occupies column 2 (it's pulled out via
     position:fixed), so tagline was needlessly cramped into column 1's
     share alone, which is what truncated "GENUINE UNDEAD COMES TO
     LIFE" down to "UNDE…". white-space:normal is a safety net so even
     a very narrow phone wraps the text instead of cutting it off -
     the blinking cursor/exponent effects (see the shared rule and
     keyframes above) keep working the same either way. */
  #matrix-tagline {
    font-size: 9px;
    padding: 4px 7px;
    letter-spacing: 0.1em;
    white-space: normal;
    text-overflow: clip;
  }

  /* "live" gets the entire top-right corner to itself now that
     reset-camera no longer competes for it (see #reset-camera below) -
     pinned to the true screen corner, independent of the grid cell, so
     it can never be crowded by anything else in the topbar. */
  #status {
    position: fixed;
    top: max(10px, env(safe-area-inset-top));
    right: max(10px, env(safe-area-inset-right));
    z-index: 30;
    font-size: 10px;
    padding: 5px 8px;
  }

  /* Search cluster: same row as "sales 24h" (see the grid areas
     above), pushed as far right as possible via #search-column's own
     align-items:flex-end - no longer its own centered row underneath. */
  #search-cluster {
    flex-wrap: nowrap;
    gap: 6px;
  }

  #search-gu {
    padding: 5px 7px;
    gap: 5px;
  }

  #search-gu label {
    font-size: 9.5px;
  }

  #search-gu input {
    width: 44px;
    font-size: 11px;
    padding: 4px 5px;
  }

  #search-gu button,
  #search-gu-reset,
  #sound-toggle {
    width: 24px;
    height: 24px;
    font-size: 11px;
  }

  /* reset-camera: back to a small, ALWAYS-VISIBLE corner badge on
     touch devices only (laptop keeps it docked under the cube - see
     the shared rule above). Bottom-right corner. The label sits to
     the LEFT of the square: row-reverse flips the visual order
     without touching the HTML - the square stays the first DOM child
     (so nothing else needs to change), it just renders second/
     rightmost. */
  #reset-camera {
    position: fixed;
    bottom: max(8px, env(safe-area-inset-bottom));
    right: max(8px, env(safe-area-inset-right));
    z-index: 30;
    flex-direction: row-reverse;
    gap: 6px;
    padding: 6px 8px;
  }

  #reset-camera .reset-label {
    font-size: 8px;
  }

  #reset-camera .reset-square {
    width: 10px;
    height: 10px;
  }

  /* Mobile: no longer a fixed left-edge strip - an in-flow band right
     under the cube (see the HTML - #code-scroll is a flex child of
     #app, between #scene and #reset-camera). Static positioning here
     overrides the fixed positioning laptop uses, so it participates
     in #app's flex column layout like any other block instead of
     floating over it. Width is capped, not full-bleed: it stops right
     at the left edge of "reset position"'s own R (see
     --code-scroll-max-width, measured live in scene.js off the
     reset-camera badge's actual rendered position) so the two never
     visually collide in the bottom-right corner. */
  #code-scroll {
    display: block;
    position: static;
    left: auto;
    top: auto;
    bottom: auto;
    width: var(--code-scroll-max-width, 70%);
    /* 8 lines * 13px line-height (9px font * 1.45) */
    height: 105px;
    flex: none;
    background: transparent;
    /* Dropped on mobile: some iOS Safari versions have real rendering
       bugs combining overflow:hidden with a CSS mask-image gradient
       on a small/dynamically-sized element, which can blank the
       element out entirely instead of just skipping the fade. Clean
       top/bottom clipping (no fade) is a safe trade-off here. */
    -webkit-mask-image: none;
    mask-image: none;
  }

  #code-scroll .code-scroll-line {
    font-size: 9px;
    line-height: 1.45em;
  }

  /* No longer fixed here - a small, discreet in-flow line, now
     nested inside #search-column right under #search-cluster (see the
     HTML) instead of sitting below the whole topbar. #search-column's
     own align-items:flex-end already right-aligns it under the arrow/
     cross/sound with no extra offset needed. margin-top adds a bit
     more breathing room than the column's own 4px gap (fine on
     laptop, too tight on a phone screen) between it and the buttons
     right above it. */
  #origin-site {
    position: static;
    display: block;
    text-align: right;
    left: auto;
    right: auto;
    bottom: auto;
    flex: none;
    margin-top: 8px;
    font-size: 7px;
    opacity: 0.55;
  }

  /* Docks almost flush under the topbar's own row of elements (hud +
     search cluster), with just the small margin --panel-top-offset
     already bakes in (see scene.js: topbar's real measured height +
     16px). Reusing that live-measured var - already used for the
     laptop panel - instead of a fixed vh guess means this always
     tracks the topbar's actual height, on any device. */
  #panel {
    top: var(--panel-top-offset, 20vh);
    bottom: auto;
    left: 50%;
    right: auto;
    transform: translate(-50%, -8px) scale(0.98);
    width: min(360px, calc(100vw - 24px));
    max-height: calc(100vh - var(--panel-top-offset, 20vh) - 16px);
  }

  #panel.open {
    transform: translate(-50%, 0) scale(1);
  }

  /* Once dragged, the panel switches to explicit left/top pixel
     positioning (set in scene.js) and this centering transform must
     no longer fight it. */
  #panel.dragged {
    left: 0;
    transform: none;
  }

  #panel.dragged.open {
    transform: none;
  }

  /* The panel necessarily sits over part of the layout when open -
     that's expected (it's a detail view, not a fourth "floor"). The
     scrim makes that explicit instead of it reading as an overlap
     bug, dims the cube so it's visually secondary while the panel is
     up, and gives a large, obvious "tap outside to close" target. */
  #panel-scrim {
    background: rgba(6, 6, 8, 0.55);
  }

  #panel-scrim.open {
    opacity: 1;
    pointer-events: auto;
  }
}

/* ===================================================================
   PORTRAIT-ONLY LOCK (phones / tablets)
   The Screen Orientation API's lock('portrait') call (attempted in
   scene.js) only actually works in a handful of browser/context
   combinations (e.g. Chrome on Android, and only in fullscreen) - it
   has no effect at all in iOS Safari, which has never implemented it.
   A plain website can't force-rotate the OS the way an installed
   native app can, so the only reliable, cross-platform way to keep
   the experience portrait-only is this: the moment a touch device is
   actually in landscape, cover the whole screen with a blocking
   message instead of trying to reflow the cube/panel sideways - see
   #orientation-lock in the HTML, hidden everywhere except here.
=================================================================== */
#orientation-lock {
  display: none;
}

@media (pointer: coarse) and (orientation: landscape) {
  #orientation-lock {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 1000;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 24px;
    background: #0a0a0c;
    color: #ddd;
    font-family: 'Courier New', monospace;
    text-align: center;
  }

  #orientation-lock .rotate-icon {
    font-size: 32px;
    animation: orientation-rotate-hint 1.8s ease-in-out infinite;
  }

  #orientation-lock p {
    max-width: 260px;
    font-size: 13px;
    line-height: 1.5;
    color: #aaa;
    margin: 0;
  }

  @keyframes orientation-rotate-hint {
    0%, 100% { transform: rotate(0deg); }
    50% { transform: rotate(-90deg); }
  }
}
