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;
}

/* Laptop only: the rubrique nav bar moved from near the topbar to a
   fixed position near the bottom of the screen (see #gu-nav in
   nav-og.css). Reserving space at the bottom of #app's own flex
   column keeps #scene (flex:1, so it fills whatever's left) and
   #reset-camera (the flex item right after it) both clear of the bar
   automatically - moving the cube and the reset button up together
   without touching either one's own rule directly. Mobile already
   has its own dedicated bottom-stack (see the touch media query) and
   is untouched by this.

   VALUE (now that nav-og.css is available to check against): #gu-nav
   sits `bottom: max(24px, safe-area)` with its tallest row - the GU
   Tools/Undead Defenders/Game icon row stacked above Official Links -
   working out to roughly 65px, plus its own 6px top/bottom padding,
   so the bar's top edge lands about 24 + 77 ≈ 101px above the
   viewport bottom. 110px keeps a small safety margin above that
   (was 130px, which had ~29px of unused slack - trimmed here to hand
   a bit more of it to #scene instead, without risking an overlap). */
@media (pointer: fine) {
  #app {
    padding-bottom: 110px;
  }
}

#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;
  cursor: default;
  pointer-events: none;
}

@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;
  cursor: default;
  pointer-events: none;
}

@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;
}

/* ETH/USD price - same neon green already used for the tagline, the
   search input's focus ring and the "last sale" badge, so it reads
   as the site's own accent rather than a new color. Laptop: plain
   inline badge to the left of the search box (see the HTML, it's the
   first child of #search-cluster). Mobile gets its own fixed-corner
   treatment below, mirroring #status. */
#eth-price {
  color: #39ff8f;
  font-family: 'Courier New', monospace;
  font-size: 11px;
  white-space: nowrap;
  background: rgba(57, 255, 143, 0.06);
  border: 1px solid rgba(57, 255, 143, 0.2);
  border-radius: 6px;
  padding: 6px 10px;
}

#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);
}

/* "Reset position" + its logo: back in the normal document flow,
   right after #scene - already centered horizontally via the base
   rule's align-self:center. A small top margin keeps it from sitting
   glued directly against the cube's own bottom edge. The earlier
   fixed-position, bottom-right-corner placement is gone entirely per
   updated spec. Every positioning property is explicitly forced back
   to static/auto with !important here, in case a stale cached rule or
   an unexpected cascade conflict is still pinning it to a corner. */
@media (pointer: fine) {
  #reset-camera {
    position: static !important;
    top: auto !important;
    right: auto !important;
    bottom: auto !important;
    left: auto !important;
    align-self: center !important;
    margin: 12px 0 0 !important;
    z-index: auto !important;
  }
}

@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(150px, 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;
}

/* FIX: max-height used to reach almost to the very bottom of the
   viewport (only 24px clearance), which let the panel grow tall
   enough to sit on top of "reset position" - now fixed near the
   bottom of the screen alongside the relocated nav bar (laptop only).
   Capping it well above that instead. Top position
   (--panel-top-offset, tracking the topbar/search row) is untouched -
   this only limits how far down the panel can grow. Placed here,
   right after the base #panel rule, so it actually wins the cascade
   (same lesson as #gu-nav/#reset-camera earlier: equal specificity
   means whichever rule is LAST in the file wins, @media condition or
   not). */
@media (pointer: fine) {
  #panel {
    max-height: calc(100vh - var(--panel-top-offset, 84px) - 150px);
  }
}

#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;
}

/* -----------------------------------------------------------------
   TRADE HISTORY FLIP CARD
   Clicking the NFT image flips it, playing-card style, to reveal who
   currently holds it, how long they've held it, and the full
   transfer history - read straight from the contract's own Transfer
   event logs (see fetchTransferHistory in events.js), not the relay:
   the old relay-fed history table was removed on 2026-08-24 for being
   unreliable (see the comment above openPanel() in scene.js), so this
   rebuild deliberately goes straight to on-chain logs instead.
----------------------------------------------------------------- */
.flip-card {
  width: 100%;
  aspect-ratio: 1;
  margin-bottom: 16px;
}

.flip-card-inner {
  position: relative;
  width: 100%;
  height: 100%;
}

/* Display (front vs back) is controlled entirely from scene.js via
   inline styles (element.style.display/opacity/visibility) - not by
   any class or CSS rule here. Two earlier CSS-only approaches (a
   preserve-3d parent rotation, then independent per-face rotation
   with an opacity/visibility fallback) both left the back face
   effectively invisible/empty in practice, for reasons that persisted
   even through hard cache clears and private windows - so this is
   deliberately just positioning/appearance now, nothing that could
   fight the inline styles JS sets. */
.flip-card-front,
.flip-card-back {
  position: absolute;
  inset: 0;
  border-radius: 6px;
  cursor: pointer;
}



.flip-card-front {
  display: flex;
  flex-direction: column;
}

.flip-card-front .thumb {
  margin-bottom: 0;
  flex: 1;
  min-height: 0;
}

.flip-hint {
  flex: none;
  font-size: 9px;
  letter-spacing: 0.03em;
  color: #666;
  text-align: center;
  padding-top: 4px;
}

.flip-card-back {
  background: #1c1c20;
  border: 1px solid rgba(255, 255, 255, 0.14);
  display: flex;
  flex-direction: column;
  padding: 10px 12px;
  overflow: hidden;
}

.flip-back-header {
  flex: none;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-size: 11px;
  font-weight: normal;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #fff;
  margin-bottom: 8px;
}

.flip-back-return {
  text-transform: none;
  letter-spacing: normal;
  color: #666;
  font-size: 9.5px;
}

/* Reuses .panel-owner's own look (see below), just without the
   negative top margin meant for sitting directly under the image. */
#panel-owner-back {
  margin-top: 0;
  flex: none;
}

.hold-duration {
  flex: none;
  font-size: 10.5px;
  color: #39ff8f;
  margin-bottom: 10px;
}

.history-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.history-entry {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 8px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: 5px;
}

.history-entry .history-date {
  font-size: 9px;
  color: #777;
}

.history-entry .history-addrs {
  font-size: 10.5px;
  color: #ccc;
  overflow-wrap: anywhere;
}

.history-entry .history-addrs a {
  color: #6fa8e0;
  text-decoration: none;
}

.history-entry .history-addrs a:hover {
  text-decoration: underline;
}

.history-note,
.history-empty {
  font-size: 10.5px;
  color: #999;
  padding: 8px 0;
}

#panel .thumb {
  width: 100%;
  height: 100%;
  background: #1c1c20;
  border-radius: 6px;
  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;
  }

  /* ETH/USD price: no longer pinned to the top-left corner - now
     tucked directly under the "last sale" badge instead (its exact
     spot is computed live in events.js, positionEthPriceMobile(),
     since #last-sale-badge's own position shifts with content/wrap
     and can't be pinned with a fixed offset here). Smaller box and
     type than the laptop version to stay unobtrusive next to the
     badges. */
  #eth-price {
    position: fixed;
    z-index: 30;
    font-size: 8.5px;
    padding: 3px 6px;
  }

  /* 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;
  }

  /* BUGFIX: on mobile, #panel/#panel-scrim were still sitting at their
     desktop z-index (50 / 40) - well below #gu-nav (75), #gu-banner
     (76), and the other rubriques' own .gu-scrim/.gu-modal (90 / 91).
     Opening a Genuine Undead's detail panel therefore rendered it
     hidden behind the fixed nav bar instead of in the foreground.
     Raised above all of those (but still under #orientation-lock's
     1000, which must always win) so the panel is unambiguously on top
     whenever it's open, same as it already reads on desktop. */
  #panel-scrim {
    z-index: 94;
  }

  #panel {
    z-index: 95;
  }
}

/* ===================================================================
   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); }
  }
}
