/* ==========================================================================
   Spatial Gallery Wall Architecture (Phase 3 — rebuilt)
   ========================================================================== */

html, body.gallery-body {
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
  user-select: none;
  background-color: #eae6db;
}

/* --- Plaster Wall Surface ---
   Every decorative layer (perimeter vignette, plaster grain, warm light pool and
   the shadow at the wall/floor junction) lives in this single element's background
   stack. Nothing is a separate overlay element any more, so nothing can end up
   painted on top of an artwork. */
.spatial-gallery-stage {
  position: relative;
  width: 100vw;
  /* dvh, not vh: on mobile browsers 100vh is the viewport *without* the browser's
     chrome collapsed in, so the stage was taller than the screen and its bottom edge
     — which is exactly where the filmstrip sits — was pushed off it. The vh line stays
     first as the fallback for browsers without dvh. */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Room tokens: the junction, the plate band and the light beam are all derived
     from these, so they cannot drift apart as the viewport changes. */
  --floor-line: clamp(150px, 22dvh, 320px);

  /* --- Vertical budget ---
     --stage-pad-top/--stage-pad-bottom are the room reserved above and below the plate
     band. They deliberately keep their existing resolved values at every breakpoint:
     the band is centred inside them, so changing them slides the paintings relative to
     the overhead fixture and its beam. Everything *below* the paintings is instead
     derived from the dock tokens, and --plate-h is the only value allowed to shrink —
     and only ever when the stage would be taller than the visible viewport. */
  --stage-pad-top: 70px;
  --stage-pad-bottom: 240px;
  --filmstrip-h: clamp(38px, 5.2dvh, 46px);
  --filmstrip-shell-h: calc(var(--filmstrip-h) + 12px); /* 5px padding + 1px border, both sides */
  --dock-gap: clamp(10px, 2.4dvh, 16px);
  --dock-inset: calc(var(--dock-gap) + env(safe-area-inset-bottom, 0px));
  --hud-clearance: clamp(14px, 2.5dvh, 22px);
  --plate-h: min(clamp(240px, 44dvh, 560px), calc(100dvh - var(--stage-pad-top) - var(--stage-pad-bottom)));
  /* Eye-level camera for the floor plane (the plates keep their own perspective
     on .wall-viewport, so this does not affect the tilt). */
  perspective: 1200px;
  perspective-origin: 50% 45%;
  background-color: #e4e1c3;
  background-image:
    radial-gradient(ellipse 78% 62% at 50% 44%, transparent 42%, rgba(58, 46, 26, 0.4) 100%),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>"),
    radial-gradient(ellipse 30% 34% at 50% 30%, rgba(255, 250, 235, 0.6) 0%, rgba(255, 250, 235, 0.18) 45%, transparent 70%),
    linear-gradient(0deg, rgba(40, 30, 15, 0.2) 0%, transparent 100%),
    radial-gradient(at 15% 15%, rgba(255, 255, 255, 0.55) 0px, transparent 65%),
    radial-gradient(at 85% 20%, rgba(254, 228, 81, 0.14) 0px, transparent 60%),
    radial-gradient(at 50% 80%, rgba(2, 155, 189, 0.08) 0px, transparent 70%),
    linear-gradient(180deg, #f5f2d2 0%, #e4e1c3 62%, #cac8ae 88%, #b1af98 100%);
  background-size:
    100% 100%,
    180px 180px,
    100% 100%,
    100% 6px,
    100% 100%,
    100% 100%,
    100% 100%,
    100% 100%;
  background-position:
    0 0,
    0 0,
    0 0,
    0 calc(100% - var(--floor-line) - 6px),
    0 0,
    0 0,
    0 0,
    0 0;
  background-repeat: no-repeat, repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat;
  transition: background-color 0.3s ease;
}

[data-theme="dark"] .spatial-gallery-stage {
  background-color: #01142e;
  /* Same eight layers, same order and geometry as the light room. The grain is
     dialled right down here — at 0.5 it read as noise, not plaster. */
  background-image:
    radial-gradient(ellipse 78% 62% at 50% 44%, transparent 38%, rgba(0, 0, 0, 0.65) 100%),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.14'/></svg>"),
    radial-gradient(ellipse 30% 34% at 50% 30%, rgba(254, 228, 81, 0.14) 0%, rgba(254, 228, 81, 0.05) 45%, transparent 70%),
    linear-gradient(0deg, rgba(0, 0, 0, 0.45) 0%, transparent 100%),
    radial-gradient(at 15% 15%, rgba(2, 155, 189, 0.12) 0px, transparent 65%),
    radial-gradient(at 85% 20%, rgba(4, 25, 56, 0.8) 0px, transparent 60%),
    radial-gradient(at 50% 80%, rgba(2, 155, 189, 0.06) 0px, transparent 70%),
    linear-gradient(180deg, #061c3d 0%, #04132c 62%, #020d1f 88%, #010713 100%);
}

/* --- Floor Plane ---
   A real plane tipped away from the camera (the perspective comes from the stage),
   hinged on its top edge so the wall/floor boundary sits exactly --floor-line above
   the bottom of the screen. Warm concrete that recedes — darkest at the far edge,
   lifted toward the viewer — with the grain in the same background stack. */
.gallery-floor {
  position: absolute;
  left: -50vw;
  width: 200vw;
  height: 90dvh;
  top: calc(100% - var(--floor-line) - 2px); /* tuck 2px under the wall: no hairline seam */
  transform-origin: top center;
  transform: rotateX(72deg);
  z-index: 2;
  pointer-events: none;
  background-color: #8d8570;
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>"),
    linear-gradient(180deg, #6a6350 0%, #857e66 26%, #a49b7d 58%, #c6bc99 100%);
  background-size: 180px 180px, 100% 100%;
  background-repeat: repeat, no-repeat;
  background-position: 0 0, 0 0;
}

[data-theme="dark"] .gallery-floor {
  background-color: #010a16;
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.12'/></svg>"),
    radial-gradient(ellipse 60% 40% at 50% 0%, rgba(2, 155, 189, 0.1) 0%, transparent 70%),
    linear-gradient(180deg, #010610 0%, #04132c 55%, #08203f 100%);
  background-size: 180px 180px, 100% 100%, 100% 100%;
  background-repeat: repeat, no-repeat, no-repeat;
  background-position: 0 0, 0 0, 0 0;
}

/* --- Wall Backdrop: Parallax Drift ---
   A separate, oversized layer behind everything else, translated at a fraction of
   the wall track's own speed. This is what sells "the wall receding as you walk
   forward" — the paintings (near) travel the full scroll distance, this (far)
   travels only ~12% of it, same direction, so it visibly lags behind. Deliberately
   NOT part of .spatial-gallery-stage's own background stack: that stack has layers
   locked to the light fixture and the floor-line junction, which must never drift. */
.wall-backdrop {
  position: absolute;
  top: -10dvh;
  left: -10vw;
  width: 120vw;
  height: 120dvh;
  z-index: 1;
  pointer-events: none;
  will-change: transform;
  background-image:
    radial-gradient(ellipse 55% 45% at 30% 20%, rgba(255, 250, 235, 0.1) 0%, transparent 65%),
    radial-gradient(ellipse 45% 40% at 75% 70%, rgba(2, 155, 189, 0.05) 0%, transparent 65%);
  background-repeat: no-repeat;
}

[data-theme="dark"] .wall-backdrop {
  background-image:
    radial-gradient(ellipse 55% 45% at 30% 20%, rgba(254, 228, 81, 0.06) 0%, transparent 65%),
    radial-gradient(ellipse 45% 40% at 75% 70%, rgba(2, 155, 189, 0.08) 0%, transparent 65%);
}

[data-perf="low"] .wall-backdrop {
  transform: none !important;
}

@media (prefers-reduced-motion: reduce) {
  .wall-backdrop {
    transform: none !important;
  }
}

/* --- Overhead Linear Spotlight Fixture ---
   A wire running up out of frame, a dark fixture body sitting on the bar, a shorter
   bar, and a beam that widens from the bar's edges down onto the plate band. The
   beam's gradient reaches zero alpha at 88% of its own height, so it can never end
   in a hard line — the structural guarantee against a bar anywhere on the wall. */
.overhead-light-fixture {
  position: absolute;
  top: clamp(72px, 10dvh, 112px);
  left: 50%;
  transform: translateX(-50%);
  width: min(240px, 22vw);
  height: 4px;
  z-index: 20;
  pointer-events: none;
}

/* Wire up into the ceiling — the fixture is visibly attached to something. */
.fixture-wire {
  position: absolute;
  bottom: 100%;
  left: 50%;
  width: 2px;
  height: 85dvh;
  transform: translateX(-50%);
  background: linear-gradient(180deg, rgba(24, 19, 12, 0.12) 0%, rgba(24, 19, 12, 0.6) 60%, rgba(24, 19, 12, 0.78) 100%);
}

/* Dark body of the fixture, directly on top of the bar. */
.fixture-holder {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  width: 78px;
  height: 13px;
  border-radius: 5px 5px 2px 2px;
  background: linear-gradient(180deg, #332c23 0%, #1b1611 70%, #0f0c08 100%);
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.14);
}

.spotlight-bar {
  width: 100%;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(180deg, #ffffff 0%, #f7f3e2 100%);
  box-shadow: 0 0 14px rgba(255, 255, 255, 0.95), 0 0 30px rgba(254, 228, 81, 0.4);
}

/* The beam: a true trapezoid from the bar's edges down to the plate band. */
.spotlight-cone {
  position: absolute;
  top: 4px;
  left: 50%;
  transform: translateX(-50%);
  width: min(420px, 88vw);
  height: clamp(160px, calc(100dvh - var(--plate-h) - 170px), 72dvh);
  clip-path: polygon(23% 0%, 77% 0%, 100% 100%, 0% 100%);
  background-image:
    radial-gradient(ellipse 46% 30% at 50% 82%, rgba(255, 250, 235, 0.3) 0%, transparent 72%),
    linear-gradient(180deg, rgba(255, 252, 238, 0.26) 0%, rgba(255, 252, 238, 0.12) 42%, rgba(255, 252, 238, 0.05) 68%, rgba(255, 252, 238, 0) 88%);
  pointer-events: none;
}

[data-theme="dark"] .fixture-wire {
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.2) 0%, rgba(2, 12, 26, 0.6) 60%, rgba(2, 12, 26, 0.8) 100%);
}

[data-theme="dark"] .fixture-holder {
  background: linear-gradient(180deg, #0b1728 0%, #050c16 70%, #010509 100%);
}

[data-theme="dark"] .spotlight-cone {
  background-image:
    radial-gradient(ellipse 46% 30% at 50% 82%, rgba(254, 228, 81, 0.16) 0%, transparent 72%),
    linear-gradient(180deg, rgba(254, 228, 81, 0.16) 0%, rgba(254, 228, 81, 0.07) 42%, rgba(254, 228, 81, 0.03) 68%, rgba(254, 228, 81, 0) 88%);
}

/* Horizontal Wall Viewport */
.wall-viewport {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  overflow: hidden;
  box-sizing: border-box;
  /* Top room for the hook and the shadow, bottom room for the HUD and the docked
     filmstrip. Both come from the stage's vertical budget, so the dock and the
     paintings can never overlap and no part of the room has to be scrolled to. */
  padding: var(--stage-pad-top) 0 var(--stage-pad-bottom);
  /* Pieces dissolve into the wall at the screen edges instead of being sliced off
     mid-canvas as they arrive from the right. A wider, softer fade (11% each side)
     reads as the painting sitting in shadow rather than being cut by the frame. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 11%, #000 89%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 11%, #000 89%, transparent 100%);
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  perspective: 1400px;
}

.wall-track {
  display: flex;
  align-items: center;
  gap: clamp(48px, 9vw, 130px);
  padding: 0 50vw; /* Centers initial and ending works */
  /* The one always-on compositing hint on the page: a single layer for the whole
     travelling track, which is exactly what a transform-per-frame loop wants. */
  will-change: transform;
  transform: translate3d(0, 0, 0);
  transform-style: preserve-3d;
}

/* Hung Painting Frame Unit */
.wall-painting-unit {
  --hang-tilt: 0deg;
  position: relative;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  transform-style: preserve-3d;
  /* No will-change here. It used to sit on all 19 units — and, with the rules further
     down, on each frame face, image and open button as well — which is roughly 76
     permanent layer hints for one screen. The hint now lives on .is-active/.is-near. */
  transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1), filter 0.28s ease, opacity 0.28s ease;
  text-decoration: none !important;
}

/* Hanging Wire — small physical detail that sells "mounted on a wall"
   far more than any amount of shadow tuning alone. */
.canvas-hook {
  position: absolute;
  top: -34px;
  left: 50%;
  width: 1px;
  height: 36px;
  transform: translateX(-50%) translateZ(2px);
  background: linear-gradient(180deg, transparent 0%, rgba(40, 34, 24, 0.55) 30%, rgba(40, 34, 24, 0.65) 100%);
  pointer-events: none;
}

.canvas-hook::before {
  /* Wall-mounted nail/hook head */
  content: "";
  position: absolute;
  top: -3px;
  left: 50%;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  transform: translateX(-50%);
  background: radial-gradient(circle at 35% 30%, #6b6357, #2c2620);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}

[data-theme="dark"] .canvas-hook {
  background: linear-gradient(180deg, transparent 0%, rgba(200, 200, 210, 0.35) 30%, rgba(200, 200, 210, 0.45) 100%);
}

/* Base Shadow Layer (Decoupled & Anchored) */
.canvas-shadow-layer {
  position: absolute;
  inset: 0;
  border-radius: 4px;
  background: transparent;
  box-shadow:
    0 32px 54px rgba(1, 35, 80, 0.28),
    0 12px 24px rgba(0, 0, 0, 0.22),
    0 1px 3px rgba(0, 0, 0, 0.15);
  /* translateZ only — never translateY. Pushing this layer down left a strip of
     un-shadowed wall between the canvas's bottom edge and the top of its own drop
     shadow, which read as a bright "lip" under every painting: cream in light mode,
     dark blue in dark mode. Keep the layer flush with the plate so the shadow starts
     at the bottom edge. */
  transform: translateZ(-20px);
  pointer-events: none;
}

[data-theme="dark"] .canvas-shadow-layer {
  box-shadow:
    0 36px 64px rgba(0, 0, 0, 0.75),
    0 14px 28px rgba(0, 0, 0, 0.55);
}

/* Physical Canvas Face */
.canvas-frame-face {
  position: relative;
  height: var(--plate-h); /* one source of truth: the beam and the plates share it */
  /* Width is derived, never measured: --plate-ratio is written from the plate's own
     pixels, so the frame stays correct on resize instead of freezing a pixel value. */
  width: calc(var(--plate-h) * var(--plate-ratio, 0.8));
  border-radius: 4px;
  overflow: hidden;
  background: #202020;
  transform: translateZ(0);
  transform-style: preserve-3d;
  backface-visibility: hidden;
  /* Physical canvas-edge bevel. Deliberately no 1px hairline ring: a thin ring on
     a rotated, composited plate shimmers while the mouse tilt animates. */
  box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.35);
  /* js/gallery.js writes the plate's LQIP here as an inline background, so the frame
     reads as a blurred version of the painting from the first paint instead of an
     empty rectangle. It stays behind the bitmap afterwards, which fills the odd
     letterboxed sliver on plates whose recorded ratio is a pixel or two out. */
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.canvas-image {
  height: 100%;
  width: auto;
  display: block;
  object-fit: contain;
  transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  pointer-events: none;
}

/* Active Center vs Flanking Depth */
.wall-painting-unit {
  opacity: 0.62;
  filter: brightness(0.88) contrast(97%) saturate(0.94);
  transform: scale(0.94) rotateZ(var(--hang-tilt));
}

.wall-painting-unit.is-active {
  opacity: 1;
  /* filter: none rather than a brighter filter — the base rule dims every plate, and
     re-rasterising the active one each frame is what made its edges crawl. */
  filter: none;
  transform: scale(1.0) rotateZ(var(--hang-tilt));
  z-index: 10;
}

/* Compositing hints, scoped to the three plates that are actually moving: the one under
   the light and its immediate neighbours (.is-near is toggled by js/gallery.js as the
   active index changes). Every other plate stays unpromoted. */
.wall-painting-unit.is-active,
.wall-painting-unit.is-near {
  will-change: transform, opacity;
}

.wall-painting-unit.is-active .canvas-frame-face,
.wall-painting-unit.is-near .canvas-frame-face {
  will-change: transform;
}

.wall-painting-unit.is-active .canvas-image {
  will-change: transform;
}

/* Desktop 3D Perspective Tilt on Active Center Piece */
@media (hover: hover) and (pointer: fine) {
  .wall-painting-unit.is-active {
    --tilt-x: 0deg;
    --tilt-y: 0deg;
    transform: scale(1.0) rotateZ(var(--hang-tilt)) rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  }
}

/* Minimal Hover Frame Specular */
.wall-painting-unit:hover .canvas-image {
  transform: scale(1.015) translateZ(0);
}

/* A per-plate "floor reflection" used to live here: a blurred navy ellipse floating
   ~24px below every canvas. In this cream room it read as a cream bar under each
   painting, so it is gone. The .canvas-shadow-layer above already grounds the plates. */

/* Big, obvious "open this piece" target in the middle of the plate. The span is
   decorative — the whole plate is the real link — so it can never swallow a
   drag or end up as the only clickable sliver. */
.canvas-open-btn {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 64px;
  height: 64px;
  margin: -32px 0 0 -32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.45);
  backdrop-filter: blur(14px) saturate(180%);
  -webkit-backdrop-filter: blur(14px) saturate(180%);
  color: var(--text-main);
  /* Ring as an inset shadow rather than a 1px border — a hairline border on a
     scaled, composited circle flickers while it animates. */
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.7),
    0 10px 26px rgba(1, 35, 80, 0.22);
  opacity: 0;
  transition: opacity 0.24s ease, transform 0.24s cubic-bezier(0.16, 1, 0.3, 1);
  pointer-events: none;
  z-index: 3;
}

.canvas-open-btn svg {
  width: 26px;
  height: 26px;
}

/* Hover is the only thing that reveals it on pointer devices — it no longer pops
   up on whichever painting happens to be centred as you scroll past. */
.wall-painting-unit:hover .canvas-open-btn {
  opacity: 1;
  transform: scale(1.08) translateZ(0);
  /* Promoted only for as long as it is actually animating. */
  will-change: opacity, transform;
}

[data-theme="dark"] .canvas-open-btn {
  background: rgba(4, 25, 56, 0.6);
  box-shadow:
    inset 0 0 0 1px rgba(18, 55, 109, 0.85),
    0 10px 26px rgba(0, 0, 0, 0.4);
}

/* Touch devices: the target is NOT always on — it pops up only while a finger is
   actually on a painting (see the is-touched class set by js/gallery.js). */
@media (hover: none) {
  .canvas-open-btn {
    width: 72px;
    height: 72px;
    margin: -36px 0 0 -36px;
  }

  .wall-painting-unit.is-touched .canvas-open-btn {
    opacity: 1;
    transform: scale(1.06) translateZ(0);
    will-change: opacity, transform;
  }
}

/* Active Piece Curatorial HUD Indicator — a bounded glass plate rather than a
   gradient scrim, so its contrast can never bleed up onto the artwork above. */
.wall-active-meta {
  position: absolute;
  /* 16px gap + 58px shell + 22px clearance is today's 96px, but it now tracks the
     filmstrip's own height (it scales with the viewport) and the home-indicator inset,
     so the HUD can never end up sitting on the dock. */
  bottom: calc(var(--dock-inset) + var(--filmstrip-shell-h) + var(--hud-clearance));
  left: 50%;
  transform: translateX(-50%);
  text-align: center;
  /* An abs-positioned box with `left: 50%` and auto width shrink-to-fits into the
     space remaining to its right — half the stage — so on phones every title wrapped
     into a narrow column and left a stub on the last line ("(Composition" / "I)").
     max-content lets the plate size to its own text instead; the cap is what stops an
     extreme title from ever running off the viewport. The transform still centres it,
     and the plate is bottom-anchored, so the extra width only ever buys shorter lines. */
  width: max-content;
  max-width: min(92vw, 620px);
  pointer-events: none;
  z-index: 30;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 9px 18px;
  border-radius: 11px;
  background: rgba(255, 255, 255, 0.45);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.7);
  box-shadow: 0 12px 32px rgba(1, 35, 80, 0.1);
  overflow: hidden;
}

/* Deliberate inversion, not a bug: in the dark room this card flips to a pale plate so
   the active piece's title keeps its contrast in low light. Same idiom as
   .residency-quote-card (home.css) and .artist-modal-card (artists.css) — pale surface,
   #e4e0ba border, the light theme's ink. Do not "fix" this back to dark glass. */
[data-theme="dark"] .wall-active-meta {
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border-color: #e4e0ba;
  box-shadow: 0 16px 36px rgba(0, 0, 0, 0.35);
}

[data-theme="dark"] .wall-active-meta .meta-title {
  color: #012350;
}

[data-theme="dark"] .wall-active-meta .meta-sub {
  color: #3a4b63;
}

[data-theme="dark"] .wall-active-meta .meta-artist {
  color: var(--accent-coral);
}

/* The plate's two text lines wrap the same way. `balance` splits a title into even
   lines so the last one is never a lone word or numeral, and break-word is only the
   backstop for a single word too long to fit the cap — without it the plate's
   `overflow: hidden` would clip that word rather than break it. */
.meta-artist,
.meta-title {
  text-wrap: balance;
  overflow-wrap: break-word;
}

.meta-artist {
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-coral);
}

.meta-title {
  font-family: var(--font-display);
  font-size: clamp(1.3rem, 2.5vw, 1.9rem);
  font-weight: 900;
  letter-spacing: 0.02em;
  color: var(--text-main);
  line-height: 1.02;
  margin: 1px 0 4px;
}

.meta-sub {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-muted);
}

.meta-status-pill {
  font-size: 0.6rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(2, 155, 189, 0.15);
  color: var(--accent-cyan);
}

.meta-status-pill.is-sold {
  background: rgba(242, 65, 93, 0.15);
  color: var(--accent-coral);
}

.meta-status-pill.is-reserved {
  background: rgba(254, 228, 81, 0.28);
  color: #012350;
  border: 1px solid rgba(254, 228, 81, 0.7);
  font-weight: 800;
}

/* Held / In-Cart Lockout State */
.wall-painting-unit.is-held {
  cursor: not-allowed !important;
  pointer-events: auto;
}

/* Scoped strictly to the plate so the badge above stays completely sharp */
.wall-painting-unit.is-held .canvas-frame-face {
  filter: grayscale(70%) brightness(0.62) blur(3px);
  opacity: 0.5;
}

.wall-painting-unit.is-held .canvas-open-btn {
  display: none !important;
}

/* Elevated floating badge above the blur layer: Zanzalu blue on yellow */
.wall-painting-unit.is-held::after {
  content: "RESERVED";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) translateZ(40px);
  background: #fee451;
  color: #012350;
  font-family: var(--font-body);
  font-size: 0.74rem;
  font-weight: 900;
  letter-spacing: 0.14em;
  padding: 7px 18px;
  border-radius: 999px;
  border: 1.5px solid rgba(1, 35, 80, 0.15);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
  pointer-events: none;
  z-index: 25;
  white-space: nowrap;
}

.filmstrip-thumb-btn.is-held {
  filter: grayscale(80%) brightness(0.5);
  opacity: 0.25 !important;
}

/* Docked Floor Filmstrip Scrubber: Locked rigidly on horizontal rails */
.docked-filmstrip-bar {
  position: absolute;
  bottom: var(--dock-inset);
  left: 50%;
  transform: translateX(-50%);
  z-index: 40;
  pointer-events: auto;
  touch-action: pan-x;
  width: auto;
  max-width: 94vw;
}

.filmstrip-shell {
  background: rgba(255, 255, 255, 0.45);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.7);
  border-radius: 12px;
  padding: 5px 6px;
  box-shadow: 0 12px 32px rgba(1, 35, 80, 0.12);
  display: flex;
  align-items: center;
  overflow: hidden;
  box-sizing: border-box;
}

[data-theme="dark"] .filmstrip-shell {
  background: rgba(4, 25, 56, 0.6);
  border-color: rgba(18, 55, 109, 0.8);
}

.filmstrip-track {
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: 90vw;
  overflow-x: auto;
  overflow-y: hidden; /* Lock vertical completely */
  overscroll-behavior: contain;
  touch-action: pan-x; /* Strict horizontal rail */
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

.filmstrip-track::-webkit-scrollbar {
  display: none;
}

.filmstrip-thumb-btn {
  position: relative;
  height: var(--filmstrip-h);
  width: calc(var(--filmstrip-h) * 0.8261);
  flex-shrink: 0;
  border-radius: 4px;
  overflow: hidden;
  background: #111;
  background-size: cover;
  background-position: center;
  padding: 0;
  cursor: pointer;
  opacity: 0.4;
  box-shadow: inset 0 0 0 1px transparent;
  transition: opacity 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}

/* Floating Quick-Add Popover directly ABOVE the filmstrip thumbnail */
.filmstrip-quick-add-popover {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 0;
  transform: translateX(-50%) translateY(6px);
  opacity: 0;
  pointer-events: none;
  z-index: 70;
  /* Transition opacity & transform only — left is locked directly to thumbnail frame-by-frame */
  transition: opacity 0.18s ease, transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
  display: flex;
  align-items: center;
  justify-content: center;
  will-change: opacity, transform;
}

.filmstrip-quick-add-popover.is-visible {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(-50%) translateY(0);
}

.btn-filmstrip-popover {
  background: var(--accent-coral);
  color: #ffffff;
  border: none;
  padding: 5px 12px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  transition: background-color 0.15s ease, transform 0.15s ease;
}

.btn-filmstrip-popover:hover {
  background: #fee451;
  color: #012350;
  transform: scale(1.06);
}

/* Sold State */
.btn-filmstrip-popover.is-sold {
  background: #f2415d;
  color: #ffffff;
  cursor: not-allowed;
  box-shadow: 0 4px 14px rgba(242, 65, 93, 0.35);
  transform: none !important;
}

/* Reserved State */
.btn-filmstrip-popover.is-reserved {
  background: rgba(1, 20, 46, 0.92);
  color: #fee451;
  border: 1px solid rgba(254, 228, 81, 0.4);
  cursor: not-allowed;
  transform: none !important;
}

/* Already in Cart */
.btn-filmstrip-popover.is-incart {
  background: rgba(2, 155, 189, 0.9);
  color: #ffffff;
  cursor: default;
  transform: none !important;
}

/* Quick Add Device Onboarding Toast (Wrapped & Mobile-Constrained) */
.gallery-tip-toast {
  position: fixed;
  top: 80px;
  left: 50%;
  transform: translateX(-50%) translateY(-14px);
  background: var(--glass-surface);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  border: 1px solid var(--accent-cyan);
  color: var(--text-main);
  padding: 8px 18px;
  border-radius: 20px;
  font-size: 0.76rem;
  font-weight: 700;
  line-height: 1.35;
  text-align: center;
  text-wrap: balance;
  width: max-content;
  max-width: min(88vw, 380px);
  box-shadow: 0 12px 32px rgba(1, 35, 80, 0.18);
  z-index: 300;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.gallery-tip-toast.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

.filmstrip-thumb-btn img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.filmstrip-thumb-btn:hover {
  opacity: 0.85;
  transform: scale(1.06) translateZ(0);
  will-change: transform;
}

.filmstrip-thumb-btn.is-active {
  opacity: 1;
  box-shadow:
    inset 0 0 0 1px var(--accent-coral),
    0 0 10px rgba(242, 65, 93, 0.4);
  transform: scale(1.12) translateZ(0);
  will-change: transform;
}

/* Fetch failure / no-data state */
.wall-error-state {
  position: absolute;
  inset: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
  padding: 24px;
}

.wall-error-title {
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 800;
  color: var(--text-main);
}

.wall-error-detail {
  font-size: 0.85rem;
  color: var(--text-muted);
  max-width: 460px;
}

/* Short windows: shrink the plates so the HUD never lands on top of a painting. */
@media (max-height: 640px) {
  .spatial-gallery-stage {
    --plate-h: min(clamp(180px, 36dvh, 420px), calc(100dvh - var(--stage-pad-top) - var(--stage-pad-bottom)));
    --floor-line: clamp(130px, 18dvh, 240px);
  }
}

@media (max-width: 768px) {
  .spatial-gallery-stage {
    --plate-h: min(48dvh, calc(100dvh - var(--stage-pad-top) - var(--stage-pad-bottom)));
    --floor-line: 18dvh;
    --filmstrip-h: clamp(34px, 6.6dvh, 44px);
    --hud-clearance: 40px;
  }
  .overhead-light-fixture {
    top: 60px;
    /* A bit narrower than the beam, so the bar reads as the lamp itself
       rather than as the full spread of the light. */
    width: min(300px, 60vw);
  }
  .spotlight-cone {
    /* Mouth matches the bar's edges (60vw inside an 88vw beam = ±34%), so the beam
       is a real cone again instead of a rectangle. */
    clip-path: polygon(16% 0%, 84% 0%, 100% 100%, 0% 100%);
  }
  .wall-active-meta {
    /* The dock tokens already resolve to today's 112px here; portrait mobile only
       needs its own padding and type scale. Tighter than the desktop plate on purpose:
       this card hangs in front of the wall, so every pixel of height it saves is a
       pixel of painting a short phone gets back. */
    padding: 8px 14px;
    gap: 1px;
  }
  .meta-artist {
    font-size: 0.72rem;
    line-height: 1.25;
  }
  .meta-title {
    font-size: clamp(1.45rem, 5.2vw, 2rem);
    margin: 1px 0 3px;
  }
  .meta-status-pill {
    font-size: 0.64rem;
    padding: 1px 8px;
  }
  /* Flatter on small screens so the plane still reads as a floor, not a ramp. */
  .gallery-floor {
    transform: rotateX(76deg);
  }
}

/* --- Phones in landscape: the screen is short, so everything compresses --- */
@media (max-width: 980px) and (orientation: landscape) {
  .spatial-gallery-stage {
    --stage-pad-top: 46px;
    --stage-pad-bottom: 162px;
    --plate-h: min(clamp(130px, 46dvh, 300px), calc(100dvh - var(--stage-pad-top) - var(--stage-pad-bottom)));
    --floor-line: clamp(110px, 24dvh, 200px);
    --filmstrip-h: clamp(30px, 9.6dvh, 36px);
    --dock-gap: 10px;
    --hud-clearance: 16px;
  }
  /* Plates get the whole height minus a slim band for the HUD, so they stop
     being clipped by the top edge. The band itself comes from the tokens above. */
  .wall-viewport {
    padding: var(--stage-pad-top) 0 var(--stage-pad-bottom);
  }
  /* The fixture rides at the ceiling line with no beam: there is no wall space
     left between the nav and the paintings for a cone to fall through. */
  .overhead-light-fixture {
    top: 20px;
    width: min(260px, 34vw);
  }
  .spotlight-cone {
    display: none;
  }
  .wall-active-meta {
    padding: 6px 12px;
    gap: 1px;
    border-radius: 10px;
  }
  .meta-artist {
    font-size: 0.6rem;
    letter-spacing: 0.1em;
    line-height: 1.25;
  }
  .meta-title {
    font-size: clamp(1rem, 2.6vw, 1.35rem);
    margin: 1px 0 2px;
  }
  .meta-status-pill {
    font-size: 0.54rem;
    padding: 0 6px;
  }
  /* Header furniture scaled down for a short screen. */
  .gallery-body .site-brand-lockup {
    top: 12px;
    left: 12px;
  }
  .gallery-body .site-brand-lockup .brand-wordmark-mobile {
    font-size: 1.1rem;
  }
  .gallery-body .mobile-glass-burger {
    width: 34px;
    height: 34px;
    top: 10px;
    right: 10px;
  }
  .gallery-body .zanzalu-emblem-svg {
    width: 20px;
    height: 20px;
    flex: 0 0 20px;
  }
  .gallery-body .burger-close-x {
    font-size: 1.4rem;
  }
}

/* --- Device-capability tiers ---
   data-perf is written on <html> by the inline probe in gallery.html's head: "medium"
   or "low", with the attribute absent meaning the full treatment. These rules only give
   up work a weak GPU cannot afford — the room, the beam and the hanging paintings all
   still read the same. Anything not matched here is untouched, so "low" is additive. */
[data-perf="medium"] .wall-painting-unit:not(.is-active),
[data-perf="low"] .wall-painting-unit:not(.is-active) {
  /* The base rule runs a filter over all 18 inactive plates every frame just to read
     slightly dimmer. Opacity alone reads the same at a fraction of the cost — but only
     for the plates that are *meant* to be dim: this rule used to match the active plate
     too and, sitting later in the file at equal specificity, silently overrode its
     opacity: 1. That is why plates on a phone stayed dim after being scrolled onto. */
  filter: none;
  opacity: 0.55;
}

/* A backdrop-filter is a live blur of everything moving behind the surface; on these
   surfaces the tier swaps it for alpha. The tier must NOT re-tint the HUD card: in the
   dark room that card is the sanctioned pale plate (see its dark-mode rule above), and
   tinting it dark here would leave its hard-coded navy ink on navy. The dock, by
   contrast, follows the room in both themes, so it can simply go opaque. */
[data-perf="medium"] .filmstrip-shell,
[data-perf="medium"] .wall-active-meta,
[data-perf="medium"] .canvas-open-btn,
[data-perf="low"] .filmstrip-shell,
[data-perf="low"] .wall-active-meta,
[data-perf="low"] .canvas-open-btn {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

[data-perf="medium"] .wall-active-meta,
[data-perf="low"] .wall-active-meta {
  /* In the light room the card is a 45%-white glass that leaned on its blur to hold the
     text apart from the wall; without the blur it has to carry itself. Values are the
     light-theme pair, and dark mode re-asserts its own below. */
  background: rgba(255, 255, 255, 0.82);
}

[data-perf="medium"][data-theme="dark"] .wall-active-meta,
[data-perf="low"][data-theme="dark"] .wall-active-meta {
  background: rgba(255, 255, 255, 0.95);
}

[data-perf="medium"] .filmstrip-shell,
[data-perf="low"] .filmstrip-shell {
  background: rgba(252, 250, 240, 0.94);
}

[data-perf="medium"][data-theme="dark"] .filmstrip-shell,
[data-perf="low"][data-theme="dark"] .filmstrip-shell {
  background: rgba(4, 25, 56, 0.95);
}

[data-perf="medium"] .canvas-open-btn,
[data-perf="low"] .canvas-open-btn {
  background: rgba(255, 255, 255, 0.85);
}

[data-perf="medium"][data-theme="dark"] .canvas-open-btn,
[data-perf="low"][data-theme="dark"] .canvas-open-btn {
  background: rgba(4, 25, 56, 0.9);
}

/* "low" also gives up the beam, the floor grain and the second layer of plate shadow. */
[data-perf="low"] .spotlight-cone {
  display: none;
}

[data-perf="low"] .gallery-floor {
  background-image: linear-gradient(180deg, #6a6350 0%, #857e66 26%, #a49b7d 58%, #c6bc99 100%);
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

[data-perf="low"][data-theme="dark"] .gallery-floor {
  background-image: linear-gradient(180deg, #010610 0%, #04132c 55%, #08203f 100%);
}

[data-perf="low"] .canvas-shadow-layer {
  box-shadow: 0 18px 30px rgba(1, 35, 80, 0.22);
}

[data-perf="low"][data-theme="dark"] .canvas-shadow-layer {
  box-shadow: 0 20px 34px rgba(0, 0, 0, 0.6);
}

@media (prefers-reduced-motion: reduce) {
  .wall-track, .wall-painting-unit {
    transition: none !important;
  }
  /* The dock and the open button are the only other things that move on their own.
     js/gallery.js drops the wall's momentum, its glide easing and the mouse tilt in
     this mode as well. */
  .canvas-open-btn,
  .filmstrip-thumb-btn {
    transition: none !important;
  }
}