/* ZOA Explore — shared rendering core (viewer + editor)
   This file is the single source of visual truth for the stage, scene
   images, hotspot markers, and the floating panel shell. Both the public
   viewer and the internal editor include this file unmodified — if the
   editor should ever look exactly like the viewer, this is why it does. */

:root{
  --bg:#131310;
  --surface:#1c1c17;
  --surface-2:#242420;
  --border:#3a3a33;
  --text:#eee9dd;
  --text-muted:#9c988b;
  --accent:#c9a227;
  --accent-dim:#8a7020;
  --scale:1;
  --marker-bg-rgb:255,255,255;
  --marker-bg-opacity:0.88;
  --marker-icon-color:#20201c;
  --marker-name-color:#17170f;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0; height:100%; overflow:hidden;}
body{
  background:var(--bg);
  color:var(--text);
  font-family:'Inter',system-ui,sans-serif;
}

.stage{
  position:relative;
  width:100%;
  height:100vh;
  height:100dvh;
  background:#0d0d0a;
  overflow:hidden;
}
/* ---- a strip you turn by hand ----
   `touch-action:none` is load-bearing, not decoration: without it a finger drag
   is claimed by the browser as a page scroll before the first pointermove
   arrives, and the building never moves on a phone. The cursor is the only
   thing telling a mouse user the picture is draggable at all, so it is on the
   stage from the moment such a sequence is entered, not only while dragging. */
.stage.freeSpin{ cursor:grab; touch-action:none; }
.stage.freeSpin.spinning{ cursor:grabbing; }
/* a drag must not paint the whole page blue on its way past */
.stage.freeSpin.spinning, .stage.freeSpin.spinning *{ user-select:none; }

/* ---- a picture you pull sideways to fly to the next scene ----
   The same affordance for the projects that have scenes instead of a strip to
   turn. `pan-y pinch-zoom` rather than `none`: a sideways pull is ours, but a
   finger going up or down is the visitor scrolling and a pinch is them looking
   closer, and taking either of those hostage would be a poor price for a
   gesture that only ever reads sideways. */
.stage.sceneDrag{ cursor:grab; touch-action:pan-y pinch-zoom; }
.stage.sceneDrag.spinning{ cursor:grabbing; }
.stage.sceneDrag.spinning, .stage.sceneDrag.spinning *{ user-select:none; }
.stage.sceneDrag img{ -webkit-user-drag:none; user-drag:none; }
.loadError{
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  max-width:420px; text-align:center; padding:0 24px;
  color:#eee9dd; font-family:'IBM Plex Mono',monospace; font-size:13px; line-height:1.6;
}
/* This is the actual image's coordinate space: always exactly matches the
   render's native aspect ratio, and is only ever cropped by .stage's
   overflow:hidden — never stretched or reflowed independently of it.
   Everything positioned by percentage (scenes, hotspots, transition video)
   lives inside here, so a hotspot's x/y always lands on the same real point
   in the image regardless of the browser window's aspect ratio. */
.stageInner{
  position:absolute; top:50%; left:50%;
  transform:translate(-50%,-50%);
  aspect-ratio:1920/1072;
  min-width:100%; min-height:100%;
}
.stageInner .scene{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  opacity:0;
  transition:opacity 550ms ease;
}
.stageInner .scene.active{opacity:1;}
/* Unit shapes sit between the render and the markers: over the building, under
   anything clickable.

   These z-indexes are load-bearing. Everything in .stageInner is absolutely
   positioned, so without them the paint order is DOM order — and the scene
   images are inserted with insertBefore(el, hotspotLayer), which puts them
   AFTER the unit layer. The shapes were rendering perfectly and then being
   covered by the very picture they belong to. */
#unitLayer, #previewUnitLayer{ position:absolute; inset:0; z-index:1; pointer-events:none; }
.zUnitSvg{ position:absolute; inset:0; width:100%; height:100%; display:block; pointer-events:none; }

/* Drawn areas sit between the unit shapes and the markers: an area outlines a
   whole building and its own pin sits on top of it, so the pin wins the click.
   Everything below mirrors the unit rules and reuses the same custom-property
   names — the two are one visual language, and a single set of names is easier
   to keep honest than two that drift apart. */
#areaLayer, #previewAreaLayer{ position:absolute; inset:0; z-index:1; pointer-events:none; }
.zAreaSvg{ position:absolute; inset:0; width:100%; height:100%; display:block; pointer-events:none; }
.zAreaSvg polygon.zAreaShape{
  fill:var(--u-fill);
  fill-opacity:var(--u-fill-opacity);
  stroke:var(--u-stroke);
  stroke-width:var(--u-stroke-width);
  transition:fill-opacity 160ms ease, stroke-width 160ms ease;
}
/* only an area that does something takes the pointer; a preview is scenery */
.zAreaSvg.interactive polygon.zAreaShape{ pointer-events:auto; cursor:pointer; }
.zAreaSvg.interactive polygon.zAreaShape:hover{
  fill-opacity:var(--u-fill-opacity-hover);
  stroke-width:var(--u-stroke-width-hover);
}
.zAreaSvg polygon.zAreaErode{
  fill:#fff;
  stroke:#000;
  stroke-width:var(--u-inset, 0);
  stroke-linejoin:round;
  transition:stroke-width 160ms ease;
}
.zAreaSvg g.lift polygon.zAreaErode{ stroke-width:var(--u-inset-hover, 0); }
@media (prefers-reduced-motion: reduce){
  .zAreaSvg polygon{ transition:none; }
}

/* #hotspotLayer is a full-bleed box sitting above the unit shapes, so until it
   was made transparent it was the hit target for the ENTIRE stage — the shapes
   could never see a pointer and :hover never fired. Marker clicks had always
   worked only because the event bubbled up to the stage from here; now the
   markers themselves take the hits, which is what was meant all along. */
#hotspotLayer{ position:absolute; inset:0; z-index:2; pointer-events:none; }
.hotspotPos{ pointer-events:auto; }

/* The layer stays transparent to the pointer and only the shapes themselves
   take hits, so a marker on top always wins and a click on bare stage still
   reaches the stage. Nothing here calls stopPropagation.

   Scoped to .zUnitShape, never to `polygon`: a negative outline thickness puts
   a second polygon inside a <mask>, and that one has to stay flat white with a
   black stroke. Painting it with the unit's colours would silently produce a
   mask of the wrong luminance and the shape would vanish. */
.zUnitSvg polygon.zUnitShape{
  pointer-events:auto;
  /* A clickable shape says so, the same way .zAreaShape does. It only became
     visible on a free-spin sequence, where the stage asks for `grab` and every
     apartment inherited it — the building looked draggable and nothing looked
     clickable. While `.moving` the shape is inert and `grab` correctly shows
     through, because then the drag really is all that is on offer. */
  cursor:pointer;
  fill:var(--u-fill);
  fill-opacity:var(--u-fill-opacity);
  stroke:var(--u-stroke);
  stroke-width:var(--u-stroke-width);
  transition:fill-opacity 160ms ease, stroke-width 160ms ease;
}
/* hover adds to both, rather than replacing them, so one pair of numbers works
   for every unit whatever its own base values are */
.zUnitSvg polygon.zUnitShape:hover{
  fill-opacity:var(--u-fill-opacity-hover);
  stroke-width:var(--u-stroke-width-hover);
}

/* ---- negative outline thickness: a transparent gap instead of a line ----
   White keeps, black removes. The stroke straddles the polygon's edge, so a
   stroke of 2n eats n pixels of the inside — which is the gap. Non-scaling, so
   the gap is n screen pixels whatever the viewBox is doing.

   The hover boost cannot reach this from CSS alone. `g:has(> .zUnitShape:hover)`
   MATCHES — the selector engine agrees, element.matches() returns true — but
   Chrome does not restyle elements inside a <mask> subtree in response to it,
   so the computed stroke-width never moves. One delegated pointerover on the
   svg adds .lift to the group instead; that is a single listener per layer,
   not one per shape, and it drives the list-row highlight too. */
.zUnitSvg polygon.zUnitErode{
  fill:#fff;
  stroke:#000;
  stroke-width:var(--u-inset, 0);
  stroke-linejoin:round;
  transition:stroke-width 160ms ease;
}
.zUnitSvg g.lift polygon.zUnitErode{
  stroke-width:var(--u-inset-hover, 0);
}
/* While the strip is playing the shapes travel with it, but nothing on them
   should respond: a hover highlight chasing the pointer across a moving
   building reads as a glitch, and the transition on fill-opacity would fight
   the per-frame updates. */
#unitLayer.moving .zUnitSvg polygon,
#previewUnitLayer.moving .zUnitSvg polygon{
  pointer-events:none;
  transition:none;
}
/* the visitor's own switch, independent of theme.units.enabled */
#unitLayer.hidden, #previewUnitLayer.hidden{ display:none; }

@media (prefers-reduced-motion: reduce){
  .zUnitSvg polygon{ transition:none; }
}

/* ---- the hover card ----
   Everything a visitor would otherwise open a panel to learn, shown where they
   are already looking. Positioned by createUnitTip in host coordinates, so the
   only thing the stylesheet decides is what it looks like — every value here
   that a project might want to change comes in as a custom property from the
   Units menu.

   pointer-events:none is load-bearing rather than tidy: the card sits over the
   render, and a card that took the pointer would put itself between the
   pointer and the flat it describes — the flat would lose its hover, the card
   would be told to hide, and the two would flicker against each other for as
   long as the pointer stayed still. */
.zUnitTip{
  position:absolute; left:0; top:0;
  z-index:8;
  width:var(--ut-w, 300px);
  max-width:calc(100% - 16px);
  box-sizing:border-box;
  padding:var(--ut-pad, 16px);
  background:var(--ut-bg, #fff);
  color:var(--ut-ink, #141410);
  border:var(--ut-border, 0) solid var(--ut-border-color, transparent);
  border-radius:var(--ut-radius, 14px);
  box-shadow:var(--ut-shadow, 0 14px 38px rgba(0,0,0,0.22));
  font-family:'IBM Plex Mono', monospace;
  pointer-events:none;
  opacity:0;
  transition:opacity var(--ut-fade, 120ms) ease;
  display:flex; flex-direction:column; gap:10px;
}
.zUnitTip.on{ opacity:1; }

.zUnitTipHead{ display:flex; align-items:center; justify-content:space-between; gap:10px; }
.zUnitTipCode{
  font-size:var(--ut-code, 20px); font-weight:600; line-height:1.1;
  letter-spacing:-0.01em; word-break:break-word;
}
.zUnitTipStatus{
  flex:0 0 auto;
  font-size:var(--ut-label, 10.5px); font-weight:600;
  letter-spacing:0.06em; text-transform:uppercase;
  padding:5px 10px; border-radius:999px; white-space:nowrap;
}
.zUnitTipName{ font-size:var(--ut-label, 10.5px); color:var(--ut-muted, #6f6b60); margin-top:-6px; }

/* The picture's box is reserved from the configured height whether or not the
   file has arrived, so the card is its final size from the first frame — a card
   that grows as its plan lands drags the numbers out from under the eye that
   is already reading them. */
.zUnitTipPlan{
  height:var(--ut-plan-h, 150px);
  background:var(--ut-plan-bg, transparent);
  border-radius:calc(var(--ut-radius, 14px) * 0.5);
  display:flex; align-items:center; justify-content:center;
  overflow:hidden;
}
.zUnitTipPlan img{
  max-width:100%; max-height:100%;
  width:auto; height:auto;
  object-fit:contain;
  display:block;
}

/* One row, spread evenly — the stats read as a band rather than as a list, so
   two of them look deliberate and four still fit. */
.zUnitTipStats{ display:flex; gap:12px; justify-content:space-between; }
.zUnitTipStat{ display:flex; flex-direction:column; align-items:center; gap:2px; flex:1 1 0; min-width:0; }
.zUnitTipStatKey{
  font-size:var(--ut-label, 10.5px); color:var(--ut-muted, #6f6b60);
  letter-spacing:0.04em; white-space:nowrap;
}
.zUnitTipStatVal{
  font-size:var(--ut-value, 15px); font-weight:600;
  font-variant-numeric:tabular-nums; white-space:nowrap;
}
.zUnitTipNotes{ font-size:var(--ut-label, 10.5px); color:var(--ut-muted, #6f6b60); line-height:1.5; }

@media (prefers-reduced-motion: reduce){
  .zUnitTip{ transition:none; }
}
/* A finger has no hover: on touch the card would appear under the tap and be
   gone before it could be read, over the flat the tap was meant to open. The
   tap opens the panel, which is the whole card and more. */
@media (hover: none){
  .zUnitTip{ display:none; }
}

/* ---- flat chooser toggle (top-right) ---- */
.zUnitToggleHost{
  position:absolute; top:16px; right:18px;
  z-index:6;
  display:flex; justify-content:flex-end;
}
.zUnitToggle{
  display:flex; flex-direction:column; align-items:flex-end; gap:7px;
  font-family:'IBM Plex Mono', monospace;
}
.zUnitBtn{
  display:flex; align-items:center; gap:8px;
  padding:7px 11px;
  background:rgba(20,20,16,0.78);
  backdrop-filter:blur(3px);
  border:0.5px solid rgba(255,255,255,0.22);
  border-radius:999px;
  color:#eee9dd; font-family:inherit; font-size:11.5px; letter-spacing:0.04em;
  text-transform:uppercase;
  cursor:pointer;
  transition:background 180ms ease, border-color 180ms ease, color 180ms ease;
}
.zUnitBtn:hover{ border-color:rgba(255,255,255,0.45); }
.zUnitBtn svg{ width:13px; height:13px; flex:0 0 13px; fill:currentColor; opacity:0.75; }
/* the state light: lit and accented when on, dim and hollow when off */
.zUnitDot{
  width:7px; height:7px; flex:0 0 7px; border-radius:50%;
  background:transparent;
  border:1px solid rgba(238,233,224,0.5);
  transition:background 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
}
.zUnitToggle.on .zUnitBtn{ color:#141410; background:rgba(201,162,39,0.92); border-color:rgba(201,162,39,0.92); }
.zUnitToggle.on .zUnitBtn svg{ opacity:0.9; }
.zUnitToggle.on .zUnitDot{
  background:#141410; border-color:#141410;
  box-shadow:0 0 0 2px rgba(20,20,16,0.18);
}

/* the legend doubles as the numbers a visitor actually wants; it only exists
   while the overlay is on, since off there is nothing for it to label */
.zUnitLegend{
  display:flex; flex-wrap:wrap; justify-content:flex-end; gap:5px 10px;
  padding:7px 11px;
  background:rgba(20,20,16,0.7);
  backdrop-filter:blur(3px);
  border:0.5px solid rgba(255,255,255,0.16);
  border-radius:10px;
  font-size:10.5px; color:#eee9dd;
  opacity:0; transform:translateY(-4px);
  pointer-events:none;
  transition:opacity 200ms ease, transform 200ms ease;
}
.zUnitToggle.on .zUnitLegend{ opacity:1; transform:none; }
.zUnitKey{ display:flex; align-items:center; gap:5px; }
.zUnitKey i{
  width:9px; height:9px; flex:0 0 9px; border-radius:2px;
  border:1px solid; display:inline-block;
}
.zUnitKey b{ font-weight:600; }
.zUnitKey span{ opacity:0.7; }

@media (max-width: 560px){
  .zUnitToggleHost{ top:12px; right:12px; }
  .zUnitBtn .zUnitBtnLabel{ display:none; }
  .zUnitLegend{ font-size:10px; }
}
@media (prefers-reduced-motion: reduce){
  .zUnitBtn, .zUnitDot, .zUnitLegend{ transition:none; }
}

/* ---- flat chooser ------------------------------------------------------
   The stage and the unit list are flex siblings inside .stageWrap, so opening
   the list genuinely narrows the render instead of covering it — which is the
   point: closing it gives the picture the width back. The render re-crops
   itself (object-fit:cover) and the shapes follow, because they reproduce that
   same crop, so nothing has to be recomputed in script.

   .zFlatChooser is display:contents, so its children take part in that flex
   layout directly: the list is the flex item, everything else is absolutely
   positioned against .stageWrap. */
.stageWrap{ position:relative; display:flex; width:100%; height:100vh; height:100dvh; overflow:hidden; }
.stageWrap > .stage{ flex:1 1 auto; width:auto; min-width:0; height:100%; }
.zFlatChooser{ display:contents; }

/* the button rail, top-left */
.zFcRail{
  position:absolute; top:16px; left:18px; z-index:7;
  display:flex; flex-direction:column; gap:8px;
}
/* Both button colours are the project's to set (--zfc-rail-bg / -on), and the
   icon colour is derived from each rather than being a third setting, so a dark
   icon can never land on a dark button. Unset, these are what the rail always
   looked like: near-black translucent, lit in the accent. */
.zFcRailBtn{
  width:38px; height:38px; padding:0;
  display:flex; align-items:center; justify-content:center;
  background:color-mix(in srgb, var(--zfc-rail-bg, #141410) 78%, transparent);
  backdrop-filter:blur(3px);
  border:0.5px solid rgba(255,255,255,0.22);
  border-radius:10px;
  color:var(--zfc-rail-fg, #eee9dd); cursor:pointer;
  transition:background 180ms ease, border-color 180ms ease, color 180ms ease, transform 180ms ease;
}
.zFcRailBtn svg{ width:17px; height:17px; fill:currentColor; opacity:0.8; }
.zFcRailBtn:hover{ border-color:rgba(255,255,255,0.5); transform:translateX(1px); }
.zFcRailBtn.on{
  background:var(--zfc-rail-bg-on, var(--accent));
  border-color:var(--zfc-rail-bg-on, var(--accent));
  color:var(--zfc-rail-fg-on, #141410);
}
.zFcRailBtn.on svg{ opacity:1; }
/* a dot on the funnel whenever the filter is actually holding something back,
   so a narrowed view is never a mystery with the panel closed */
.zFcRailBtn[data-act="filter"]{ position:relative; }
.zFcRailBtn[data-act="filter"].narrowed::after{
  content:''; position:absolute; top:5px; right:5px;
  width:6px; height:6px; border-radius:50%;
  background:var(--zfc-rail-bg-on, var(--accent));
  box-shadow:0 0 0 2px color-mix(in srgb, var(--zfc-rail-bg, #141410) 80%, transparent);
}
.zFcRailBtn.on[data-act="filter"].narrowed::after{
  background:var(--zfc-rail-fg-on, #141410); box-shadow:none;
}
/* with the overlay off there is nothing to filter or list */
.zFlatChooser.unitsOff .zFcRailBtn[data-act="filter"],
.zFlatChooser.unitsOff .zFcRailBtn[data-act="list"]{ opacity:0.35; pointer-events:none; }
.zFlatChooser.empty .zFcRail{ display:none; }

/* ---- two winks, every few seconds, until the pointer comes near ----
   Three small dark squares in a corner read as decoration, and the filter and
   the unit list — the two things that make this a chooser rather than a
   picture — were being missed entirely. A single pair of winks was easy to
   miss too: a visitor looking at the building at that moment never saw it.
   So the pair repeats on a long cycle, mostly silence, and the JS stops it the
   moment the pointer is anywhere near. Staggered down the rail so it reads as
   one gesture arriving, not as three buttons flashing at each other.

   The two winks take 1.24s of the 5s cycle; the rest is deliberately dead
   time, which is what keeps this a nudge rather than an alarm. */
.zFcRail.wink .zFcRailBtn{ animation:zFcRailWink 5s ease-in-out infinite; }
.zFcRail.wink .zFcRailBtn:nth-child(2){ animation-delay:70ms; }
.zFcRail.wink .zFcRailBtn:nth-child(3){ animation-delay:140ms; }
@keyframes zFcRailWink{
  0%{ transform:translateX(0); border-color:rgba(255,255,255,0.22); box-shadow:0 0 0 0 rgba(255,255,255,0); }
  5.6%{ transform:translateX(4px); border-color:rgba(255,255,255,0.85); box-shadow:0 0 0 4px rgba(255,255,255,0.14); }
  12.4%{ transform:translateX(0); border-color:rgba(255,255,255,0.22); box-shadow:0 0 0 0 rgba(255,255,255,0); }
  18%{ transform:translateX(4px); border-color:rgba(255,255,255,0.85); box-shadow:0 0 0 4px rgba(255,255,255,0.14); }
  24.8%,100%{ transform:translateX(0); border-color:rgba(255,255,255,0.22); box-shadow:0 0 0 0 rgba(255,255,255,0); }
}
@media (prefers-reduced-motion: reduce){
  .zFcRail.wink .zFcRailBtn{ animation:none; }
}

/* the title shares this corner — step it past the rail rather than under it.
   Only when the rail is really there: a project without units keeps the
   title where it has always been. */
.zFcActive .onImageTitle{ left:70px; }

/* the filter, opening beside the rail */
.zFcFilter{
  position:absolute; top:16px; left:64px; z-index:8;
  width:280px; max-width:calc(100% - 84px);
  padding:12px 14px 14px;
  background:rgba(20,20,16,0.9);
  backdrop-filter:blur(6px);
  border:0.5px solid rgba(255,255,255,0.2);
  border-radius:12px;
  color:#eee9dd;
  font-family:'IBM Plex Mono', monospace; font-size:11.5px;
  opacity:0; transform:translateX(-6px) scale(0.98);
  transform-origin:top left;
  pointer-events:none;
  transition:opacity 180ms ease, transform 180ms cubic-bezier(.2,.9,.3,1.1);
}
.zFcFilter.open{ opacity:1; transform:none; pointer-events:auto; }
.zFcFilterHead{
  display:flex; align-items:baseline; justify-content:space-between;
  text-transform:uppercase; letter-spacing:0.06em; font-size:10.5px;
  color:rgba(238,233,224,0.65);
  margin-bottom:10px;
}
.zFcReset{
  background:none; border:none; padding:0;
  color:var(--accent); font-family:inherit; font-size:10.5px;
  letter-spacing:0.04em; cursor:pointer; text-transform:uppercase;
}
.zFcReset:hover{ text-decoration:underline; }
.zFcStatusRow{ display:flex; gap:6px; }
.zFcCheck{
  flex:1 1 0; min-width:0;
  display:flex; flex-direction:column; align-items:center; gap:3px;
  padding:7px 4px;
  border:0.5px solid rgba(255,255,255,0.16);
  border-radius:8px;
  cursor:pointer; text-align:center;
  transition:border-color 160ms ease, background 160ms ease, opacity 160ms ease;
  opacity:0.45;
}
.zFcCheck:has(input:checked){ opacity:1; border-color:rgba(255,255,255,0.36); background:rgba(255,255,255,0.05); }
.zFcCheck input{ position:absolute; opacity:0; width:0; height:0; }
.zFcCheck i{ width:10px; height:10px; border-radius:3px; border:1px solid; }
.zFcCheck b{ font-size:12px; font-weight:600; }
/* no text-transform anywhere a status name is shown: these are the project's
   own words now, and forcing case would mangle a translation */
.zFcCheck > span{ font-size:9.5px; opacity:0.75; }
.zFcSliders{ margin-top:12px; display:flex; flex-direction:column; gap:14px; }
.zFcFilter.noSliders .zFcSliders{ margin-top:0; }
.zFcFilterFoot{
  margin:12px 0 0; padding-top:9px;
  border-top:0.5px solid rgba(255,255,255,0.12);
  font-size:10px; letter-spacing:0.04em; color:rgba(238,233,224,0.55);
}

/* two-handled range */
.zFcRange{ display:block; }
.zFcRangeHead{
  display:flex; justify-content:space-between; align-items:baseline; gap:8px;
  margin-bottom:7px;
}
.zFcRangeLabel{ text-transform:uppercase; letter-spacing:0.06em; font-size:10px; color:rgba(238,233,224,0.6); }
.zFcRangeVal{ font-size:10.5px; color:rgba(238,233,224,0.9); text-align:right; }
.zFcRange.narrowed .zFcRangeVal{ color:var(--accent); }
.zFcRangeBody{ position:relative; height:20px; }
.zFcRangeTrack, .zFcRangeFill{
  position:absolute; top:50%; height:3px; border-radius:2px;
  transform:translateY(-50%); pointer-events:none;
}
.zFcRangeTrack{ left:0; right:0; background:rgba(255,255,255,0.18); }
.zFcRangeFill{ background:var(--accent); }
/* the inputs are stacked and transparent to the pointer; only the thumbs take
   hits, so the handle you press is the one that moves even where they overlap */
.zFcRangeInput{
  position:absolute; inset:0; width:100%; height:20px;
  margin:0; padding:0; background:none; border:none;
  -webkit-appearance:none; appearance:none;
  pointer-events:none;
}
.zFcRangeInput:focus{ outline:none; }
.zFcRangeInput::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none;
  pointer-events:auto;
  width:14px; height:14px; border-radius:50%;
  background:#eee9dd; border:2px solid #141410;
  box-shadow:0 1px 4px rgba(0,0,0,0.5);
  cursor:grab;
}
.zFcRangeInput::-moz-range-thumb{
  pointer-events:auto;
  width:14px; height:14px; border-radius:50%;
  background:#eee9dd; border:2px solid #141410;
  box-shadow:0 1px 4px rgba(0,0,0,0.5);
  cursor:grab;
}
.zFcRangeInput:focus-visible::-webkit-slider-thumb{ border-color:var(--accent); }
.zFcRangeInput:focus-visible::-moz-range-thumb{ border-color:var(--accent); }

/* the unit list — a flex sibling of the stage, so it takes width rather than
   covering it */
.zFcList{
  flex:0 0 auto;
  position:relative;
  width:var(--zfc-list-w, 0px);
  height:100%;
  background:var(--surface);
  border-left:1px solid var(--border);
  display:flex; flex-direction:column;
  overflow:hidden;
  transition:width 320ms cubic-bezier(.4,0,.2,1);
}
.zFlatChooser.resizing .zFcList{ transition:none; }
.zFcGrip{
  position:absolute; top:0; bottom:0; left:0; width:7px;
  cursor:col-resize; z-index:3;
}
.zFcGrip::after{
  content:''; position:absolute; top:0; bottom:0; left:3px; width:1px;
  background:transparent; transition:background 160ms ease;
}
.zFcGrip:hover::after, .zFlatChooser.resizing .zFcGrip::after{ background:var(--accent); }
.zFcListHead{
  flex:0 0 auto;
  display:flex; align-items:center; justify-content:space-between; gap:8px;
  padding:12px 12px 10px 14px;
  border-bottom:1px solid var(--border);
}
.zFcListCounts{
  display:flex; flex-wrap:wrap; gap:4px 10px;
  font-family:'IBM Plex Mono', monospace; font-size:10.5px; color:var(--text);
}
.zFcCount{ display:flex; align-items:center; gap:4px; }
.zFcCount i{ width:8px; height:8px; border-radius:2px; flex:0 0 8px; }
.zFcCount b{ font-weight:600; }
.zFcCount span{ opacity:0.6; }
.zFcListClose{
  flex:0 0 auto;
  width:22px; height:22px; padding:0;
  display:flex; align-items:center; justify-content:center;
  background:var(--surface-2); border:1px solid var(--border); border-radius:3px;
  color:var(--text-muted); font-family:'IBM Plex Mono', monospace; font-size:12px;
  cursor:pointer;
}
.zFcListClose:hover{ color:var(--accent); border-color:var(--accent); }

/* ---- the tools row: thumbnail size and sort ---- */
.zFcListTools{
  flex:0 0 auto;
  display:flex; align-items:center; gap:10px;
  padding:7px 12px 7px 14px;
  border-bottom:1px solid var(--border);
}
.zFcSizer{ flex:1 1 auto; display:flex; align-items:center; gap:4px; min-width:0; }
/* The mountains are buttons, not decoration. Given a real hit area — the
   glyphs themselves are 9 and 15px, which is nothing to aim at — and they must
   not grow or shrink with the slider between them. */
.zFcSizeStep{
  flex:0 0 auto;
  width:20px; height:20px; padding:0;
  display:flex; align-items:center; justify-content:center;
  background:none; border:1px solid transparent; border-radius:4px;
  cursor:pointer;
}
.zFcSizeStep:hover:not(:disabled){ background:var(--surface-2); border-color:var(--border); }
.zFcSizeStep:active:not(:disabled) .zFcSizeGlyph{ fill:var(--accent); }
/* at the end of its travel it says so, rather than going dead quietly */
.zFcSizeStep:disabled{ cursor:default; opacity:0.3; }
.zFcSizeGlyph{ flex:0 0 auto; fill:var(--text-muted); transition:fill 120ms ease; }
.zFcSizeStep:hover:not(:disabled) .zFcSizeGlyph{ fill:var(--text); }
.zFcSizeRange{
  flex:1 1 auto; min-width:0; height:14px; margin:0;
  -webkit-appearance:none; appearance:none; background:none; cursor:pointer;
}
.zFcSizeRange::-webkit-slider-runnable-track{
  height:3px; border-radius:2px; background:var(--border);
}
.zFcSizeRange::-moz-range-track{ height:3px; border-radius:2px; background:var(--border); }
.zFcSizeRange::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none;
  width:11px; height:11px; margin-top:-4px; border-radius:50%;
  background:var(--accent); border:none; cursor:pointer;
}
.zFcSizeRange::-moz-range-thumb{
  width:11px; height:11px; border-radius:50%;
  background:var(--accent); border:none; cursor:pointer;
}
.zFcSortWrap{ position:relative; flex:0 0 auto; }
.zFcSortBtn{
  width:24px; height:24px; padding:0;
  display:flex; align-items:center; justify-content:center;
  background:none; border:1px solid transparent; border-radius:4px;
  cursor:pointer; color:var(--text-muted);
}
.zFcSortBtn svg{ width:15px; height:15px; }
.zFcSortBtn svg path{
  fill:none; stroke:currentColor; stroke-width:1.4;
  stroke-linecap:round; stroke-linejoin:round;
}
.zFcSortBtn:hover{ color:var(--text); border-color:var(--border); background:var(--surface-2); }
/* the icon says the list is not in its default order, so the reason the rows
   are where they are is visible without opening the menu */
.zFcSortBtn.sorted{ color:var(--accent); }
/* anchored to the button's left edge, because the button is now the leftmost
   thing in the row — right-anchored it would hang off the panel */
.zFcSortMenu{
  position:absolute; top:calc(100% + 5px); left:0; z-index:4;
  display:none; min-width:150px; padding:4px;
  background:var(--surface); border:1px solid var(--border); border-radius:6px;
  box-shadow:0 8px 22px rgba(0,0,0,0.38);
}
.zFcSortMenu.open{ display:block; }
.zFcSortItem{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  width:100%; padding:6px 9px;
  background:none; border:none; border-radius:4px;
  color:var(--text); font-family:inherit; font-size:12px; text-align:left;
  cursor:pointer;
}
.zFcSortItem:hover{ background:var(--surface-2); }
.zFcSortItem.on{ color:var(--accent); }
.zFcSortArrow{
  font-style:normal; font-family:'IBM Plex Mono', monospace; font-size:12px;
  width:10px; text-align:center;
}

/* Rows are a grid, not a stack: `auto-fill` fits as many row-min tracks as the
   panel allows and stretches them to fill it, so a wide panel shows two
   columns and a narrow one shows a single column, with no breakpoint to pick
   and nothing to keep in step with the thumbnail slider. */
.zFcRows{
  flex:1 1 auto; overflow-y:auto; overflow-x:hidden; padding:6px;
  display:grid; gap:2px; align-content:start;
  grid-template-columns:repeat(auto-fill, minmax(var(--zfc-row-min, 206px), 1fr));
}
.zFcRow{
  display:flex; align-items:center; gap:10px;
  width:100%; min-width:0; padding:7px 8px; margin:0;
  background:none; border:1px solid transparent; border-radius:6px;
  color:var(--text); font-family:inherit; text-align:left;
  cursor:pointer;
  transition:background 140ms ease, border-color 140ms ease;
}
/* Past a certain thumbnail the picture is the point and the text is a caption,
   so the row turns into a card rather than letting a 148px plan sit beside two
   lines of type in a 300px column. */
.zFcList.tallThumbs .zFcRow{ flex-direction:column; align-items:stretch; gap:6px; position:relative; }
.zFcList.tallThumbs .zFcThumb{ width:100%; }
.zFcList.tallThumbs .zFcRowDot{ position:absolute; top:12px; right:12px; }
.zFcRow:hover{ background:var(--surface-2); border-color:var(--border); }
/* fired from the render rather than from the pointer: same look as :hover, so
   pointing at a building and pointing at its row read as the one gesture */
.zFcRow.hl{ background:var(--surface-2); border-color:var(--accent); }
.zFcRow.off{ display:none; }
.zFcThumb{
  flex:0 0 var(--zfc-thumb, 56px);
  width:var(--zfc-thumb, 56px); height:var(--zfc-thumb, 56px);
  border-radius:4px; overflow:hidden;
  background:var(--surface-2) center/60% no-repeat;
  border:1px solid var(--border);
  display:flex; align-items:center; justify-content:center;
}
.zFcThumb img{ width:100%; height:100%; object-fit:contain; display:block; }
.zFcRowText{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:2px; }
.zFcRowTop{ display:flex; align-items:baseline; gap:7px; min-width:0; }
.zFcRowTop b{ font-family:'IBM Plex Mono', monospace; font-size:11.5px; letter-spacing:0.03em; }
.zFcRowName{
  font-size:12.5px; color:var(--text);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.zFcRowMeta{ font-size:11px; color:var(--text-muted); }
.zFcRowDot{ flex:0 0 8px; width:8px; height:8px; border-radius:50%; }
.zFcEmpty{
  display:none; margin:0; padding:18px 16px;
  font-size:12px; color:var(--text-muted); text-align:center;
}
/* the list is a flex item, so at width 0 it is already gone — but its content
   must not reflow while it collapses */
.zFcList > *:not(.zFcGrip){ min-width:var(--zfc-list-min, 220px); }

/* highlight fired from a list row, using the same numbers as :hover */
.zUnitSvg polygon.zUnitShape.hl{
  fill-opacity:var(--u-fill-opacity-hover);
  stroke-width:var(--u-stroke-width-hover);
}

/* the unit detail view — same manners as the image panel: dim backdrop,
   centred box, close on the backdrop or Escape */
.zFcDetail{
  position:fixed; inset:0; z-index:40;
  display:flex; align-items:center; justify-content:center;
  padding:28px;
  background:rgba(8,8,6,0.72);
  backdrop-filter:blur(2px);
  opacity:0; pointer-events:none;
  transition:opacity 200ms ease;
}
.zFcDetail.open{ opacity:1; pointer-events:auto; }
.zFcDetailBox{
  position:relative;
  width:min(980px, 100%); max-height:100%;
  display:flex; flex-direction:column;
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:8px;
  box-shadow:0 18px 48px rgba(0,0,0,0.55);
  overflow:auto;
  transform:scale(0.94); opacity:0;
  transition:transform 240ms cubic-bezier(.2,.9,.3,1.2), opacity 180ms ease;
}
.zFcDetail.open .zFcDetailBox{ transform:none; opacity:1; }
.zFcDetailClose{
  position:absolute; top:10px; right:10px; z-index:2;
  width:26px; height:26px; padding:0;
  display:flex; align-items:center; justify-content:center;
  background:var(--surface-2); border:1px solid var(--border); border-radius:3px;
  color:var(--text-muted); font-family:'IBM Plex Mono', monospace; font-size:13px;
  cursor:pointer;
}
.zFcDetailClose:hover{ color:var(--accent); border-color:var(--accent); }
.zFcDetailMain{ display:flex; gap:0; align-items:stretch; }
.zFcDetailPlan{
  flex:1 1 58%; min-width:0;
  display:flex; align-items:center; justify-content:center;
  padding:20px;
  background:#f4f2ec;
}
.zFcDetailPlan img{ max-width:100%; max-height:56vh; display:block; object-fit:contain; }
.zFcNoPlan{ color:#8a8578; font-family:'IBM Plex Mono', monospace; font-size:12px; }
.zFcDetailInfo{ flex:1 1 42%; min-width:0; padding:22px 22px 20px; }
/* colour is set per unit in script, from the status colour's luminance */
.zFcStatusTag{
  display:inline-block; padding:3px 9px; border-radius:999px;
  color:#141410; font-family:'IBM Plex Mono', monospace;
  font-size:10px; letter-spacing:0.08em;
}
.zFcDetailName{ margin:10px 0 2px; font-size:20px; font-weight:600; }
.zFcDetailCode{
  margin:0; font-family:'IBM Plex Mono', monospace;
  font-size:11.5px; letter-spacing:0.06em; color:var(--text-muted);
}
.zFcFacts{
  display:grid; grid-template-columns:auto 1fr; gap:6px 16px;
  margin:18px 0 0; padding:14px 0 0;
  border-top:1px solid var(--border);
  font-size:13px;
}
.zFcFacts dt{ color:var(--text-muted); font-size:11.5px; align-self:center; }
.zFcFacts dd{ margin:0; font-weight:600; }
.zFcNotes{ margin:14px 0 0; font-size:12.5px; line-height:1.55; color:var(--text-muted); white-space:pre-wrap; }
.zFcActions{ display:flex; flex-wrap:wrap; gap:8px; margin-top:18px; }
.zFcAction{
  padding:8px 14px; border-radius:5px;
  background:var(--accent); color:#141410;
  font-size:12px; font-weight:600; text-decoration:none;
}
.zFcAction:hover{ filter:brightness(1.08); }
.zFcAction + .zFcAction{ background:var(--surface-2); color:var(--text); border:1px solid var(--border); }
.zFcThumbs{
  display:flex; gap:8px; flex-wrap:wrap;
  padding:14px 22px 18px;
  border-top:1px solid var(--border);
}
.zFcThumbs button{
  width:74px; height:56px; padding:0;
  background:var(--surface-2); border:1px solid var(--border); border-radius:4px;
  overflow:hidden; cursor:pointer;
}
.zFcThumbs button.on{ border-color:var(--accent); }
.zFcThumbs img{ width:100%; height:100%; object-fit:cover; display:block; }

@media (max-width: 820px){
  .zFcDetailMain{ flex-direction:column; }
  .zFcDetailPlan{ padding:14px; }
  .zFcDetailPlan img{ max-height:38vh; }
}
/* Below this the list would leave the render no room worth looking at, so it
   covers the stage instead of narrowing it. Taking it out of flow is what does
   that — the stage reclaims the width on its own, and the inline
   --zfc-list-w (which a stylesheet could not override) goes back to meaning
   just the panel's own width. */
@media (max-width: 720px){
  .zFcList{ position:absolute; top:0; right:0; z-index:20; max-width:88vw; box-shadow:-8px 0 24px rgba(0,0,0,0.4); }
}
@media (max-width: 560px){
  .zFcRail{ top:12px; left:12px; }
  .zFcRailBtn{ width:34px; height:34px; }
  .zFcFilter{ left:54px; top:12px; }
  .zFcActive .onImageTitle{ left:58px; }
}
@media (prefers-reduced-motion: reduce){
  .zFcList, .zFcFilter, .zFcDetail, .zFcDetailBox, .zFcRailBtn{ transition:none; }
}

/* on-image title, top-left */
.onImageTitle{
  position:absolute;
  top:16px; left:18px;
  z-index:5;
  margin:0;
  text-shadow:0 1px 4px rgba(0,0,0,0.6);
  pointer-events:none;
}

/* on-image footer, bottom-left — a real link, not just decorative text */
.onImageFooter{
  position:absolute;
  bottom:12px; left:18px;
  z-index:5;
  margin:0;
  text-shadow:0 1px 3px rgba(0,0,0,0.6);
  text-decoration:none;
}
.onImageFooter:hover{ text-decoration:underline; }

/* ---- breadcrumb, directly above the nav ----
   "Project Name > House Name". Its `bottom` is set from script
   (ZOACore.anchorAbove) because the three nav styles are very different
   heights; everything else is themed through the vars renderCrumbs sets.
   Hidden until it has something to say — a project with no sub-sequences
   never sees it. */
.crumbBar{
  position:absolute;
  bottom:0; left:50%;
  transform:translateX(-50%);
  z-index:6;
  display:none;
  align-items:center;
  gap:calc(6px * var(--scale));
  padding:calc(4px * var(--scale)) calc(12px * var(--scale));
  border-radius:20px;
  max-width:min(92%, 760px);
  backdrop-filter:blur(3px);
  background:rgba(var(--crumb-bg-rgb, 20,20,16), var(--crumb-bg-opacity, 0.55));
  font-family:var(--crumb-font-family, inherit);
  font-size:calc(var(--crumb-font-size, 12px) * var(--scale));
  font-weight:var(--crumb-font-weight, 400);
  font-style:var(--crumb-font-style, normal);
  white-space:nowrap;
}
.crumbBar.open{ display:flex; }

.crumbSeg{
  font:inherit;
  color:var(--crumb-color, rgba(238,233,224,0.72));
  text-decoration:var(--crumb-text-decoration, none);
  background:none; border:0; padding:0; margin:0;
  /* every segment can be squeezed, but the one you are on is squeezed last —
     a long project name must not push "House 3" out of the bar */
  min-width:0; overflow:hidden; text-overflow:ellipsis;
}
button.crumbSeg{ cursor:pointer; transition:color 140ms ease; }
button.crumbSeg:hover{ color:var(--crumb-active-color, #eee9dd); }
.crumbSeg.current{ color:var(--crumb-active-color, #eee9dd); flex:0 0 auto; }
/* falls back to the segment colour, which is what it was before it had a var
   of its own — only the bar look sets it */
.crumbSep{ color:var(--crumb-sep-color, var(--crumb-color, rgba(238,233,224,0.72))); opacity:0.6; flex:0 0 auto; }

/* ---- the crumb bar wearing the project's bar navigation ----
   Only on a free-spin sequence, where it IS the navigation: the pill with the
   scene list is gone, so this is the one piece of furniture at the bottom of
   the picture and it should look like the piece the project chose. Every value
   comes from theme.navBar, so restyling the nav restyles this with it.
   The back arrow is the only arrow: forward from here is nowhere.

   NOT scaled by --scale, unlike everything else in the crumb bar. .navBarShell
   is a flat `height:var(--bar-height)` and barStyleSlot writes plain pixel font
   sizes, so multiplying here made the same capsule 20% bigger on the flat
   chooser than on the overview — the one place the difference is visible,
   since the two are a click apart. Matching the nav means matching how the nav
   measures, not just what it measures. */
.crumbBar.asBar{
  border-radius:var(--crumb-radius, 999px);
  min-height:var(--crumb-height, 44px);
  padding:0 18px;
  gap:9px;
  font-size:var(--crumb-font-size, 13px);
  backdrop-filter:blur(4px);   /* the same blur .navBarShell has */
  border:var(--crumb-stroke-width, 0px) solid var(--crumb-stroke-color, transparent);
}
.crumbBar.asBar .crumbSeg.current{
  font-family:var(--crumb-active-family, inherit);
  font-size:var(--crumb-active-size, 15px);
  font-weight:600;   /* the same weight barStyleSlot gives the current scene */
}
.crumbBar.asBar .crumbSep{ opacity:1; }
.crumbBar.asBar .crumbBack{
  color:var(--crumb-arrow-color, var(--crumb-color));
  width:20px; height:20px;
  margin-right:4px;
}

/* the way out, sitting inside the bar rather than being a second piece of
   chrome that says the same thing */
.crumbBack{
  display:flex; align-items:center; justify-content:center;
  flex:0 0 auto;
  width:calc(18px * var(--scale)); height:calc(18px * var(--scale));
  margin-right:calc(2px * var(--scale));
  padding:0; border:0; background:none;
  color:var(--crumb-color, rgba(238,233,224,0.72));
  cursor:pointer;
  transition:color 140ms ease, transform 140ms ease;
}
.crumbBack svg{ width:100%; height:100%; fill:none; stroke:currentColor; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round; }
.crumbBack:hover{ color:var(--crumb-active-color, #eee9dd); transform:translateX(-2px); }

/* nav pill, bottom-center, on the image */
.navBox{
  position:absolute;
  bottom:14px; left:50%;
  transform:translateX(-50%);
  z-index:6;
  display:flex;
  align-items:center;
  gap:calc(14px * var(--scale));
  padding:calc(7px * var(--scale)) calc(16px * var(--scale));
  border-radius:20px;
  backdrop-filter:blur(3px);
  background:rgba(var(--nav-bg-rgb, 20,20,16), var(--nav-bg-opacity, 0.55));
}
/* A nav box with nothing in it is a blurred smear on the render. The viewer
   hides it outright on a free-spin sequence — but zoa-core.js is one shared
   file and a site's HTML is not versioned, so a visitor holding a cached page
   from before the spin arrows were removed will empty the box and leave it.
   This is for them; the current viewer never relies on it. */
.navBox:empty{ display:none; }

/* ---- "drag to rotate", at both edges of a free-spin picture ----
   Where the nav pill's two arrows used to be. They sit at the sides because
   that is where the gesture goes and because the bottom of the picture already
   has the breadcrumb; they are quiet because they are a hint, not a control,
   and they leave (.retired) once the visitor has turned the building twice.
   One drawing: the left one is the right one mirrored. */
.zSpinHint{
  position:absolute;
  /* Placed off the centre line rather than off the edges: one distance out
     from the middle and one up from the floor, each a setting, the pair
     symmetric by construction because they share a single number with
     opposite signs. Anchored by their centres, hence the -50% that every
     keyframe has to repeat. */
  left:calc(50% + var(--zsh-dx, 230px));
  bottom:var(--zsh-y, 76px);
  transform:translateX(-50%);
  z-index:6;
  pointer-events:none;
  display:flex; flex-direction:column; align-items:center;
  gap:var(--zsh-gap, 6px);
  /* White by default rather than the scroll hint's off-white: these sit over
     renders that are mostly sky and pale ground, where 0.85 off-white came out
     as a grey smudge. The drop shadow carries them over a light frame. */
  color:var(--zsh-color, #ffffff);
  opacity:var(--zsh-opacity, 0.96);
  filter:var(--zsh-shadow, none);
  transition:opacity 420ms ease;
}
.zSpinHint.retired{ opacity:0; }
.zSpinHint svg{
  display:block;
  /* `size` is the arrow's width in pixels — what it looks like on screen,
     rather than a multiplier of some other glyph's size. */
  width:var(--zsh-size, 55px); height:auto;
  fill:currentColor; stroke:none;
  /* Tipped anticlockwise (and mirrored, on the left), which is what turns a
     sweep that goes RIGHT into one that goes ROUND: it leaves horizontally and
     finishes pointing upward. Built in the JS, because the angle is a setting
     and because the mirror has to be applied after the rotation. */
  transform:var(--zsh-rotate, rotate(-30deg));
  overflow:visible;   /* the rotated corners are outside the box, not clipped */
}
.zSpinHintText{
  font-family:var(--nav-font-family, inherit);
  font-size:var(--zsh-text-size, 11px);
  font-weight:500;
  letter-spacing:0.14em;
  text-transform:uppercase;
  white-space:nowrap;
}
/* The nudge goes the way that side turns, so the pair reads as one gesture
   with two directions rather than as two blinking labels. Each keyframe
   restates the -50%, since transform is one property and the base rule's
   centring would otherwise be dropped for the duration. */
.zSpinHint.right{ animation:zSpinNudgeR 2.4s ease-in-out infinite; }
.zSpinHint.left{ animation:zSpinNudgeL 2.4s ease-in-out infinite; }
@keyframes zSpinNudgeR{
  0%,100%{ transform:translateX(-50%); }
  50%{ transform:translateX(calc(-50% + 6px)); }
}
@keyframes zSpinNudgeL{
  0%,100%{ transform:translateX(-50%); }
  50%{ transform:translateX(calc(-50% - 6px)); }
}
@media (prefers-reduced-motion: reduce){
  .zSpinHint{ animation:none; }
}
@media (max-width: 640px){
  /* on a phone the two labels meet in the middle; the arrows alone still say it */
  .zSpinHintText{ display:none; }
}

/* The render is scenery, not a file to drag to the desktop. Without this a
   press-and-drag on a free-spin strip starts the browser's native image drag
   and the building never moves — pointermove simply stops arriving. */
.stageInner img.scene{ -webkit-user-drag:none; user-drag:none; }
.stage.freeSpin img{ -webkit-user-drag:none; user-drag:none; }

.navStop{
  display:flex; align-items:center; gap:calc(6px * var(--scale));
  cursor:pointer;
  background:none; border:none;
  padding:0;
  white-space:nowrap;
}
.navDot{
  position:relative;
  width:calc(9px * var(--scale)); height:calc(9px * var(--scale));
  border-radius:50%;
  background:rgba(255,255,255,0.4);
  flex-shrink:0;
  transition:background 150ms ease, transform 150ms ease;
}
.navStop.active .navDot{ background:var(--accent); transform:scale(1.3); }
.navStop:not(.active) .navDot::after{
  content:'';
  position:absolute; inset:calc(-4px * var(--scale));
  border:1px solid rgba(255,255,255,0.55);
  border-radius:50%;
  opacity:0.55;
  animation:navpulse 2.4s ease-out infinite;
  pointer-events:none;
}
.navStop:not(.active):hover .navDot::after{ animation:none; opacity:0; }
@keyframes navpulse{
  0%{ transform:scale(1); opacity:0.55; }
  100%{ transform:scale(2.3); opacity:0; }
}
.navLabel{ transition:color 150ms ease; font-family:var(--nav-font-family, inherit); font-size:var(--nav-font-size-base, 11px); color:var(--nav-text-color, #eee9dd); font-weight:var(--nav-font-weight, 400); font-style:var(--nav-font-style, normal); text-decoration:var(--nav-text-decoration, none); }
.navStop.active .navLabel{ color:var(--nav-selected-color, var(--accent)); }

/* alternate "carousel" nav style: current scene centered, one neighbour
   labelled on each side, connected by a line — clicking a neighbour makes
   it the new centre */
.navBox.carouselMode{
  background:none; backdrop-filter:none; padding:0;
  width:min(380px, 82vw);
}
.navCarouselLine{ position:relative; width:100%; height:1px; background:rgba(255,255,255,0.4); }
.navCarouselDotWrap{
  position:absolute; top:50%; transform:translate(-50%,-50%);
  display:flex; flex-direction:column; align-items:center; gap:calc(8px * var(--scale));
  cursor:pointer; background:none; border:none; padding:0;
}
.navCarouselDot{
  width:calc(9px * var(--scale)); height:calc(9px * var(--scale));
  border-radius:50%; background:rgba(255,255,255,0.5);
  transition:background 150ms ease, transform 150ms ease;
}
.navCarouselDotWrap:hover .navCarouselDot{ transform:scale(1.2); }
.navCarouselDotWrap.active .navCarouselDot{ background:var(--nav-selected-color, var(--accent)); transform:scale(1.3); }
.navCarouselLabel{
  font-family:var(--nav-font-family, inherit);
  font-size:var(--nav-font-size-base, 11px);
  color:var(--nav-text-color, #eee9dd);
  font-weight:var(--nav-font-weight, 400);
  font-style:var(--nav-font-style, normal);
  text-decoration:var(--nav-text-decoration, none);
  white-space:nowrap;
  transition:color 150ms ease, background 150ms ease, padding 150ms ease;
}
.navCarouselDotWrap.active .navCarouselLabel{
  background:rgba(var(--nav-bg-rgb, 20,20,16), var(--nav-bg-opacity, 0.55));
  padding:calc(5px * var(--scale)) calc(12px * var(--scale));
  border-radius:14px;
  color:var(--nav-selected-color, var(--accent));
  backdrop-filter:blur(3px);
}

/* "wheel" nav style — a dipping elliptical line with the current scene
   centred on it, fading out at both edges, one labelled neighbour each side */
.navBox.wheelMode{
  background:none; backdrop-filter:none; padding:0;
  width:min(460px, 88vw);
  height:80px;
}
.navWheelSvg{
  position:absolute; inset:0; width:100%; height:100%;
  pointer-events:none; overflow:visible;
}
.navWheelPath{ stroke: url(#navWheelFade); }
.navWheelDotWrap{
  position:absolute;
  left:0; top:0;
  display:flex; flex-direction:column; align-items:center; gap:6px;
  background:none; border:none; cursor:pointer; padding:0;
}
.navWheelDot{
  position:relative;
  width:12px; height:12px; border-radius:50%;
  background:var(--wheel-dot-color, #ffffff);
  color:var(--wheel-dot-color, #ffffff);
  transition:background 200ms ease, color 200ms ease;
}
.navWheelDotWrap.active .navWheelDot{
  background:var(--nav-selected-color, var(--accent));
  color:var(--nav-selected-color, var(--accent));
}
.navWheelDot::after{
  content:'';
  position:absolute; inset:-6px;
  border-radius:50%;
  border:1px solid currentColor;
  color:inherit;
  opacity:0.5;
  animation:navWheelPulse 2.4s ease-out infinite;
  pointer-events:none;
}
.navWheelDotWrap:hover .navWheelDot::after{ animation:none; opacity:0; }
@keyframes navWheelPulse{
  0%{ transform:scale(1); opacity:0.5; }
  100%{ transform:scale(2.3); opacity:0; }
}
.navWheelLabel{ white-space:nowrap; display:inline-block; transition:transform 500ms ease, color 500ms ease, text-shadow 500ms ease; }

/* hotspot markers — a position wrapper (anchors to x/y, never animates)
   plus an inner element (scales in/out on arrival/departure) */
.hotspotPos{ position:absolute; }
.hotspotPos.plain{ transform:translate(-50%,-50%); }
.hotspotPos.named{ transform:translate(-50%,-100%) translateY(calc(-8px * var(--scale))); }

/* --marker-pad adds to the marker's own geometry, growing the box without
   changing the icon/label inside it. --marker-radius-* is set per shape:
   rounded (the original), pill (fully round ends, a circle when there's no
   label) and rect (square corners). */
.hotspot{
  position:relative;
  cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  background:rgba(var(--marker-bg-rgb), var(--marker-bg-opacity));
  backdrop-filter:blur(2px);
  transform:scale(1);
  transition:transform 150ms ease;
  box-shadow:var(--marker-shadow, none);
}
.hotspot.plain{
  width:calc((28px + 2 * var(--marker-pad, 0px)) * var(--scale));
  height:calc((28px + 2 * var(--marker-pad, 0px)) * var(--scale));
  border-radius:var(--marker-radius-plain, 4px);
}
.hotspot.plain:hover{ transform:scale(1.12); }
.hotspot.named{
  height:calc((28px + 2 * var(--marker-pad, 0px)) * var(--scale));
  padding:0 calc((10px + var(--marker-pad, 0px)) * var(--scale)) 0 calc((8px + var(--marker-pad, 0px)) * var(--scale));
  border-radius:var(--marker-radius-named, 5px);
  gap:calc(6px * var(--scale));
  white-space:nowrap;
}
.hotspot.named:hover{ transform:scale(1.06); }
.hotspot.named::after{
  content:'';
  position:absolute;
  left:50%; bottom:calc(-7px * var(--scale));
  transform:translateX(-50%);
  width:0; height:0;
  border-left:calc(6px * var(--scale)) solid transparent;
  border-right:calc(6px * var(--scale)) solid transparent;
  border-top:calc(7px * var(--scale)) solid var(--pin-color, var(--accent));
}
.hotspot .hsIcon{ font-size:calc(13px * var(--scale)); color:var(--marker-icon-color); }
.hotspot .hsName{ font-size:calc(var(--marker-font-size-base, 11.5px) * var(--scale)); color:var(--marker-name-color); font-family:var(--marker-font-family, inherit); font-weight:var(--marker-font-weight, 400); font-style:var(--marker-font-style, normal); text-decoration:var(--marker-text-decoration, none); }
/* the pulsating ring. Colour/thickness/starting opacity/speed are all
   configurable; the defaults reproduce the original look (contour colour,
   1px, 0.6 starting opacity, 2.4s). The animation's starting opacity comes
   from --marker-pulse-opacity so the keyframes stay generic. */
.hotspot::before{
  content:'';
  position:absolute; inset:calc(-6px * var(--scale));
  border-radius:inherit;
  border:calc(var(--marker-pulse-width, 1px) * var(--scale)) solid var(--marker-pulse-color, var(--pin-color, var(--accent)));
  opacity:var(--marker-pulse-opacity, 0.6);
  animation:pulse var(--marker-pulse-duration, 2.4s) ease-out infinite;
  pointer-events:none;
}
/* thickness 0 would still paint a hairline border on some engines */
.hotspot.noPulse::before{ display:none; }
.hotspot:hover::before{ animation:none; opacity:0; }
/* --marker-pulse-scale is how far the ring travels before fading out, as a
   multiple of the marker's size (1 = no growth). var() resolves against the
   element the animation runs on, so this stays per-marker overridable. */
@keyframes pulse{
  0%{ transform:scale(1); opacity:var(--marker-pulse-opacity, 0.6); }
  100%{ transform:scale(var(--marker-pulse-scale, 1.6)); opacity:0; }
}

/* ---- custom marker: thought cloud ----
   Built from overlapping bordered lobes with unbordered fills laid over the
   top, which erases the internal borders and leaves one continuous outline.
   All geometry is set inline from JS as fractions of the bubble width, so the
   whole thing scales from a single number. */
.hotspotPos.custom{ transform:translate(-50%,-100%); }
.hotspot.custom{
  background:none; backdrop-filter:none; border:none; padding:0;
  box-shadow:none;
  display:block; cursor:pointer;
  transform:scale(1);
  transition:transform 150ms ease;
}
.hotspot.custom::before{ display:none; }   /* no pulse ring behind a cloud */
.hotspot.custom:hover{ transform:scale(1.03); }
.zcCloud{ position:relative; filter:drop-shadow(var(--marker-shadow-flat, 0 0 0 transparent)); }
.zcLayer{
  position:absolute;
  transition:transform 0.15s cubic-bezier(0.25, 1, 0.5, 1);
}
.zcCloudLayer{ left:0; top:0; }
.zcFloat{
  position:relative; width:100%; height:100%;
  animation:zcFloat 3s ease-in-out infinite alternate 2.2s;
}
/* the whole union carries the background transparency ONCE — applying it to
   each lobe would compound wherever they overlap and look blotchy */
.zcBody{ display:block; position:absolute; inset:0; opacity:var(--zc-alpha, 1); }
.zcDot, .zcLobe{
  position:absolute;
  background:var(--zc-fill, #fff);
  border:var(--zc-stroke, 4px) solid var(--zc-border, #333);
  border-radius:50%;
  opacity:0; transform:scale(0);
  animation:zcPop 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
/* dots sit outside the body group and don't overlap each other, so they can
   carry the alpha themselves — via the keyframe, which would otherwise end at 1 */
.zcDot{ display:block; animation-name:zcPopDot; }
.zcFill{
  position:absolute;
  background:var(--zc-fill, #fff);
  border-radius:50%;
  opacity:0;
  animation:zcFade 0.1s forwards;
}
/* pictogram + name, centred on the cloud body */
.zcContent{
  position:absolute; inset:14% 14% 22%;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:0.35em;
  opacity:0;
  animation:zcFade 0.35s forwards;
  pointer-events:none;
  z-index:2;
}
.zcIcon{ color:var(--zc-text, #17170f); line-height:1; }
.zcText{
  display:block; max-width:100%;
  text-align:center; line-height:1.2;
  color:var(--zc-text, #17170f);
  font-family:var(--marker-font-family, inherit);
  font-weight:var(--marker-font-weight, 600);
  font-style:var(--marker-font-style, normal);
  overflow-wrap:break-word;
}
@keyframes zcPop{
  0%{ opacity:0; transform:scale(0); }
  100%{ opacity:1; transform:scale(1); }
}
@keyframes zcPopDot{
  0%{ opacity:0; transform:scale(0); }
  100%{ opacity:var(--zc-alpha, 1); transform:scale(1); }
}
@keyframes zcFade{ to{ opacity:1; } }
@keyframes zcFloat{
  0%{ transform:translateY(0); }
  100%{ transform:translateY(-6px); }
}
@media (prefers-reduced-motion: reduce){
  .zcFloat{ animation:none; }
  .zcDot, .zcLobe, .zcFill, .zcContent{ animation-duration:0.01s; }
}

/* ---- "bar" nav style ----
   Current scene centred, neighbours either side, arrow at each end. The three
   slots are absolutely positioned thirds so the relay can slide them between
   fixed positions without label widths shifting each other mid-animation. */
/* One slot width is the unit the whole strip is measured in, so a dead end can
   drop exactly one slot (and one arrow) off the capsule.

   The box around the capsule is pinned to the FULL strip width and never
   shrink-wraps its contents. It used to be width:auto, which deadlocked the
   collapse: the box sized itself to the capsule, the capsule (a flex item) was
   then clamped to the box, and the pair could only ever ratchet smaller — the
   capsule could shrink but never grow back. */
.navBox.barMode{
  --bar-arrow-w:38px;
  --bar-slot-w:calc((min(var(--bar-width, 520px), 92vw) - 2 * var(--bar-arrow-w)) / 3);
  background:none; backdrop-filter:none; padding:0;
  width:calc(var(--bar-slot-w) * 3 + var(--bar-arrow-w) * 2);
  justify-content:center;
  /* the box stays full width even when the capsule is collapsed, so it must
     not swallow clicks in the empty margin either side */
  pointer-events:none;
}
.navBarShell{
  flex:0 0 auto;
  pointer-events:auto;
  display:flex; align-items:center;
  width:calc(var(--bar-slot-w) * 3 + var(--bar-arrow-w) * 2);
  height:var(--bar-height, 44px);
  background:var(--bar-bg, rgba(255,255,255,0.92));
  border:var(--bar-stroke-width, 0) solid var(--bar-stroke-color, transparent);
  border-radius:var(--bar-radius, 999px);
  backdrop-filter:blur(4px);
  box-sizing:border-box;
  overflow:hidden;
  transition:width var(--bar-anim, 600ms) cubic-bezier(.3,.9,.3,1),
             transform var(--bar-anim, 600ms) cubic-bezier(.3,.9,.3,1);
}
/* The track never changes size — a negative margin slides it under the shell's
   clip so the dead side's slot disappears while the surviving slots keep both
   their width and their position. */
.navBarTrack{
  position:relative; flex:0 0 auto; height:100%;
  width:calc(var(--bar-slot-w) * 3);
  transition:margin var(--bar-anim, 600ms) cubic-bezier(.3,.9,.3,1);
}
.navBarSlot{
  position:absolute; top:0;
  width:33.3333%; height:100%;
  display:flex; align-items:center; justify-content:center;
  padding:0 6px;
  background:none; border:none;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  cursor:pointer;
  transition:color var(--bar-anim, 600ms) ease, font-size var(--bar-anim, 600ms) ease;
  box-sizing:border-box;
}
.navBarSlot.current{ cursor:default; }
.navBarSlot.isEmpty{ cursor:default; opacity:0; pointer-events:none; }
.navBarSlot:not(.current):not(.isEmpty):hover{ filter:brightness(0.75); }
.navBarDivider{
  position:absolute; top:22%; bottom:22%;
  width:1px;
  background:var(--bar-divider, #c9c9c4);
  pointer-events:none;
  transition:opacity var(--bar-anim, 600ms) ease;
}
.navBarArrow{
  flex:0 0 auto;
  width:var(--bar-arrow-w, 38px); height:100%;
  min-width:0;
  display:flex; align-items:center; justify-content:center;
  background:none; border:none; padding:0;
  cursor:pointer;
  overflow:hidden;
  color:var(--bar-arrow, #141410);
  transition:width var(--bar-anim, 600ms) cubic-bezier(.3,.9,.3,1),
             opacity var(--bar-anim, 600ms) ease;
}
.navBarArrow svg{ width:18px; height:18px; flex:0 0 18px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
.navBarArrow:not(.isEmpty):hover{ filter:brightness(0.6); }
/* Dead end of a non-looping sequence: the arrow, its divider and the empty
   slot all go, and the capsule closes up against the current scene — so there
   is visibly only one direction left to travel. */
.navBarShell.deadPrev .navBarArrow.prev,
.navBarShell.deadNext .navBarArrow.next{ width:0; opacity:0; cursor:default; }
.navBarShell.deadPrev .navBarDivider.d1,
.navBarShell.deadNext .navBarDivider.d2{ opacity:0; }
/* The capsule is centred in the box, so shrinking it alone would drag the
   current scene sideways. This offset by half of what was removed pins the
   current scene where it was: only the dead edge travels. */
.navBarShell.deadPrev{ transform:translateX(calc((var(--bar-slot-w) + var(--bar-arrow-w)) / 2)); }
.navBarShell.deadNext{ transform:translateX(calc((var(--bar-slot-w) + var(--bar-arrow-w)) / -2)); }
/* a two-scene, non-looping sequence can be dead on both sides at once */
.navBarShell.deadPrev.deadNext{ transform:none; }

@media (prefers-reduced-motion: reduce){
  .navBarSlot, .navBarShell, .navBarTrack, .navBarArrow, .navBarDivider{ transition:none; }
}

/* ---- scroll hint ---- */
.zScrollHint{
  position:absolute; left:50%; transform:translateX(-50%);
  z-index:6;
  pointer-events:none;
  opacity:var(--zsh-opacity, 0.85);
  filter:var(--zsh-shadow, none);
  transition:opacity 420ms ease;
  animation:zshBob 2.4s ease-in-out infinite;
}
.zScrollHint.retired{ opacity:0; }
.zScrollHint svg{
  display:block;
  /* 55/46: the artwork grew a second chevron and got taller, the MOUSE did
     not. Scaling by the same ratio the box grew keeps `size` meaning the
     rendered mouse it has always meant, so no stored setting silently shrank. */
  height:calc(var(--zsh-size, 34px) * 55 / 46); width:auto;
  fill:none;
  stroke:var(--zsh-color, #eee9dd);
  stroke-width:2; stroke-linecap:round; stroke-linejoin:round;
}
.zScrollHint .zshDot{
  fill:var(--zsh-color, #eee9dd); stroke:none;
  animation:zshDot 1.7s ease-in-out infinite;
}
/* Both chevrons pulse at once, each leaning the way it points. Alternating
   them would read as an instruction to go that way FIRST; together they read
   as "either way", which is what the gesture actually offers. */
.zScrollHint .zshChev.down{ animation:zshChevDown 1.7s ease-in-out infinite; }
.zScrollHint .zshChev.up{ animation:zshChevUp 1.7s ease-in-out infinite; }
@keyframes zshDot{
  0%,100%{ transform:translateY(0); opacity:1; }
  55%{ transform:translateY(7px); opacity:0.15; }
}
@keyframes zshChevDown{
  0%,100%{ opacity:0.35; transform:translateY(0); }
  55%{ opacity:1; transform:translateY(3px); }
}
@keyframes zshChevUp{
  0%,100%{ opacity:0.35; transform:translateY(0); }
  55%{ opacity:1; transform:translateY(-3px); }
}
@keyframes zshBob{
  0%,100%{ transform:translateX(-50%) translateY(0); }
  50%{ transform:translateX(-50%) translateY(-3px); }
}
/* ---- the same hint on its side, for a free-spin strip ----
   Rotating the artwork by -90deg sends "down" to "right" and "up" to "left",
   and takes the dot and both chevrons with it — so the drop and the two nudges
   that teach a scroll become the drag that turns a building either way, with
   no second drawing to keep in step with the first. The bob turns sideways to
   match. */
.zScrollHint.horizontal svg{ transform:rotate(-90deg); }
.zScrollHint.horizontal{ animation-name:zshBobX; }
@keyframes zshBobX{
  0%,100%{ transform:translateX(-50%); }
  50%{ transform:translateX(calc(-50% + 3px)); }
}
@media (prefers-reduced-motion: reduce){
  .zScrollHint{ animation:none; }
  .zScrollHint .zshDot, .zScrollHint .zshChev{ animation:none; }
}

.pendingPin{
  position:absolute; width:14px; height:14px; transform:translate(-50%,-50%);
  border-radius:50%; border:2px dashed var(--accent); pointer-events:none;
}

#transitionVideo{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  z-index:7;
  opacity:0;
  pointer-events:none;
}
#transitionVideo.playing{ opacity:1; }

/* A scene whose still view is replaced by a short film once it has arrived.

   Not #transitionVideo's job: that one covers the JOURNEY between two scenes
   and is gone by the time you get there. This plays while the visitor stands
   still, so it has to sit exactly where the render sits — same box, same
   object-fit — or the swap would nudge the picture.

   No z-index on purpose. It is inserted after the render and before
   #hotspotLayer, so DOM order puts it over the frame it replaces while the
   unit shapes (z-index 1) and the markers (2) stay on top of both. */
#sceneVideo{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  pointer-events:none;
  opacity:0;
  transition:opacity 300ms ease;
}
#sceneVideo.on{ opacity:1; }

.stageOverlay{
  position:absolute; inset:0; background:rgba(10,10,8,0.32);
  opacity:0; pointer-events:none; transition:opacity 220ms ease; z-index:9;
}
.stageOverlay.open{ opacity:1; pointer-events:auto; }

.panel{
  position:absolute;
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:6px;
  box-shadow:0 12px 32px rgba(0,0,0,0.45);
  z-index:10;
  opacity:0;
  pointer-events:none;
  transform:scale(0.18);
  transition:transform 240ms cubic-bezier(.2,.9,.3,1.2), opacity 180ms ease;
  overflow:hidden;
}
.panel.open{ opacity:1; pointer-events:auto; transform:scale(1); }
.panel .panelInner{ padding:14px; }
.panel .pimgWrap{ margin:-14px -14px 12px; position:relative; }
.panel .pimgWrap img{ width:100%; display:block; }
.panel .pimgWrap.clickable{ cursor:zoom-in; }
.panel .ptitle{ font-size:calc(var(--panel-title-font-size-base, 14.5px)); font-weight:var(--panel-title-font-weight, 600); font-style:var(--panel-title-font-style, normal); text-decoration:var(--panel-title-text-decoration, none); margin:0 0 5px; font-family:var(--panel-title-font-family, inherit); color:var(--panel-title-color, var(--text)); }
/* white-space:pre-wrap keeps the line breaks the author typed in the editor's
   textarea — the newlines are already in the DOM via textContent, but the
   default `white-space:normal` collapses them into single spaces. */
.panel .pbody{ font-size:var(--panel-text-font-size-base, 13px); line-height:1.5; color:var(--panel-text-color, var(--text-muted)); margin:0; font-family:var(--panel-text-font-family, inherit); font-weight:var(--panel-text-font-weight, 400); font-style:var(--panel-text-font-style, normal); text-decoration:var(--panel-text-text-decoration, none); white-space:pre-wrap; overflow-wrap:break-word; }
.panel .close{
  position:absolute; top:8px; right:8px;
  width:24px; height:24px;
  display:flex; align-items:center; justify-content:center;
  background:var(--surface-2);
  border:1px solid var(--border);
  border-radius:3px;
  color:var(--text-muted);
  font-family:'IBM Plex Mono',monospace; font-size:13px; line-height:1;
  cursor:pointer;
  z-index:2;
  padding:0;
}
.panel .close:hover{ color:var(--accent); border-color:var(--accent); }
.panel .moreInfoBtn{
  display:block; width:calc(100% + 28px);
  margin:12px -14px -14px;
  padding:10px 14px;
  border:none;
  background:var(--panel-more-bg, var(--accent));
  color:var(--panel-more-color, #141410);
  font-family:var(--panel-more-font-family, inherit);
  font-size:var(--panel-more-font-size-base, 12px);
  font-weight:var(--panel-more-font-weight, 600);
  font-style:var(--panel-more-font-style, normal);
  text-align:left;
  text-decoration:var(--panel-more-text-decoration, none);
  cursor:pointer;
  letter-spacing:0.02em;
}
.panel .moreInfoBtn:hover{ filter:brightness(1.08); }
/* inset variant — sits inside the box's padding as a normal button instead
   of bleeding out to the box's edges */
.panel .moreInfoBtn.inset{
  display:inline-block; width:auto;
  margin:12px 0 0;
  padding:8px 16px;
  border-radius:5px;
}

.lightbox{
  position:absolute; inset:0;
  z-index:20;
  background:rgba(8,8,6,0.44);
  display:flex; align-items:center; justify-content:center;
  opacity:0; pointer-events:none;
  transition:opacity 200ms ease;
  cursor:zoom-out;
}
.lightbox.open{ opacity:1; pointer-events:auto; }
.lightbox img{ max-width:92%; max-height:92%; object-fit:contain; border-radius:4px; box-shadow:0 20px 60px rgba(0,0,0,0.6); }

/* ---- 360° panorama viewer (vr hotspots) ----
   .vrBackdrop is the click-outside-to-close surface; .vrWindow is the framed
   box whose bounds that refers to, so clicks inside it must not bubble. */
.vrBackdrop{
  position:absolute; inset:0;
  z-index:25;
  background:rgba(8,8,6,0.82);
  opacity:0; pointer-events:none;
  transition:opacity 200ms ease;
}
.vrBackdrop.open{ opacity:1; pointer-events:auto; }
/* absolutely positioned (not flex-centred) because left/top/width/height are
   driven from JS once the window can be dragged and resized — openVr() sets
   the initial centred geometry */
.vrWindow{
  position:absolute;
  background:#12120f;
  border:1px solid var(--border);
  border-radius:8px;
  overflow:hidden;
  box-shadow:0 18px 48px rgba(0,0,0,0.55);
}
.vrStage{ position:absolute; inset:0; cursor:grab; }
.vrStage:active{ cursor:grabbing; }
.vrBtn{
  position:absolute; top:10px; z-index:4;
  width:28px; height:28px;
  display:flex; align-items:center; justify-content:center;
  background:rgba(20,20,16,0.85);
  border:1px solid var(--border);
  border-radius:4px;
  color:var(--text-muted);
  font-family:'IBM Plex Mono',monospace; font-size:14px; line-height:1;
  cursor:pointer; padding:0;
}
.vrBtn:hover{ color:var(--accent); border-color:var(--accent); }
.vrClose{ right:10px; }
.vrMove{ right:44px; cursor:grab; }
.vrMove:active{ cursor:grabbing; }

/* Resize grips — one per edge and corner, above the panorama canvas so a drag
   here resizes instead of panning. Kept fully inside the box: .vrWindow has
   overflow:hidden (to clip the canvas to the rounded corners), so anything
   pushed outside its bounds would be clipped away and lose grab area. */
.vrGrip{ position:absolute; z-index:3; }
.vrGrip.n{ top:0; left:16px; right:16px; height:8px; cursor:ns-resize; }
.vrGrip.s{ bottom:0; left:16px; right:16px; height:8px; cursor:ns-resize; }
.vrGrip.w{ left:0; top:16px; bottom:16px; width:8px; cursor:ew-resize; }
.vrGrip.e{ right:0; top:16px; bottom:16px; width:8px; cursor:ew-resize; }
.vrGrip.nw{ top:0; left:0; width:16px; height:16px; cursor:nwse-resize; }
.vrGrip.se{ bottom:0; right:0; width:16px; height:16px; cursor:nwse-resize; }
.vrGrip.ne{ top:0; right:0; width:16px; height:16px; cursor:nesw-resize; }
.vrGrip.sw{ bottom:0; left:0; width:16px; height:16px; cursor:nesw-resize; }
.vrMsg{
  position:absolute; left:0; right:0; top:50%; transform:translateY(-50%);
  margin:0; padding:0 24px; text-align:center;
  color:var(--text-muted);
  font-family:'IBM Plex Mono',monospace; font-size:13px; line-height:1.6;
  pointer-events:none; z-index:2;
}
/* pannellum ships a light-grey loading backdrop — keep it in the dark theme */
.vrStage .pnlm-container{ background:#12120f; }
.vrStage .pnlm-load-box{ display:none; }
