/* =============================================================================
   3rdi-indoor-map.css
   "The map does not stop at the front door." The enterprise page's indoor
   mapping section: five generated floor slabs stacked into an exploded
   isometric building that the reader scrubs with scroll, with the page's own
   gradient route threading indoors from the entrance to the room.

   THREE CONSTRAINTS SHAPE THIS FILE:

   1. html/body carry `overflow-x: hidden` (mobile invariant), which silently
      disables `position: sticky` site wide. The header already works around it
      with `position: fixed` (app.css ~267). This section pins the SAME way:
      3rdi-indoor-map.js flips one element between absolute (top 0), fixed
      (top var(--header-h)) and absolute (bottom 0) inside a tall track. The
      three states share left/right/height so there is no jump at either
      boundary.

   2. NOTHING between the pinned element and <body> may carry a transform,
      filter, backdrop-filter or will-change: any of those become the fixed
      containing block and the pin silently scrolls away with the page. That is
      why `.rv` (which animates a transform) is only ever on the section head,
      never on the track or the pin.

   3. Base styles here ARE the static layout: no JS and reduced motion both get
      a plain CSS stacked building with all four copy blocks and a popup legend
      below it. `.is-live` (added by the script) is what turns on the pin.

   Only transforms, opacity and filters animate. The scroll driver writes three
   custom properties on the section (--imap-gap, --imap-rotx, --imap-roty) plus
   data-beat (copy), data-stage (popups, markers, route) and data-lit (which
   floor is active), and everything else is CSS. The per frame work is the three
   custom properties and one dash offset; filters only change on a beat.
   ========================================================================== */

.thirdi-indoor-map {
  --imap-rail-w: 152px;
  --imap-rail-gap: 20px;
  /* Written per frame by the scroll driver. The defaults are the static state.
     The gap NEVER goes below about 14 percent, in any state, at any width: at a
     tighter gap the slab interiors disappear behind the floor above and the
     model reads as a stack of blank white trays instead of a building. */
  --imap-gap: 19%;
  --imap-rotx: 3deg;
  --imap-roty: -6deg;
  --imap-sx: 0;
  /* The pinned viewport height, in px, measured by the script (never a vh unit:
     mobile toolbars change vh mid scroll and the JS maths would drift). */
  --imap-pin-h: 720px;
  --imap-stack-w: min(560px, 74vw);
  --imap-card-w: 226px;
  /* Leader line from a popup card corner to its anchor on the building. The
     angle is fixed at 57.5 degrees (the ::after rotations below are authored to
     match), so a longer leader is the same vector scaled: this one is twice the
     original, which lifts the alarm card off the lit floor's interior now that
     the stack opens wider and the slabs stay opaque. */
  --imap-lead: 105px;
  --imap-lead-x: 56px;
  --imap-lead-y: 88px;
  /* SVG user units (viewBox is 1536 wide). At the 560px desktop model one unit
     is 0.365 device px, so 17 units is about 6px of visible route: the line has
     to read at a glance over a full opacity render, not just exist. The casing
     is the route plus 11 units, which is the 2px of separation on each side. */
  --imap-stroke: 17;
  --imap-stroke-case: 28;
  --imap-stroke-bed: 9;
  --imap-stroke-mark: 7;
  --imap-stroke-halo: 5;

  /* ---- the floor state language ----------------------------------------
     NEVER dim a floor with opacity. The renders are the selling point of this
     section and anything under about 0.9 turns the building into translucent
     fog (client review 2026-08-07). Non active floors take a mild desaturate
     and a small darken, the active floor takes a brightness and contrast lift
     plus a soft brand gradient edge glow, and everything stays opaque. The glow
     is three small radius drop shadows on the slab's own alpha silhouette:
     sky around the edge, royal underneath, a magenta rim on the shadow side.
     Blur radii stay small (site rule: no heavy blur shadows). */
  --imap-mute: saturate(0.78) brightness(0.93) contrast(1.02);
  --imap-lit: brightness(1.07) contrast(1.05) saturate(1.08);
  --imap-glow: drop-shadow(0 0 7px rgba(92, 193, 246, 0.55))
    drop-shadow(0 7px 12px rgba(73, 121, 239, 0.38))
    drop-shadow(2px 2px 3px rgba(243, 26, 145, 0.34));

  position: relative;
  padding: clamp(96px, 11vw, 168px) 0 clamp(80px, 9vw, 132px);
}

/* Light ground: the paper is bright, so the mute leans on desaturation and a
   touch of contrast rather than darkening (a dark slab on paper reads as a
   different material), and the glow needs less spread to stay visible. */
html[data-theme="light"] .thirdi-indoor-map {
  --imap-mute: saturate(0.72) brightness(0.965) contrast(1.05);
  --imap-lit: brightness(1.045) contrast(1.06) saturate(1.1);
  --imap-glow: drop-shadow(0 0 5px rgba(73, 121, 239, 0.34))
    drop-shadow(0 5px 9px rgba(46, 66, 130, 0.26))
    drop-shadow(2px 2px 3px rgba(243, 26, 145, 0.26));
}

/* Two very soft brand blooms so the pinned stage reads as its own room without
   adding a second grid on top of the document's city fabric. */
.thirdi-indoor-map__ambient {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.thirdi-indoor-map__ambient::before,
.thirdi-indoor-map__ambient::after {
  content: "";
  position: absolute;
  width: min(52vw, 720px);
  aspect-ratio: 1;
  border-radius: 50%;
}
.thirdi-indoor-map__ambient::before {
  left: -18vw;
  top: 12%;
  background: radial-gradient(circle, rgba(92, 193, 246, 0.09), transparent 66%);
}
.thirdi-indoor-map__ambient::after {
  right: -20vw;
  top: 44%;
  background: radial-gradient(circle, rgba(243, 26, 145, 0.075), transparent 66%);
}

.thirdi-indoor-map__head { position: relative; z-index: 1; }

/* ---------- the track and the pinned element ---------------------------- */

.thirdi-indoor-map__track {
  position: relative;
  z-index: 1;
  margin-top: clamp(40px, 5vw, 68px);
}

/* Live mode: the track is the scroll runway, four viewports tall. */
.thirdi-indoor-map.is-live .thirdi-indoor-map__track { height: 420vh; }

.thirdi-indoor-map.is-live {
  /* The model is sized off the pinned viewport, not the page width, and it must
     never overhang the pin (that is what would slide under the fixed header).
     Each slab's ink is 62.6 percent of its box height (measured off the source
     alpha masks), so at the widest gap in the scrub the visible stack is about
     1.145x its own width tall; 1.2 is that plus air for the active floor lift. */
  --imap-stack-w: min(560px, calc((var(--imap-pin-h) - 40px) / 1.2));
}

.thirdi-indoor-map.is-live .thirdi-indoor-map__pin {
  position: absolute;
  left: 0;
  right: 0;
  height: var(--imap-pin-h);
}
/* The three pin states. Identical box in all three, so the switch between
   absolute and fixed cannot shift a pixel. */
.thirdi-indoor-map.is-live .thirdi-indoor-map__pin.is-before { top: 0; }
.thirdi-indoor-map.is-live .thirdi-indoor-map__pin.is-after { bottom: 0; }
.thirdi-indoor-map.is-live .thirdi-indoor-map__pin.is-pinned {
  position: fixed;
  top: var(--header-h, 72px);
  bottom: auto;
}

.thirdi-indoor-map__frame {
  position: relative;
  width: min(calc(100% - 44px), var(--wrap));
  height: 100%;
  margin-inline: auto;
}

/* ---------- floor rail -------------------------------------------------- */

.thirdi-indoor-map__rail {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 clamp(24px, 3vw, 34px);
  padding: 0;
  /* Static layout: the rail lines up with the section head's text edge. */
  justify-content: flex-start;
  list-style: none;
}
.thirdi-indoor-map__rail li { display: block; min-width: 0; }

.thirdi-indoor-map.is-live .thirdi-indoor-map__rail {
  position: absolute;
  left: 0;
  top: 50%;
  z-index: 3;
  width: var(--imap-rail-w);
  margin: 0;
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: stretch;
  transform: translateY(-50%);
}

.thirdi-indoor-map__chip {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  min-height: 38px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel-2);
  color: var(--dim);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.055em;
  line-height: 1.15;
  text-align: left;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 260ms ease, border-color 260ms ease, background 260ms ease,
    opacity 420ms ease, transform 520ms cubic-bezier(0.16, 1, 0.3, 1);
}
.thirdi-indoor-map__chip:focus-visible {
  outline: 2px solid var(--acc-sky);
  outline-offset: 2px;
}
.thirdi-indoor-map__chipdot {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--soft);
  transition: background 260ms ease, box-shadow 260ms ease, transform 260ms ease;
}
.thirdi-indoor-map__chip.is-lit {
  border-color: var(--line-hi);
  background: var(--panel);
  color: var(--text);
}
.thirdi-indoor-map__chip.is-lit .thirdi-indoor-map__chipdot {
  background: var(--royal);
  box-shadow: 0 0 0 4px rgba(73, 121, 239, 0.16);
  transform: scale(1.14);
}
.thirdi-indoor-map__chip.is-target {
  border-color: transparent;
  background: linear-gradient(150deg, rgba(243, 26, 145, 0.16), var(--panel) 62%);
  color: var(--text);
}
.thirdi-indoor-map__chip.is-target .thirdi-indoor-map__chipdot {
  background: var(--magenta);
  box-shadow: 0 0 0 4px rgba(243, 26, 145, 0.16);
  transform: scale(1.14);
}

/* Beat 1 staggers the rail in as the blueprints "stand up". Expressed as a
   :not() so the visible state carries NO transform rule at all, and the handset
   media query only has to restate the hidden offset. */
.thirdi-indoor-map.is-live:not(.is-chips) .thirdi-indoor-map__chip {
  opacity: 0;
  transform: translateX(-14px);
}
.thirdi-indoor-map.is-live .thirdi-indoor-map__chip[data-floor="upper"] { transition-delay: 60ms; }
.thirdi-indoor-map.is-live .thirdi-indoor-map__chip[data-floor="main"] { transition-delay: 120ms; }
.thirdi-indoor-map.is-live .thirdi-indoor-map__chip[data-floor="ground"] { transition-delay: 180ms; }
.thirdi-indoor-map.is-live .thirdi-indoor-map__chip[data-floor="parking"] { transition-delay: 240ms; }

.thirdi-indoor-map__chipshort { display: none; }

/* ---------- stage region ------------------------------------------------ */

.thirdi-indoor-map__region { position: relative; }
.thirdi-indoor-map.is-live .thirdi-indoor-map__region {
  position: absolute;
  left: calc(var(--imap-rail-w) + var(--imap-rail-gap));
  right: 0;
  top: 0;
  bottom: 0;
}

/* Only the side switch lives on this element, so the per frame rotation below
   never inherits a transition and never lags behind the scroll. */
.thirdi-indoor-map__stagemove { position: relative; }
.thirdi-indoor-map.is-live .thirdi-indoor-map__stagemove {
  position: absolute;
  inset: 0;
  transform: translate3d(calc(var(--imap-sx) * ((100% - var(--imap-stack-w)) / 2 - 12px)), 0, 0);
  transition: transform 940ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* Which side the model sits on. Set as an attribute rather than an inline
   custom property so the handset media query can still zero it out. */
.thirdi-indoor-map.is-live[data-shift="right"] { --imap-sx: 1; }
.thirdi-indoor-map.is-live[data-shift="left"] { --imap-sx: -1; }

.thirdi-indoor-map__stage {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Static layout: at the static 19 percent gap the roof overhangs the stack box
     by about 0.13 of its width and the parking deck by about the same, so this is
     that overhang plus a little air, not an arbitrary pad.  */
  padding-block: calc(var(--imap-stack-w) * 0.17);
}
.thirdi-indoor-map.is-live .thirdi-indoor-map__stage {
  height: 100%;
  padding-block: 0;
}

.thirdi-indoor-map__stack {
  position: relative;
  /* flex-shrink 0 is load bearing: the stage is a centring flex row, and on a
     handset the model is deliberately WIDER than the frame (the render carries
     transparent margin, so its ink still lands inside the viewport). Without
     this the flex algorithm quietly shrank it back to the frame width and the
     mobile model stayed small. */
  flex: 0 0 auto;
  width: var(--imap-stack-w);
  aspect-ratio: 1536 / 1024;
  perspective: 1700px;
}

/* Dark ground: a cool ambient pool under the white model. Light ground: a soft
   tinted contact shadow, small blur only, so the slabs sit on the paper. */
.thirdi-indoor-map__stack::before {
  content: "";
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: -14%;
  height: 46%;
  z-index: 0;
  border-radius: 50%;
  background: radial-gradient(50% 50% at 50% 50%, rgba(73, 121, 239, 0.3), rgba(92, 193, 246, 0.1) 46%, transparent 72%);
  filter: blur(26px);
  pointer-events: none;
}
html[data-theme="light"] .thirdi-indoor-map__stack::before {
  bottom: -6%;
  height: 26%;
  background: radial-gradient(50% 50% at 50% 50%, rgba(46, 66, 130, 0.22), rgba(46, 66, 130, 0.08) 52%, transparent 74%);
  filter: blur(12px);
}

.thirdi-indoor-map__slabs {
  position: absolute;
  inset: 0;
  z-index: 1;
  transform-style: preserve-3d;
  transform: rotateX(var(--imap-rotx)) rotateY(var(--imap-roty));
}

.thirdi-indoor-map__floor {
  position: absolute;
  inset: 0;
  display: block;
  /* --imap-gap is a percentage, so the offset scales with the render height and
     the whole stack stays proportional at any width. Level 0 is the main floor,
     positive levels sit below it. */
  transform: translateY(calc(var(--lvl) * var(--imap-gap)));
}

.thirdi-indoor-map__slab {
  display: block;
  width: 100%;
  height: auto;
  /* Per slab alignment: the five renders share one camera but their footprints
     differ by up to 4% (the roof carries a parapet overhang, the parking deck
     has no walls). These values line every floor plate up on one centre line
     and one bottom corner, measured off the source alpha masks. */
  transform: translate(var(--fx, 0%), calc(var(--fy, 0%) + var(--imap-lift, 0%))) scale(var(--fs, 1));
  transition: filter 520ms ease, opacity 520ms ease,
    transform 620ms cubic-bezier(0.16, 1, 0.3, 1);
}
.thirdi-indoor-map__floor[data-floor="parking"] .thirdi-indoor-map__slab { --fx: 1.47%; --fy: 2.25%; --fs: 0.993; }
.thirdi-indoor-map__floor[data-floor="ground"] .thirdi-indoor-map__slab { --fx: 0.51%; --fy: -0.77%; --fs: 1.012; }
.thirdi-indoor-map__floor[data-floor="main"] .thirdi-indoor-map__slab { --fx: 0%; --fy: 0%; --fs: 1; }
.thirdi-indoor-map__floor[data-floor="upper"] .thirdi-indoor-map__slab { --fx: 0.32%; --fy: -1.27%; --fs: 1.009; }
.thirdi-indoor-map__floor[data-floor="roof"] .thirdi-indoor-map__slab { --fx: 0.18%; --fy: -0.81%; --fs: 0.965; }

/* Signal the active floor POSITIVELY. Everything stays at 0.94 or better: the
   old language dropped the other floors to 0.42 (0.5 on paper) and the building
   turned into fog, which is exactly what the renders are here to avoid. The
   quiet floors take --imap-mute, the active floor takes --imap-lit plus the
   gradient edge glow and a slightly stronger lift.

   Filters are state changes, not per frame work: the scroll driver only swaps
   data-lit four times across the whole scrub, so the one 520ms filter
   transition per beat is the entire cost. Nothing here animates on scroll. */
.thirdi-indoor-map[data-lit="main"] .thirdi-indoor-map__floor:not([data-floor="main"]) .thirdi-indoor-map__slab,
.thirdi-indoor-map[data-lit="route"] .thirdi-indoor-map__floor:not([data-floor="main"]):not([data-floor="ground"]) .thirdi-indoor-map__slab {
  opacity: 0.94;
  filter: var(--imap-mute);
}
.thirdi-indoor-map[data-lit="main"] .thirdi-indoor-map__floor[data-floor="main"] .thirdi-indoor-map__slab,
.thirdi-indoor-map[data-lit="route"] .thirdi-indoor-map__floor[data-floor="main"] .thirdi-indoor-map__slab,
.thirdi-indoor-map[data-lit="route"] .thirdi-indoor-map__floor[data-floor="ground"] .thirdi-indoor-map__slab {
  --imap-lift: -4.8%;
  filter: var(--imap-lit) var(--imap-glow);
}
/* The ground floor is the second half of the beat 3 pair (entrance plus room),
   so it lifts less: two floors lifting the same amount reads as a gap error. */
.thirdi-indoor-map[data-lit="route"] .thirdi-indoor-map__floor[data-floor="ground"] .thirdi-indoor-map__slab {
  --imap-lift: -2.6%;
}

/* Hovering or focusing a floor chip lifts that floor, on any beat. Same
   language as the beat highlight: lift the one, mute the rest, never fog. */
@media (hover: hover) and (pointer: fine) {
  .thirdi-indoor-map__chip:hover {
    border-color: var(--line-hi);
    background: var(--panel);
    color: var(--text);
  }
  .thirdi-indoor-map__chip:hover .thirdi-indoor-map__chipdot {
    background: var(--sky);
    box-shadow: 0 0 0 4px rgba(92, 193, 246, 0.16);
    transform: scale(1.14);
  }
  .thirdi-indoor-map.is-live[data-hover] .thirdi-indoor-map__slab {
    opacity: 0.94;
    filter: var(--imap-mute);
  }
  .thirdi-indoor-map.is-live[data-hover="roof"] .thirdi-indoor-map__floor[data-floor="roof"] .thirdi-indoor-map__slab,
  .thirdi-indoor-map.is-live[data-hover="upper"] .thirdi-indoor-map__floor[data-floor="upper"] .thirdi-indoor-map__slab,
  .thirdi-indoor-map.is-live[data-hover="main"] .thirdi-indoor-map__floor[data-floor="main"] .thirdi-indoor-map__slab,
  .thirdi-indoor-map.is-live[data-hover="ground"] .thirdi-indoor-map__floor[data-floor="ground"] .thirdi-indoor-map__slab,
  .thirdi-indoor-map.is-live[data-hover="parking"] .thirdi-indoor-map__floor[data-floor="parking"] .thirdi-indoor-map__slab {
    opacity: 1;
    --imap-lift: -4.2%;
    filter: var(--imap-lit) var(--imap-glow);
  }
}

/* ---------- the indoor route ------------------------------------------- */

/* Inside the rotated group so the route stays in the building's space, lifted
   1px on Z so it paints over every slab (z-index is ignored between siblings
   in a preserve-3d context). */
.thirdi-indoor-map__route {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  overflow: visible;
  transform: translateZ(1px);
  pointer-events: none;
}
/* Stroke widths are in the SVG's own user units, NOT non-scaling-stroke: with
   vector-effect: non-scaling-stroke the dash array and dash offset are resolved
   in device pixels instead of user units, which turns a pathLength="1" scrub
   into a 1px dotted line that looks fully drawn from the first frame. Verified
   2026-08-07. The trade is that the line scales with the model, which is what a
   route drawn on a plan should do anyway. */
.thirdi-indoor-map__routebed,
.thirdi-indoor-map__routecase,
.thirdi-indoor-map__routeline {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.thirdi-indoor-map__routebed {
  stroke: rgba(255, 255, 255, 0.16);
  stroke-width: var(--imap-stroke-bed);
  opacity: 0;
  transition: opacity 520ms ease;
}
html[data-theme="light"] .thirdi-indoor-map__routebed { stroke: rgba(28, 40, 82, 0.18); }

/* The casing. Now that every floor stays opaque, the gradient route has to be
   separated from the render underneath it: this is the same 2px-a-side casing a
   printed map puts under a highway. WHITE in BOTH themes: the route spends its
   life crossing the white building, and a dark casing there read as a black
   smear on the model (Adam flagged it live, 2026-08-07). Over the dark page
   ground the white casing reads as a soft halo, which is fine. It carries the
   identical pathLength=1 dash scrub as the line, set by the same driver, so it
   can never run ahead of the route it is under. */
.thirdi-indoor-map__routecase {
  stroke: rgba(255, 255, 255, 0.82);
  stroke-width: var(--imap-stroke-case);
  opacity: 0;
  transition: opacity 420ms ease;
}
html[data-theme="light"] .thirdi-indoor-map__routecase { stroke: rgba(255, 255, 255, 0.94); }
.thirdi-indoor-map__routeline {
  stroke: url("#imapRouteGrad");
  stroke-width: var(--imap-stroke);
  filter: drop-shadow(var(--route-shadow));
  opacity: 0;
  transition: opacity 420ms ease;
}
.thirdi-indoor-map[data-stage="3"] .thirdi-indoor-map__routebed,
.thirdi-indoor-map[data-stage="4"] .thirdi-indoor-map__routebed { opacity: 1; }
.thirdi-indoor-map[data-stage="3"] .thirdi-indoor-map__routeline,
.thirdi-indoor-map[data-stage="3"] .thirdi-indoor-map__routecase { opacity: 1; }
/* Beat 4 settles the stack back down, so the drawn route is running across
   plates that sit closer together. Easing it back lets the closing statement
   lead, but not far enough to lose the line. */
.thirdi-indoor-map[data-stage="4"] .thirdi-indoor-map__routeline { opacity: 0.86; }
.thirdi-indoor-map[data-stage="4"] .thirdi-indoor-map__routecase { opacity: 0.8; }

/* The white leading point, the same device the page ribbon uses. */
.thirdi-indoor-map__routetip {
  fill: #ffffff;
  filter: drop-shadow(0 0 6px rgba(92, 193, 246, 0.9));
  opacity: 0;
  transition: opacity 300ms ease;
}
html[data-theme="light"] .thirdi-indoor-map__routetip {
  fill: var(--royal);
  filter: drop-shadow(0 0 5px rgba(73, 121, 239, 0.5));
}
.thirdi-indoor-map.is-drawing .thirdi-indoor-map__routetip { opacity: 1; }

.thirdi-indoor-map__mark { opacity: 0; transition: opacity 460ms ease; }
.thirdi-indoor-map[data-stage="2"] .thirdi-indoor-map__mark--room,
.thirdi-indoor-map[data-stage="3"] .thirdi-indoor-map__mark,
.thirdi-indoor-map[data-stage="4"] .thirdi-indoor-map__mark { opacity: 1; }

.thirdi-indoor-map__markhalo { fill: none; stroke-width: var(--imap-stroke-halo); }
.thirdi-indoor-map__markdot { stroke: #ffffff; stroke-width: var(--imap-stroke-mark); }
.thirdi-indoor-map__mark--entrance .thirdi-indoor-map__markdot { fill: var(--sky); }
.thirdi-indoor-map__mark--entrance .thirdi-indoor-map__markhalo { stroke: rgba(92, 193, 246, 0.5); }
.thirdi-indoor-map__mark--stair .thirdi-indoor-map__markdot { fill: var(--royal); }
.thirdi-indoor-map__mark--stair .thirdi-indoor-map__markhalo { stroke: rgba(73, 121, 239, 0.5); }
.thirdi-indoor-map__mark--room .thirdi-indoor-map__markdot { fill: var(--magenta); }
.thirdi-indoor-map__mark--room .thirdi-indoor-map__markhalo { stroke: rgba(243, 26, 145, 0.55); }

/* ---------- popups ----------------------------------------------------- */

/* A flat layer, deliberately OUTSIDE the perspective container: the cards are
   type, and 8 degrees of rotateY on a text card looks like a mistake. Their
   anchor dots live in the route SVG instead, so they do rotate with the model. */
.thirdi-indoor-map__pins {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}
.thirdi-indoor-map.is-live .thirdi-indoor-map__pins { display: block; }

/* A zero width column at the anchor's x, full stack height, carrying the floor
   offset. Its own height is the stack box height, so the percentage offset
   resolves against exactly the same base as the slab it belongs to. */
.thirdi-indoor-map__pin-anchor {
  position: absolute;
  left: var(--ax);
  top: 0;
  width: 0;
  height: 100%;
  transform: translateY(calc(var(--lvl) * var(--imap-gap)));
}

.thirdi-indoor-map__pop {
  position: absolute;
  top: var(--ay);
  left: 0;
  width: var(--imap-card-w);
  padding: 13px 15px 14px;
  border: 1px solid var(--line-hi);
  border-radius: 15px;
  background: var(--glass);
  box-shadow: 0 14px 30px -16px rgba(8, 14, 32, 0.5);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  backdrop-filter: blur(14px) saturate(150%);
  opacity: 0;
  visibility: hidden;
  transition: opacity 460ms ease,
    transform 620ms cubic-bezier(0.16, 1, 0.3, 1),
    visibility 0s linear 620ms;
}
html[data-theme="light"] .thirdi-indoor-map__pop {
  box-shadow: 0 14px 30px -18px rgba(40, 64, 140, 0.32);
}

/* Two leader geometries, mirrored. The card sits one leader vector away from
   its anchor, so the 2px gradient line always lands exactly on the dot. */
.thirdi-indoor-map__pop--upleft {
  transform: translate(-100%, -100%) translate(calc(-1 * var(--imap-lead-x)), calc(18px - var(--imap-lead-y)));
}
.thirdi-indoor-map__pop--upleft.is-on {
  transform: translate(-100%, -100%) translate(calc(-1 * var(--imap-lead-x)), calc(-1 * var(--imap-lead-y)));
}
.thirdi-indoor-map__pop--downright {
  /* Its leader points down, where there is far less air than above the roof, so
     it keeps a short vector at the same 57.5 degree angle. */
  --imap-lead: 63px;
  --imap-lead-x: 34px;
  --imap-lead-y: 53px;
  transform: translate(var(--imap-lead-x), calc(var(--imap-lead-y) + 18px));
}
.thirdi-indoor-map__pop--downright.is-on {
  transform: translate(var(--imap-lead-x), var(--imap-lead-y));
}
.thirdi-indoor-map__pop.is-on {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s, 0s, 0s;
}

.thirdi-indoor-map__pop::after {
  content: "";
  position: absolute;
  width: var(--imap-lead);
  height: 2px;
  border-radius: 2px;
  transform-origin: 0 50%;
}
.thirdi-indoor-map__pop--upleft::after {
  left: 100%;
  top: 100%;
  background: linear-gradient(90deg, var(--royal), var(--magenta));
  transform: rotate(57.5deg);
}
.thirdi-indoor-map__pop--downright::after {
  left: 0;
  top: 0;
  background: linear-gradient(90deg, var(--royal), var(--sky));
  transform: rotate(-122.5deg);
}

.thirdi-indoor-map__poplabel {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  color: var(--dim);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.13em;
  line-height: 1.2;
  text-transform: uppercase;
}
.thirdi-indoor-map__poplabel .pip { flex: 0 0 auto; }
.thirdi-indoor-map__poptitle {
  margin: 8px 0 0;
  color: var(--text);
  font-size: 17px;
  font-weight: 640;
  letter-spacing: -0.02em;
  line-height: 1.15;
}
.thirdi-indoor-map__popsub {
  margin: 5px 0 0;
  color: var(--dim);
  font-size: 13px;
  line-height: 1.42;
}
/* The entrance callout is the sky end of the route, so its pip matches its own
   marker rather than the alarm's magenta. On a handset, where the leader lines
   are dropped, the pip colour IS the tie between card and marker. */
.thirdi-indoor-map__pop--entrance .pip {
  background: var(--sky);
  box-shadow: 0 0 0 4px rgba(92, 193, 246, 0.14);
}

/* The secondary beat 3 chip: a quiet line under the model, not a card. */
.thirdi-indoor-map__note {
  position: absolute;
  left: 50%;
  bottom: 20px;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 9px;
  max-width: min(94%, 420px);
  padding: 9px 16px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel-2);
  color: var(--dim);
  font-size: 12.5px;
  line-height: 1.4;
  text-align: left;
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, 14px);
  transition: opacity 460ms ease,
    transform 620ms cubic-bezier(0.16, 1, 0.3, 1),
    visibility 0s linear 620ms;
}
.thirdi-indoor-map__note.is-on {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
  transition-delay: 0s, 0s, 0s;
}
.thirdi-indoor-map__notedash {
  flex: 0 0 auto;
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--sky), var(--royal));
}

/* ---------- the four copy blocks --------------------------------------- */

.thirdi-indoor-map__copyset {
  display: grid;
  gap: clamp(26px, 3.4vw, 40px);
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: clamp(34px, 4vw, 52px);
}
.thirdi-indoor-map.is-live .thirdi-indoor-map__copyset {
  display: block;
  margin: 0;
}

.thirdi-indoor-map__copy { position: relative; }
.thirdi-indoor-map.is-live .thirdi-indoor-map__copy {
  position: absolute;
  top: 50%;
  z-index: 3;
  width: calc(100% - var(--imap-stack-w) - 44px);
  max-width: 440px;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-50%) translateY(18px);
  transition: opacity 500ms ease,
    transform 700ms cubic-bezier(0.16, 1, 0.3, 1),
    visibility 0s linear 700ms;
}
.thirdi-indoor-map.is-live .thirdi-indoor-map__copy[data-side="left"] { left: 0; }
.thirdi-indoor-map.is-live .thirdi-indoor-map__copy[data-side="right"] { right: 0; }
.thirdi-indoor-map.is-live .thirdi-indoor-map__copy.is-on {
  opacity: 1;
  visibility: visible;
  transform: translateY(-50%);
  transition-delay: 0s, 0s, 0s;
}

/* Scoped with the parent class on purpose: the step label is a <p> inside
   .thirdi-indoor-map__copy, and the block-copy rule below (one class + one
   element) would otherwise win the cascade and render this 15.5px label copy
   instead of an 11px street sign. */
.thirdi-indoor-map__copy .thirdi-indoor-map__step {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 12px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  line-height: 1.2;
  text-transform: uppercase;
}
.thirdi-indoor-map__stepnum {
  color: var(--soft);
  font-variant-numeric: tabular-nums;
}
.thirdi-indoor-map__steprule {
  width: 22px;
  height: 2px;
  border-radius: 2px;
  background: var(--grad);
}
.thirdi-indoor-map__stepname {
  background: var(--grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.thirdi-indoor-map__copy h3 {
  margin: 0;
  max-width: 20ch;
  font-size: clamp(24px, 2.5vw, 33px);
  letter-spacing: -0.032em;
  line-height: 1.08;
}
.thirdi-indoor-map__copy p {
  margin: 16px 0 0;
  max-width: 38rem;
  color: var(--dim);
  font-size: 15.5px;
  line-height: 1.6;
}

/* ---------- popup legend (static and reduced motion only) -------------- */

.thirdi-indoor-map__legendwrap { margin-top: clamp(30px, 3.6vw, 44px); }
.thirdi-indoor-map__legendwrap .eyebrow { margin: 0 0 14px; }
.thirdi-indoor-map__legend {
  display: grid;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.thirdi-indoor-map__legend li {
  display: grid;
  grid-template-columns: 12px minmax(0, 1fr);
  gap: 12px;
  align-items: baseline;
  color: var(--dim);
  font-size: 14.5px;
  line-height: 1.55;
}
.thirdi-indoor-map__legend li::before {
  content: "";
  width: 8px;
  height: 8px;
  margin-top: 0.45em;
  border-radius: 50%;
  background: var(--grad);
}
.thirdi-indoor-map__legend b { color: var(--text); font-weight: 620; }
/* In live mode the popups carry these callouts visually, and they are
   aria-hidden decoration, so the legend stays in the accessibility tree as the
   text version rather than being removed with display: none. */
.thirdi-indoor-map.is-live .thirdi-indoor-map__legendwrap {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}
.thirdi-indoor-map:not(.is-live) .thirdi-indoor-map__pins,
.thirdi-indoor-map:not(.is-live) .thirdi-indoor-map__note { display: none; }
/* No JS and reduced motion both show the finished route, not an empty one. The
   dash offset has to be cleared here too: the markup ships it at 1 so the line
   is hidden before the first scroll frame, and with JS off nothing would ever
   set it back. */
.thirdi-indoor-map:not(.is-live) .thirdi-indoor-map__routebed,
.thirdi-indoor-map:not(.is-live) .thirdi-indoor-map__routecase,
.thirdi-indoor-map:not(.is-live) .thirdi-indoor-map__routeline,
.thirdi-indoor-map:not(.is-live) .thirdi-indoor-map__mark { opacity: 1; }
.thirdi-indoor-map:not(.is-live) .thirdi-indoor-map__routecase,
.thirdi-indoor-map:not(.is-live) .thirdi-indoor-map__routeline { stroke-dashoffset: 0; }

.thirdi-indoor-map__close {
  position: relative;
  z-index: 1;
  margin-top: clamp(34px, 4vw, 56px);
}

/* ---------- tablet ----------------------------------------------------- */

@media (max-width: 1120px) {
  .thirdi-indoor-map { --imap-rail-w: 142px; --imap-card-w: 208px; }
  .thirdi-indoor-map.is-live .thirdi-indoor-map__copy { max-width: 380px; }
}

/* ---------- handset: model, chips, copy card as ONE stacked unit --------

   The three pieces are docked bottom up from the copy card, not spread down
   from the top of the pin: model, then chips, then card, with every gap fixed
   in px. Client review 2026-08-07 called out big empty vertical bands between
   them, and those bands were the old layout centring each piece in its own
   share of the pinned height. Any slack now collects in ONE place, above the
   model, where it reads as air under the header instead of a hole. */

@media (max-width: 900px) {
  .thirdi-indoor-map {
    /* Wide enough that "NEAREST ENTRANCE" holds one line, capped in vw so the
       two top cards cannot meet in the middle on a 320px handset. */
    --imap-card-w: min(168px, 43vw);
    --imap-stack-w: min(max(408px, 62vw), 104vw);
    /* The model is smaller here, so the same visible weight needs more user
       units of stroke: 26 units on a 405px model is about 6.5px of route, which
       is what it takes to read at a glance at 390px. */
    --imap-stroke: 26;
    --imap-stroke-case: 43;
    --imap-stroke-bed: 14;
    /* The marker groups are scaled up 1.75x by the driver on a handset, so the
       ring weights come DOWN to compensate; the dots themselves get bigger. */
    --imap-stroke-mark: 4;
    --imap-stroke-halo: 3.2;
    /* Height the bottom copy card reserves, tallest beat included (the card
       carries a min-height so all four beats agree). Everything above it is
       positioned off this one number. The driver overwrites it with the measured
       tallest card on every resize, because the line count changes with the
       viewport; this value is only the pre measurement default. */
    --imap-copy-h: 256px;
    /* The visible ink of a slab is 62.6 percent of its box height, so the fully
       exploded stack stands 1.06x its own width tall at the handset gap keys.
       That, not the box, is what the layout budgets for. */
    --imap-model-h: calc(var(--imap-stack-w) * 1.06);
  }
  .thirdi-indoor-map.is-live,
  .thirdi-indoor-map.is-live[data-shift="right"],
  .thirdi-indoor-map.is-live[data-shift="left"] {
    --imap-sx: 0;
    /* The model may bleed wider than the content wrap (the render carries about
       7 percent of transparent margin on each side, so the ink still lands
       inside the viewport), but never taller than the space above the chips. */
    --imap-stack-w: min(
      max(408px, 62vw),
      104vw,
      calc((var(--imap-pin-h) - var(--imap-copy-h) - 46px) / 1.06)
    );
  }
  .thirdi-indoor-map.is-live .thirdi-indoor-map__track { height: 340vh; }
  .thirdi-indoor-map__copyset { grid-template-columns: 1fr; }
  .thirdi-indoor-map__frame { width: min(calc(100% - 32px), var(--wrap)); }

  /* The region stays FULL height so the bottom copy card can dock against the
     bottom of the pin; it is the stage that gets capped, not the region. Sizing
     the region to the stage height was the bug that put the card on top of the
     building. */
  .thirdi-indoor-map.is-live .thirdi-indoor-map__region { left: 0; }
  .thirdi-indoor-map.is-live .thirdi-indoor-map__stagemove {
    inset: auto 0 calc(var(--imap-copy-h) + 42px);
    height: var(--imap-model-h);
    transition: none;
  }

  /* The rail becomes a single centred row sitting directly under the model's
     ink, 10px clear of it, with the copy card 10px under that. */
  .thirdi-indoor-map.is-live .thirdi-indoor-map__rail {
    left: 0;
    top: auto;
    bottom: calc(var(--imap-copy-h) + 4px);
    width: 100%;
    flex-direction: row;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 6px;
    transform: none;
  }
  .thirdi-indoor-map.is-live .thirdi-indoor-map__chip {
    width: auto;
    min-height: 32px;
    padding: 0 9px;
    gap: 6px;
    border-radius: 10px;
    font-size: 9.5px;
    letter-spacing: 0.05em;
  }
  .thirdi-indoor-map.is-live:not(.is-chips) .thirdi-indoor-map__chip { transform: translateY(10px); }
  .thirdi-indoor-map.is-live .thirdi-indoor-map__chipdot { width: 6px; height: 6px; }
  /* Short labels on a handset: five full floor names do not fit one row. */
  .thirdi-indoor-map.is-live .thirdi-indoor-map__chiplong { display: none; }
  .thirdi-indoor-map.is-live .thirdi-indoor-map__chipshort { display: inline; }

  .thirdi-indoor-map.is-live .thirdi-indoor-map__copy {
    left: 0;
    right: 0;
    top: auto;
    bottom: 10px;
    width: auto;
    max-width: none;
    /* Every beat's card is the same height, so the chips and the model above it
       never move between beats and no beat leaves a band under the card. The
       number is the tallest beat (04, six lines of body copy) measured at
       390px. */
    min-height: calc(var(--imap-copy-h) - 10px);
    /* Centred inside that fixed height so the shorter beats read as a card with
       generous padding rather than a card with a hole in the bottom. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 18px 18px 20px;
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--panel);
    box-shadow: var(--soft-shadow);
    transform: translateY(16px);
  }
  .thirdi-indoor-map.is-live .thirdi-indoor-map__copy.is-on { transform: none; }
  .thirdi-indoor-map__copy h3 { max-width: none; font-size: 20.5px; }
  .thirdi-indoor-map__copy p { margin-top: 10px; font-size: 14px; line-height: 1.52; }
  /* Tightened so the longest step name ("The route continues indoors") holds to
     one line inside the bottom card. */
  .thirdi-indoor-map__copy .thirdi-indoor-map__step {
    margin-bottom: 9px;
    gap: 8px;
    font-size: 10px;
    letter-spacing: 0.08em;
  }
  .thirdi-indoor-map__steprule { width: 18px; }

  /* One line of secondary copy is enough on a handset: the same point is made
     in the beat 3 paragraph. */
  .thirdi-indoor-map__note { display: none; }

  /* ---- handset popups: two corner cards, no leader lines ---------------
     At 390px a leader that reaches its marker has to cross the whole model, and
     a 2px gradient line lying across the plates reads as a second route. So the
     cards move to the two corners of the stage that are air (the isometric
     silhouette leaves both), the leader is dropped, and the coloured pip in the
     card's label is what ties it to its marker: magenta for the alarm and the
     room, sky for the entrance. The alarm card was covering the upper floors of
     the model, which is the whole reason this block exists (client review
     2026-08-07); at these offsets the worst it touches is the roof edge.

     Offsets are percentages of the STACK BOX, not px, so they scale with the
     model. The box itself is wider than the viewport by design, hence the 7 to
     8 percent insets: they land the cards inside the visible frame. */
  .thirdi-indoor-map.is-live .thirdi-indoor-map__pin-anchor {
    left: 0;
    right: 0;
    top: 0;
    bottom: auto;
    width: auto;
    height: 0;
    transform: none;
  }
  .thirdi-indoor-map.is-live .thirdi-indoor-map__pop {
    top: auto;
    bottom: auto;
    left: auto;
    right: auto;
    transform: translateY(14px);
  }
  .thirdi-indoor-map.is-live .thirdi-indoor-map__pop.is-on { transform: none; }
  /* BOTH cards live in the band above the building. Below the roof there is no
     band tall enough for a card that does not sit on the main floor or the
     ground floor, and those two are the subject of the beats the cards belong
     to. Staggered by 22px so they read as two callouts, not a banner. */
  /* Round 3 (Adam): on real phones, browser toolbars shrink the visual
     viewport and the negative-percentage tops (measured off the floor anchors)
     poked the cards under the header. The anchors are zero-height at the STAGE
     top on handsets now, so the card tops are plain pixels below the pinned
     stage's top edge and no viewport shape can push them off screen. */
  .thirdi-indoor-map.is-live [data-indoor-map-anchor="room"] .thirdi-indoor-map__pop {
    top: 10px;
    left: 12px;
  }
  .thirdi-indoor-map.is-live [data-indoor-map-anchor="entrance"] .thirdi-indoor-map__pop {
    top: 10px;
    right: 12px;
  }
  /* Guard rails (client review 2026-08-07, round 2): the % insets were measured
     against the stack box, which is wider than the viewport, so on some phones
     the right card clipped off screen; and stage 3 turns on BOTH cards, which
     cannot share a 390px row without colliding. Pixel insets anchor the cards
     to the visible frame on every device, the clamp keeps any card narrower
     than the frame, and on handsets the alarm card yields stage 3 to the
     entrance card (its content already owned stage 2; stage 3 is the
     entrance/route beat). */
  .thirdi-indoor-map.is-live .thirdi-indoor-map__pop {
    max-width: min(176px, calc(100vw - 48px));
  }
  .thirdi-indoor-map.is-live[data-stage="3"]
    [data-indoor-map-anchor="room"]
    .thirdi-indoor-map__pop[data-indoor-map-pop="2 3"] {
    opacity: 0;
    visibility: hidden;
  }
  .thirdi-indoor-map__pop { padding: 10px 12px 11px; border-radius: 13px; }
  .thirdi-indoor-map__poplabel { font-size: 9.5px; letter-spacing: 0.1em; gap: 6px; }
  .thirdi-indoor-map__poptitle { margin-top: 6px; font-size: 14.5px; }
  .thirdi-indoor-map__popsub { margin-top: 4px; font-size: 12px; line-height: 1.35; }
  .thirdi-indoor-map__pop::after { display: none; }
}

@media (max-width: 420px) {
  .thirdi-indoor-map.is-live .thirdi-indoor-map__chip { padding: 0 7px; font-size: 9px; }
}

/* ---------- large monitors (app.css 1720px pass) ----------------------- */

@media (min-width: 1720px) {
  .thirdi-indoor-map { --imap-rail-w: 168px; --imap-card-w: 244px; }
  .thirdi-indoor-map.is-live .thirdi-indoor-map__copy { max-width: 480px; }
}

/* ---------- reduced motion --------------------------------------------- */

/* The script never adds .is-live under reduced motion, so the static stacked
   layout above is what renders. This block only makes sure nothing left over
   animates. */
@media (prefers-reduced-motion: reduce) {
  .thirdi-indoor-map__slab,
  .thirdi-indoor-map__chip,
  .thirdi-indoor-map__pop,
  .thirdi-indoor-map__copy,
  .thirdi-indoor-map__note,
  .thirdi-indoor-map__stagemove {
    transition: none !important;
  }
}
