/* Ported VERBATIM from the approved prototype's component layer into the mxv-digital-tour plugin
   at D3232. Component rules are byte-identical.
   The source repository, branch and commit used to be named on this line. They are not any more:
   this file is served anonymously at 65,662 bytes on a client production domain, and it answers
   200 with the plugin DEACTIVATED (gray-fox N-1). The provenance lives in seed/SOURCE.md and in
   the commit history, neither of which ships.
   Dropped, and staying dropped: the prototype's own review chrome (.fm-protobar*, .fm-archetype*).
   The CMS never emits those markers and WordPress supplies the real chrome.
   Additions live in the supplements block at the end and nowhere else. */

/* MxV Rail — facility map prototype component layer.
   Tokens lifted from the live wsk-theme computed styles (same set the
   2026-08 design pack used), not invented. Everything new is namespaced
   .fm- so nothing collides with the theme. */

:root{
  --fm-ink:#10171F;
  --fm-white:#FFFFFF;
  --fm-page:#E9EAE8;
  --fm-panel:#F6F7F6;
  --fm-muted:#707070;
  --fm-hairline:#BCBCBC;
  --fm-hairline-soft:#E3E4E2;
  --fm-teal:#1F9EA8;
  --fm-teal-deep:#234B5A;
  --fm-coral:#F95341;
  --fm-gap:24px;
  --fm-ease:cubic-bezier(.22,.61,.36,1);
  --fm-stage-radius:0px;
}

/* ── page shell ──────────────────────────────────────────────────────────── */
.fm-lede{font:300 clamp(17px,.5vw + 15px,20px)/1.55 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 8px;max-width:64ch}
.fm-eyebrow{font:700 12px/1 "museo-sans",sans-serif;letter-spacing:.12em;text-transform:uppercase;color:var(--fm-muted);margin:0 0 12px}

/* ── the two-column map layout: stage + info panel ───────────────────────── */
.fm-layout{display:grid;gap:var(--fm-gap);grid-template-columns:1fr;align-items:start}
@media (min-width:992px){
  .fm-layout{grid-template-columns:minmax(0,1fr) 348px;gap:32px}
  .fm-children,.fm-factband{grid-column:1 / -1}
}

/* full-width fact band: five sourced facts read across, not down a narrow rail */
.fm-factband{margin-top:8px}
.fm-factband .fm-facts{border-top:0;display:grid;gap:0 32px}
@media (min-width:768px){
  .fm-factband .fm-facts{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}
  .fm-factband .fm-facts li{border-top:2px solid var(--fm-ink);border-bottom:0;padding:14px 0 18px 0}
  .fm-factband .fm-facts li::before{display:none}
}

/* ── image stage ─────────────────────────────────────────────────────────── */
.fm-stage{position:relative;overflow:hidden;background:var(--fm-page);border-radius:var(--fm-stage-radius)}
.fm-stage__img{display:block;width:100%;height:auto;aspect-ratio:16/10;object-fit:cover}
.fm-stage__vignette{position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(to bottom,rgba(16,23,31,0) 55%,rgba(16,23,31,.35) 100%)}

/* hotspot pin. The pin is a 44px PUCK and nothing else: the label is taken out
   of flow so it cannot change the button's box.

   It was previously a flex row [dot][label] with the label at opacity 0 — which
   still occupies layout width, because opacity and transform do not remove a box.
   translate(-50%,-50%) therefore centred the dot PLUS the label on the
   coordinate, displacing the visible dot by half the label's width. Measured
   across the fifteen navigating pins: button boxes 160-340px wide, dots 58-148px
   off their own coordinate, and three pins clipped by the right edge of the
   stage. No check could see it — the build arm validates percentages, not
   rendering, and a probe reading the BUTTON box reads a box that is centred on
   the coordinate by construction. Measure the dot. */
.fm-pin{position:absolute;transform:translate(-50%,-50%);
  width:44px;height:44px;padding:0;
  display:grid;place-items:center;
  border:0;background:transparent;cursor:pointer;
  color:var(--fm-white);text-align:left;-webkit-tap-highlight-color:transparent}
.fm-pin__dot{position:relative;width:22px;height:22px;border-radius:50%;
  background:var(--fm-accent,var(--fm-coral));
  box-shadow:0 0 0 4px rgba(249,83,65,.28),0 2px 10px rgba(16,23,31,.45);
  transition:transform .2s var(--fm-ease),box-shadow .2s var(--fm-ease)}
.fm-pin__dot::after{content:"";position:absolute;inset:-6px;border-radius:50%;
  border:2px solid var(--fm-accent,var(--fm-coral));opacity:0;
  animation:fm-pulse 2.6s var(--fm-ease) infinite}
@keyframes fm-pulse{
  0%{transform:scale(.85);opacity:.75}
  70%{transform:scale(1.5);opacity:0}
  100%{transform:scale(1.5);opacity:0}
}
/* P2. PERSISTENT labels. Eleven identical dots with hover-only names are not
   legible on a projected demo, and it is worst where the drawing is most
   abstract — seven of the eleven sit in the building cluster. The label is still
   position:absolute so it cannot alter the 44px puck, which remains the hit
   target and the coordinate anchor; only its PLACEMENT is chosen, by a runtime
   pass that measures each label and takes the first candidate that stays inside
   the stage and clears every label and puck already placed. A pin with no
   collision-free candidate keeps the hover-only behaviour and is counted, rather
   than shipping overlap. */
.fm-pin__label{position:absolute;
  font:500 13px/1.15 "museo-sans",sans-serif;letter-spacing:.005em;
  padding:6px 10px;pointer-events:none;
  /* Long names wrap rather than running on. "Heavy Duty Maintenance Facility"
     is 220px on one line and will not fit beside its pin in the building
     cluster; at this cap it is ~150px and two lines, and the cluster is dense
     HORIZONTALLY, so a narrower, taller box is the one that places. */
  white-space:normal;max-width:150px;text-wrap:balance;
  background:var(--fm-white);color:var(--fm-ink);
  border:1px solid var(--fm-hairline);border-radius:3px;
  box-shadow:0 1px 5px rgba(16,23,31,.16);
  opacity:1;
  transition:background-color .18s var(--fm-ease),color .18s var(--fm-ease),
             border-color .18s var(--fm-ease),box-shadow .18s var(--fm-ease),opacity .18s var(--fm-ease)}

/* placement modifiers — assigned by archetypes.js, never authored per pin */
.fm-pin__label--r{left:calc(100% - 5px);top:50%;transform:translateY(-50%)}
.fm-pin__label--l{right:calc(100% - 5px);top:50%;transform:translateY(-50%)}
.fm-pin__label--b{left:50%;top:calc(100% - 7px);transform:translateX(-50%)}
.fm-pin__label--t{left:50%;bottom:calc(100% - 7px);transform:translateX(-50%)}
.fm-pin__label--br{left:calc(50% - 6px);top:calc(100% - 7px)}
.fm-pin__label--bl{right:calc(50% - 6px);top:calc(100% - 7px)}
.fm-pin__label--tr{left:calc(50% - 6px);bottom:calc(100% - 7px)}
.fm-pin__label--tl{right:calc(50% - 6px);bottom:calc(100% - 7px)}

/* the richer state stays on hover/focus: the pill inverts and lifts */
.fm-pin:hover,.fm-pin:focus-visible{z-index:6}
.fm-pin:hover .fm-pin__label,.fm-pin:focus-visible .fm-pin__label{
  background:var(--fm-ink);color:var(--fm-white);border-color:var(--fm-ink);
  box-shadow:0 3px 14px rgba(16,23,31,.4);opacity:1}
.fm-pin:hover .fm-pin__dot,.fm-pin:focus-visible .fm-pin__dot{transform:scale(1.15);
  box-shadow:0 0 0 6px rgba(249,83,65,.3),0 3px 14px rgba(16,23,31,.5)}
.fm-pin:focus-visible{outline:2px solid var(--fm-ink);outline-offset:3px}

/* a pin the placement pass could not fit reverts to hover-only rather than
   overlapping a neighbour */
.fm-pin--hoveronly .fm-pin__label{opacity:0}
.fm-pin--hoveronly:hover .fm-pin__label,.fm-pin--hoveronly:focus-visible .fm-pin__label{opacity:1}

/* below the stage breakpoint the card directory replaces the plan entirely */
@media (max-width:991px){ .fm-pin__label{display:none} }

/* ── info panel ──────────────────────────────────────────────────────────── */
.fm-panel{background:var(--fm-panel);padding:28px;align-self:start}
.fm-panel__title{font:100 clamp(26px,1.1vw + 20px,32px)/1.2 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 14px}
.fm-panel__blurb{font:300 16px/1.6 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 20px}
/* ── equipment overlay card ──────────────────────────────────────────────── */
.fm-overlay{position:relative}
.fm-overlay__card{background:var(--fm-white);padding:32px;border-top:3px solid var(--fm-coral)}
@media (min-width:992px){
  .fm-overlay__card{position:absolute;right:32px;bottom:32px;width:min(430px,46%);
    box-shadow:0 18px 48px rgba(16,23,31,.28)}
}
.fm-overlay__title{font:100 clamp(26px,1.1vw + 20px,34px)/1.18 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 14px}
.fm-overlay__desc{font:300 16px/1.6 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 20px}
.fm-specs{margin:0 0 24px;padding:0;border-top:1px solid var(--fm-hairline-soft)}
.fm-specs__row{display:grid;grid-template-columns:38% 1fr;gap:16px;
  padding:11px 0;border-bottom:1px solid var(--fm-hairline-soft)}
.fm-specs dt{font:700 12px/1.5 "museo-sans",sans-serif;letter-spacing:.08em;text-transform:uppercase;
  color:var(--fm-muted);margin:1px 0 0}
.fm-specs dd{font:300 15px/1.5 "museo-sans",sans-serif;color:var(--fm-ink);margin:0}
.fm-specs cite{display:block;font:300 13px/1.4 "museo-sans",sans-serif;color:var(--fm-muted);font-style:normal;margin-top:3px}
.fm-specs cite a{color:var(--fm-muted)}
@media (max-width:520px){.fm-specs__row{grid-template-columns:1fr;gap:3px}}

/* ── back / breadcrumb-style up link inside the panel ────────────────────── */
.fm-up{display:inline-flex;align-items:center;gap:9px;margin-top:22px;
  min-height:44px;                       /* touch target, not decoration */
  font:500 14px/1 "museo-sans",sans-serif;color:var(--fm-ink);text-decoration:none;
  box-shadow:inset 0 -1px 0 0 var(--fm-hairline)}
.fm-up svg{width:15px;height:15px;fill:currentColor;flex:none;transition:transform .2s var(--fm-ease)}
.fm-up:hover{color:var(--fm-teal-deep);box-shadow:inset 0 -1px 0 0 var(--fm-teal-deep)}
.fm-up:hover svg{transform:translateX(-3px)}
.fm-up:focus-visible{outline:2px solid var(--fm-teal);outline-offset:3px}

/* ── list of child links under the stage (keyboard/no-JS parity) ─────────── */
/* P1. The hairlines were a 1px GAP over a grey container background — which
   paints wherever no cell covers it. Eleven items in a five-column grid leave a
   four-cell remainder on the last row, and that remainder rendered as a solid
   grey band: measured on the live page, 1012x79px at 1440 and 253x79px at 1100,
   where the column count drops to four. The rule is now one border per CELL, so
   an empty cell paints nothing and the last row terminates wherever the items
   do, at any column count. */
.fm-children{margin:8px 0 0;padding:0;list-style:none;display:grid;gap:0;background:none;
  border-top:1px solid var(--fm-hairline-soft);border-left:1px solid var(--fm-hairline-soft)}
.fm-children li{border-right:1px solid var(--fm-hairline-soft);
  border-bottom:1px solid var(--fm-hairline-soft)}
@media (min-width:768px){.fm-children{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}}
.fm-children a{display:flex;height:100%;align-items:center;justify-content:space-between;gap:12px;
  background:var(--fm-white);padding:18px 20px;text-decoration:none;color:var(--fm-ink);
  font:300 16px/1.35 "museo-sans",sans-serif;
  transition:background-color .18s var(--fm-ease),color .18s var(--fm-ease)}
.fm-children a svg{width:17px;height:17px;fill:currentColor;flex:none;transition:transform .18s var(--fm-ease)}
.fm-children a:hover{background:var(--fm-ink);color:var(--fm-white)}
.fm-children a:hover svg{transform:translateX(3px)}
.fm-children a:focus-visible{outline:2px solid var(--fm-teal);outline-offset:-2px}


/* ── the transition (R-4) ────────────────────────────────────────────────── */
/* The whole page is the transitioning element. transform-origin is set inline
   by transition.js to the clicked hotspot's centre, so the zoom pushes out of
   the point the visitor picked. */
.fm-tx.fm-exit,.fm-tx.fm-exit-out,.fm-tx.fm-enter,.fm-tx.fm-enter-out,.fm-tx.fm-settle{
  will-change:transform,filter,opacity}
.fm-tx.fm-exit{transform:scale(1.35);filter:blur(12px);opacity:.6;
  transition:transform .45s cubic-bezier(.55,0,.85,.35),filter .45s ease-in,opacity .45s ease-in}
.fm-tx.fm-exit-out{transform:scale(.78);filter:blur(12px);opacity:.6;
  transition:transform .45s cubic-bezier(.55,0,.85,.35),filter .45s ease-in,opacity .45s ease-in}
.fm-tx.fm-enter{transform:scale(1.12);filter:blur(10px);opacity:.55}
.fm-tx.fm-enter-out{transform:scale(.9);filter:blur(10px);opacity:.55}
.fm-tx.fm-settle{transform:none;filter:blur(0);opacity:1;
  transition:transform .5s var(--fm-ease),filter .5s var(--fm-ease),opacity .35s var(--fm-ease)}

@media (prefers-reduced-motion:reduce){
  .fm-tx.fm-exit,.fm-tx.fm-exit-out{transform:none;filter:none;opacity:0;transition:opacity .2s linear}
  .fm-tx.fm-enter,.fm-tx.fm-enter-out{transform:none;filter:none;opacity:0}
  .fm-tx.fm-settle{transform:none;filter:none;opacity:1;transition:opacity .25s linear}
  .fm-pin__dot::after{animation:none}
  .fm-pin__label{transition:none}
}

/* ── mobile ──────────────────────────────────────────────────────────────── */
@media (max-width:767px){
  .fm-panel{padding:22px 20px}
  .fm-overlay__card{padding:24px 20px}
  .fm-pin__label{display:none}
  .fm-stage__img{aspect-ratio:4/3}
}

/* ═══════════════════════════════════════════════════════════════════════════
   v2 — page heads, the three archetypes, and the plain touch-point page.
   Tokens are the same wsk-theme set the v1 layer used. Accent colours are
   MEASURED off the client's own illustrations (dominant saturated hue per
   plate), never invented, and arrive as a --fm-accent custom property.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── page head ───────────────────────────────────────────────────────────── */
.fm-head{max-width:72ch;margin:0 0 30px}
.fm-head__title{font:100 clamp(30px,2vw + 20px,46px)/1.12 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 14px}
.fm-head__lede{font:300 clamp(16px,.4vw + 15px,19px)/1.6 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 14px}
.fm-head__lede:last-child{margin-bottom:0}


/* a document (plan, Earth capture) must not be cropped; a photograph must fill */
.fm-stage__img--contain{object-fit:contain;background:var(--fm-white)}

/* ═══ C · TABS ══════════════════════════════════════════════════════════════
   The live site puts each track map inside its own pane, so switching tab
   replaces one image with another and the drawing jumps. The four supplied
   maps are drawn on the same geometry, so stacking them in ONE stage and
   cross-fading holds the grey base still and lets the coloured segment travel. */
.fm-tabs{--fm-accent-active:var(--fm-teal-deep)}
.fm-tabs__list{display:flex;gap:2px;overflow-x:auto;scrollbar-width:none;
  border-bottom:1px solid var(--fm-hairline-soft);margin:0 0 28px}
.fm-tabs__list::-webkit-scrollbar{display:none}
.fm-tab{position:relative;flex:0 0 auto;appearance:none;border:0;background:transparent;cursor:pointer;
  padding:15px 22px 14px;min-height:44px;
  font:500 15px/1.2 "museo-sans",sans-serif;color:var(--fm-muted);white-space:nowrap;
  box-shadow:inset 0 -3px 0 0 transparent;
  transition:color .2s var(--fm-ease),box-shadow .2s var(--fm-ease),background-color .2s var(--fm-ease)}
.fm-tab:hover{color:var(--fm-ink);background:var(--fm-panel)}
.fm-tab[aria-selected="true"]{color:var(--fm-ink);font-weight:700;
  box-shadow:inset 0 -3px 0 0 var(--fm-accent)}
.fm-tab:focus-visible{outline:2px solid var(--fm-teal);outline-offset:-3px}

.fm-tabs__body{display:grid;gap:28px;align-items:start}
@media (min-width:992px){.fm-tabs__body{grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:40px}}

/* one stage, four registered plates, cross-faded in place */
.fm-tabs__stage{position:relative;aspect-ratio:3300/1500;background:var(--fm-white);
  border:1px solid var(--fm-hairline-soft)}
.fm-tabmap{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;
  opacity:0;transition:opacity .42s var(--fm-ease)}
.fm-tabmap.is-on{opacity:1}
@media (prefers-reduced-motion:reduce){.fm-tabmap{transition:none}}

.fm-tabpanel__title{font:100 clamp(23px,1vw + 18px,30px)/1.2 "museo-sans",sans-serif;
  color:var(--fm-ink);margin:0 0 14px;padding-top:12px;
  box-shadow:inset 0 3px 0 0 var(--fm-accent)}
.fm-tabpanel p{font:300 16px/1.62 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 15px}

/* ═══ A · CAROUSEL ══════════════════════════════════════════════════════════
   A scroll-snap track: touch swipe and momentum are the browser's own. */
.fm-car__frame{position:relative;background:var(--fm-ink)}
.fm-car__track{display:flex;list-style:none;margin:0;padding:0;
  overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;scroll-behavior:smooth;
  scrollbar-width:none;-ms-overflow-style:none}
.fm-car__track::-webkit-scrollbar{display:none}
.fm-car__track:focus-visible{outline:2px solid var(--fm-teal);outline-offset:-2px}
@media (prefers-reduced-motion:reduce){.fm-car__track{scroll-behavior:auto}}

.fm-car__slide{flex:0 0 100%;min-width:0;scroll-snap-align:start;scroll-snap-stop:always}
.fm-car__media{position:relative;aspect-ratio:16/9;background:#000}
.fm-car__media img,.fm-car__media video{display:block;width:100%;height:100%;object-fit:cover}
.fm-car__media video{object-fit:contain;background:#000}
/* the poster alone does not read as a video; a play mark does */
.fm-car__playhint{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:72px;height:72px;border-radius:50%;background:rgba(16,23,31,.62);
  display:grid;place-items:center;pointer-events:none;transition:opacity .2s var(--fm-ease)}
.fm-car__playhint svg{width:30px;height:30px;fill:#fff;margin-left:3px}
.fm-car__slide:has(video[controls]:not([paused])) .fm-car__playhint{opacity:0}

.fm-car__cap{margin:0;padding:16px 20px;background:var(--fm-ink);color:rgba(255,255,255,.85);
  font:300 15px/1.55 "museo-sans",sans-serif;display:flex;gap:12px;align-items:baseline;min-height:64px}
.fm-car__capn{flex:none;font:700 12px/1 "museo-sans",sans-serif;letter-spacing:.08em;
  color:var(--fm-coral);padding-top:3px}

.fm-car__nav{position:absolute;top:calc(50% - 32px);transform:translateY(-50%);
  width:52px;height:52px;border:0;border-radius:50%;cursor:pointer;
  background:rgba(255,255,255,.92);color:var(--fm-ink);display:grid;place-items:center;
  box-shadow:0 3px 16px rgba(16,23,31,.35);transition:background-color .18s var(--fm-ease),opacity .18s var(--fm-ease)}
.fm-car__nav svg{width:26px;height:26px;fill:currentColor}
.fm-car__nav--prev{left:16px}
.fm-car__nav--next{right:16px}
.fm-car__nav:hover{background:var(--fm-white)}
.fm-car__nav:focus-visible{outline:2px solid var(--fm-teal);outline-offset:3px}
.fm-car__nav[disabled]{opacity:.32;cursor:default}

.fm-car__bar{display:flex;align-items:center;justify-content:space-between;gap:20px;
  padding:14px 2px 0}
.fm-car__count{margin:0;font:700 12px/1 "museo-sans",sans-serif;letter-spacing:.1em;
  text-transform:uppercase;color:var(--fm-muted)}
.fm-car__dots{display:flex;gap:4px}
.fm-car__dot{width:28px;height:28px;padding:0;border:0;background:transparent;cursor:pointer;
  position:relative}
.fm-car__dot::after{content:"";position:absolute;left:6px;top:12px;width:16px;height:4px;
  background:var(--fm-hairline);transition:background-color .18s var(--fm-ease)}
.fm-car__dot:hover::after{background:var(--fm-muted)}
.fm-car__dot[aria-current]::after{background:var(--fm-coral)}
.fm-car__dot:focus-visible{outline:2px solid var(--fm-teal);outline-offset:-2px}

/* ═══ B · NESTED HOTSPOT ════════════════════════════════════════════════════
   Same normalised x/y percentage model as the facility map. A pin zooms the
   plate toward its OWN coordinate — transform-origin is literally the pin's
   x%/y%, so the zoom lands on it exactly — and opens a detail card. */
.fm-hot{display:grid;gap:24px;align-items:start}
@media (min-width:992px){
  .fm-hot{grid-template-columns:minmax(0,1fr) 340px;gap:32px}
  .fm-hot__list{grid-column:1 / -1}
}
.fm-hot__stage{position:relative;overflow:hidden;aspect-ratio:3505/2137;
  background:var(--fm-white);border:1px solid var(--fm-hairline-soft)}
.fm-hot__pan{position:absolute;inset:0;transform-origin:50% 50%;
  transition:transform .55s var(--fm-ease)}
@media (prefers-reduced-motion:reduce){.fm-hot__pan{transition:none}}
.fm-hot__img{display:block;width:100%;height:100%;object-fit:contain}

.fm-hot__pin{position:absolute;
  /* --fm-z counter-scales the pin so a zoomed pin stays the same size on screen */
  transform:translate(-50%,-50%) scale(var(--fm-z,1));
  width:34px;height:34px;padding:0;border:2px solid var(--fm-white);border-radius:50%;
  background:var(--fm-accent,var(--fm-ink));color:var(--fm-white);cursor:pointer;
  display:grid;place-items:center;
  box-shadow:0 2px 8px rgba(16,23,31,.4);
  transition:box-shadow .2s var(--fm-ease)}
.fm-hot__mark{position:relative;width:14px;height:14px;pointer-events:none}
/* A cross, not a numeral: the plate's own circled numbers mean "test section n",
   and a numeric pin badge would be read as one of them. */
.fm-hot__mark::before,.fm-hot__mark::after{content:"";position:absolute;left:50%;top:50%;
  background:currentColor;transition:transform .22s var(--fm-ease)}
.fm-hot__mark::before{width:12px;height:2px;transform:translate(-50%,-50%)}
.fm-hot__mark::after{width:2px;height:12px;transform:translate(-50%,-50%)}
.fm-hot__pin[aria-expanded="true"] .fm-hot__mark::before{transform:translate(-50%,-50%) rotate(45deg)}
.fm-hot__pin[aria-expanded="true"] .fm-hot__mark::after{transform:translate(-50%,-50%) rotate(45deg)}
.fm-hot__pinlabel{position:absolute;left:calc(100% + 9px);top:50%;transform:translateY(-50%);
  white-space:nowrap;font:500 13px/1 "museo-sans",sans-serif;
  padding:8px 11px;background:var(--fm-ink);color:var(--fm-white);
  opacity:0;pointer-events:none;transition:opacity .18s var(--fm-ease)}
.fm-hot__pin:hover .fm-hot__pinlabel,.fm-hot__pin:focus-visible .fm-hot__pinlabel{opacity:1}
/* A pin in the right-hand third opens its label leftwards so it stays in frame. */
.fm-hot__pin[style*="left:8"] .fm-hot__pinlabel,.fm-hot__pin[style*="left:9"] .fm-hot__pinlabel,
.fm-hot__pin[style*="left:7"] .fm-hot__pinlabel{left:auto;right:calc(100% + 9px)}
.fm-hot__pin:hover{box-shadow:0 0 0 5px rgba(255,255,255,.55),0 2px 10px rgba(16,23,31,.5)}
.fm-hot__pin:focus-visible{outline:3px solid var(--fm-ink);outline-offset:2px}
.fm-hot__pin[aria-expanded="true"]{box-shadow:0 0 0 6px rgba(255,255,255,.75),0 3px 14px rgba(16,23,31,.55)}

.fm-hot__reset{position:absolute;right:14px;top:14px;z-index:2;
  min-height:40px;padding:0 16px;border:0;cursor:pointer;
  background:var(--fm-ink);color:var(--fm-white);
  font:500 13px/1 "museo-sans",sans-serif;letter-spacing:.02em}
.fm-hot__reset:focus-visible{outline:2px solid var(--fm-white);outline-offset:-4px}

.fm-hot__detail{background:var(--fm-white);border-top:3px solid var(--fm-accent,var(--fm-ink));
  padding:24px;margin:0 0 20px;box-shadow:0 10px 30px rgba(16,23,31,.12)}
.fm-hot__dzone{font:700 11px/1 "museo-sans",sans-serif;letter-spacing:.11em;text-transform:uppercase;
  color:var(--fm-accent,var(--fm-muted));margin:0 0 10px}
.fm-hot__dtitle{font:100 26px/1.2 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 12px}
.fm-hot__dbody{font:300 15px/1.6 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 16px}
.fm-hot__close{min-height:44px;padding:0 4px;border:0;background:transparent;cursor:pointer;
  font:500 14px/1 "museo-sans",sans-serif;color:var(--fm-ink);
  box-shadow:inset 0 -1px 0 0 var(--fm-hairline)}
.fm-hot__close:focus-visible{outline:2px solid var(--fm-teal);outline-offset:3px}

.fm-hot__legend{background:var(--fm-panel);padding:22px 24px}
.fm-hot__ltitle{font:700 12px/1 "museo-sans",sans-serif;letter-spacing:.1em;text-transform:uppercase;
  color:var(--fm-muted);margin:0 0 14px}
.fm-hot__legend ul{list-style:none;margin:0;padding:0}
.fm-hot__legend li{display:flex;align-items:center;gap:10px;padding:8px 0;
  font:300 15px/1.35 "museo-sans",sans-serif;color:var(--fm-ink);
  border-bottom:1px solid var(--fm-hairline-soft)}
.fm-hot__legend li:last-child{border-bottom:0}
.fm-hot__swatch{flex:none;width:14px;height:14px;border-radius:3px}
.fm-hot__zcount{margin-left:auto;font:700 12px/1 "museo-sans",sans-serif;color:var(--fm-muted)}

/* keyboard and no-JS parity: every pin is also a listed button */
.fm-hot__list{display:grid;gap:1px;background:var(--fm-hairline-soft);margin-top:4px}
@media (min-width:768px){.fm-hot__list{grid-template-columns:repeat(auto-fit,minmax(230px,1fr))}}
.fm-hot__group{background:var(--fm-white);padding:20px}
.fm-hot__gtitle{font:700 12px/1 "museo-sans",sans-serif;letter-spacing:.1em;text-transform:uppercase;
  color:var(--fm-accent,var(--fm-muted));margin:0 0 12px}
.fm-hot__items{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}
.fm-hot__item{display:inline-flex;align-items:center;gap:8px;min-height:40px;
  padding:0 12px 0 6px;border:1px solid var(--fm-hairline-soft);background:var(--fm-white);
  cursor:pointer;font:300 14px/1 "museo-sans",sans-serif;color:var(--fm-ink);
  transition:border-color .18s var(--fm-ease),background-color .18s var(--fm-ease)}
.fm-hot__item:hover{background:var(--fm-panel);border-color:var(--fm-accent)}
.fm-hot__item:focus-visible{outline:2px solid var(--fm-teal);outline-offset:2px}
.fm-hot__itemn{flex:none;width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
  background:var(--fm-accent,var(--fm-ink));color:var(--fm-white);
  font:700 12px/1 "museo-sans",sans-serif}

/* ═══ plain touch-point page ════════════════════════════════════════════════ */
.fm-plain{display:grid;gap:26px;align-items:start}
@media (min-width:992px){.fm-plain{grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:44px}}
.fm-plain__title{font:100 clamp(30px,2vw + 20px,44px)/1.14 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 18px}
.fm-plain__copy p{font:300 16px/1.65 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 16px;max-width:60ch}
.fm-plain__fig{margin:0}
.fm-plain__fig img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover}

@media (max-width:767px){
  .fm-car__nav{width:44px;height:44px}
  .fm-car__nav--prev{left:8px}
  .fm-car__nav--next{right:8px}
  /* No aspect-ratio override here. The stage's ratio is set INLINE from the
     plate's own pixel dimensions, because percentage pins are only true when
     the image fills the box they are positioned against. A 4/3 rule here would
     be inert (inline wins) but would silently misalign every pin the day the
     inline value went. Measured at 390: stage 342x208.5 = 3505/2137 exactly. */
  .fm-hot__pin{width:30px;height:30px}
  .fm-hot__pinn{font-size:12px}
  .fm-tab{padding:13px 16px;font-size:14px}
}

/* ── R-6 "Sample content" marker ─────────────────────────────────────────────
   Rendered on exactly the pages that declare an invented statement — three of
   eighteen. A marker on a fully sourced page would devalue it on the three that
   need it, so the build asserts declared == rendered rather than trusting it. */
.fm-sample{display:inline-flex;align-items:center;gap:8px;
  font:700 11px/1 "museo-sans",sans-serif;letter-spacing:.11em;text-transform:uppercase;
  color:var(--fm-ink);background:#FFE8AE;border:1px solid #E4B94A;
  padding:7px 12px;border-radius:32px;margin:0 0 16px}
.fm-sample svg{width:13px;height:13px;fill:currentColor;flex:none}

/* ── fact list (label / value, with provenance where the value is sourced) ── */
.fm-facts{margin:20px 0 0;padding:0;border-top:1px solid var(--fm-hairline-soft)}
.fm-facts__row{display:grid;grid-template-columns:40% 1fr;gap:14px;
  padding:11px 0;border-bottom:1px solid var(--fm-hairline-soft)}
.fm-facts dt{font:700 11px/1.45 "museo-sans",sans-serif;letter-spacing:.08em;text-transform:uppercase;
  color:var(--fm-muted);margin:2px 0 0}
.fm-facts dd{font:300 15px/1.5 "museo-sans",sans-serif;color:var(--fm-ink);margin:0}
.fm-facts cite{display:block;font-style:normal;margin-top:3px}
.fm-facts cite a{font:300 12px/1.35 "museo-sans",sans-serif;color:var(--fm-muted)}
@media (max-width:520px){.fm-facts__row{grid-template-columns:1fr;gap:3px}}

/* ── hero figure (card-directory overview) ───────────────────────────────── */
/* D3202: this block keeps the name .fm-hero. The aerial overview is a DIFFERENT
   component and is named .fm-aerial precisely because of what happened when it
   was not: `.fm-hero img` below (0-1-1) outranked the new `.fm-hero__img`
   (0-1-0) and forced the full-bleed photograph to 16/9 inside a 4/3 stage,
   leaving 192px of bare background across the foot of the hero at 1024x1366.
   A retired component's descendant selector is still a live selector. */
.fm-hero{margin:0}
.fm-hero img{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover}
.fm-hero figcaption{font:300 14px/1.5 "museo-sans",sans-serif;color:var(--fm-muted);margin-top:10px}

/* ── card directory ──────────────────────────────────────────────────────────
   RULING A: pins on desktop, cards at narrow widths. The zoom hand-off fires
   identically from a card and from a pin, so this is one interaction in two
   presentations, not two interactions. */
.fm-mobiledir__title{font:700 12px/1 "museo-sans",sans-serif;letter-spacing:.1em;text-transform:uppercase;
  color:var(--fm-muted);margin:36px 0 16px}
.fm-cards{list-style:none;margin:0;padding:0;display:grid;gap:14px;
  grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}
.fm-card{display:block;text-decoration:none;color:var(--fm-ink);background:var(--fm-white);
  border:1px solid var(--fm-hairline-soft);
  transition:border-color .18s var(--fm-ease),transform .18s var(--fm-ease),box-shadow .18s var(--fm-ease)}
.fm-card__img{display:block;overflow:hidden;background:var(--fm-page)}
.fm-card__img img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;
  transition:transform .35s var(--fm-ease)}
.fm-card__label{display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:15px 16px;font:500 15px/1.3 "museo-sans",sans-serif}
.fm-card__label svg{width:16px;height:16px;fill:currentColor;flex:none;
  transition:transform .18s var(--fm-ease)}
.fm-card:hover{border-color:var(--fm-ink);box-shadow:0 8px 24px rgba(16,23,31,.12)}
.fm-card:hover .fm-card__img img{transform:scale(1.045)}
.fm-card:hover .fm-card__label svg{transform:translateX(3px)}
.fm-card:focus-visible{outline:2px solid var(--fm-teal);outline-offset:2px}
@media (prefers-reduced-motion:reduce){.fm-card,.fm-card__img img,.fm-card__label svg{transition:none}}

/* On a stage page the cards are the NARROW-WIDTH treatment of the same pins:
   below 992px the pinned plan is replaced by the directory, because a 34px pin
   on a 390px-wide plan is neither legible nor a reliable touch target. */
@media (min-width:992px){ .fm-layout + .fm-mobiledir{display:none} }
@media (max-width:991px){
  .fm-layout:has(+ .fm-mobiledir) .fm-stage{display:none}
  /* The child-link list under the plan is keyboard/no-JS parity for the PINS.
     Where the card directory replaces the pins it carries the same eleven
     destinations, so showing both would list every location twice. */
  .fm-layout:has(+ .fm-mobiledir) .fm-children{display:none}
}

/* ── a document base (SVG plan, floor plan) is contained, never cropped ───── */
.fm-stage--doc{background:var(--fm-white);border:1px solid var(--fm-hairline-soft)}
.fm-stage--doc .fm-stage__img{height:100%}

/* ── carousel layout with a fact rail ────────────────────────────────────── */
.fm-carlayout{display:grid;gap:26px;align-items:start}
@media (min-width:992px){.fm-carlayout{grid-template-columns:minmax(0,1fr) 320px;gap:34px}}
/* zer0 flagged two selections whose composition depends on their height; a
   portrait frame is letterboxed against the dark frame rather than beheaded. */
.fm-car__slide--portrait .fm-car__media img{object-fit:contain;background:#000}

/* ── tabs: abbreviation + panel call to action ───────────────────────────── */
/* The abbreviation is a RESPONSIVE SWAP, not a hidden span. It was previously
   position:absolute with no positioned ancestor, so it anchored to the section
   and — because overflow:auto does NOT clip an absolutely positioned descendant
   whose containing block is an ANCESTOR of the scroller — it escaped the tab
   strip's own scroller and inflated the page to 623px against a 390px viewport
   while the strip itself measured a correct 342. Nothing here is absolutely
   positioned now, and .fm-tab is position:relative so any future child is
   contained by its own button. */
.fm-tab__abbr{display:none}
@media (max-width:767px){
  .fm-tab__full{display:none}
  .fm-tab__abbr{display:inline}
}
.fm-tabpanel__cta{display:inline-flex;align-items:center;gap:10px;min-height:44px;
  font:500 15px/1 "museo-sans",sans-serif;color:var(--fm-ink);text-decoration:none;
  box-shadow:inset 0 -2px 0 0 var(--fm-accent)}
.fm-tabpanel__cta svg{width:16px;height:16px;fill:currentColor;transition:transform .18s var(--fm-ease)}
.fm-tabpanel__cta:hover svg{transform:translateX(3px)}
.fm-tabpanel__cta:focus-visible{outline:2px solid var(--fm-teal);outline-offset:3px}

/* ── hotspot detail: section list and the zone still ─────────────────────── */
.fm-hot__dsections{font:700 11px/1.5 "museo-sans",sans-serif;letter-spacing:.08em;
  text-transform:uppercase;color:var(--fm-muted);margin:0 0 12px}
.fm-hot__dstill{margin:0 0 16px}
.fm-hot__dstill img,.fm-hot__dstill video{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;background:#000}
.fm-hot__dstill figcaption{font:300 13px/1.45 "museo-sans",sans-serif;color:var(--fm-muted);margin-top:8px}
.fm-hot__unassigned{font:300 13px/1.5 "museo-sans",sans-serif;color:var(--fm-muted);
  margin:14px 0 0;padding-top:14px;border-top:1px solid var(--fm-hairline-soft)}
.fm-hot__list{list-style:none;margin:6px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:8px;background:none}

/* ── plain page: a portrait hero gets its own slot ───────────────────────── */
.fm-plain--portrait .fm-plain__fig img{aspect-ratio:2/3;object-fit:cover;max-height:78vh;width:auto}
@media (min-width:992px){.fm-plain--portrait{grid-template-columns:minmax(0,7fr) minmax(0,5fr)}}

/* ── horizontal-overflow cures (measured at 390) ─────────────────────────────
   A flex or grid CHILD defaults to min-width:auto, which is its min-content
   width. A child that contains a horizontal scroller therefore refuses to be
   narrower than its own content, and the scroller never scrolls — it pushes the
   document instead. Measured before the cure: the tabs page laid out to 623px
   and the carousel page to 695px against a 390px viewport. min-width:0 lets the
   scroller do the job it was built for. */
.fm-tabs,.fm-tabs__list,.fm-tabs__body,.fm-tabs__stage,.fm-tabs__panels{min-width:0}
.fm-tabs__list{max-width:100%}
.fm-carlayout,.fm-car,.fm-car__frame,.fm-car__track,.fm-car__slide,.fm-car__media{min-width:0}
.fm-car__slide{max-width:100%}
.fm-hot,.fm-hot__stage,.fm-hot__pan{min-width:0}

/* The hotspot pin's hover label has no meaning on a touch pointer and is laid
   out even at opacity 0, so it was reaching 57px off the left edge of the plate
   and 52px off the right. The list below the plate already names every zone. */
@media (max-width:991px){ .fm-hot__pinlabel{display:none} }


/* ═══ D3192 — MxV's own copy, 17 September 2026 ═══════════════════════════════
   Three shapes the v2 pages had no slot for. All three are content-led: nothing
   here is a redesign, and every value below is an existing token or an existing
   measure.

   1. STRAPLINE (C-01). An eyebrow above the page title, at the same scale the
      fact labels already use, so it reads as a kicker rather than as a heading
      competing with the h1.
   2. ARTICLE (ten notes anchored below the feature panel). One measure-width
      column under the main panel. It is deliberately NOT inside .fm-panel: the
      panel is a fixed sidebar column and 300 words of client prose in it would
      run to the length of the map beside it.
   3. LIST- and PARAGRAPH-VALUED FACTS (C-30, C-31, C-38, C-45). The row is not
      height-capped and carries no overflow:hidden, because a fixed-height box
      drops the tail of a long value silently and the longest of these is three
      paragraphs. */

/* NO text-transform. The eyebrow scale elsewhere on this site upper-cases, but
   a strapline is the client's own line, capitalised the way they capitalised
   it. Upper-casing it in CSS is a restyle of their copy that no diff would
   show. */
.fm-strapline{font:700 13px/1.5 "museo-sans",sans-serif;letter-spacing:.055em;
  color:var(--fm-muted);margin:0 0 10px}

/* D3429 — decision 71. A heading of more than one sentence breaks between its
   sentences and never inside one. Each sentence is an inline-block phrase, so a
   line can break only at the space between two phrases; max-width caps a phrase
   at the column, so a sentence wider than the column wraps inside itself instead
   of overflowing. transition.js adds .fm-phrases--stacked to the heading when the
   phrases do not all fit on one line, and then each takes a line of its own. No
   size, weight or spacing changes: the phrases inherit the heading's type. */
.fm-phrase{display:inline-block;max-width:100%}
.fm-phrases--stacked .fm-phrase{display:block}

.fm-article{max-width:68ch;margin:34px 0 0;padding-top:26px;
  border-top:1px solid var(--fm-hairline-soft)}
.fm-article__p{font:300 clamp(15px,.25vw + 14px,17px)/1.72 "museo-sans",sans-serif;
  color:var(--fm-ink);margin:0 0 18px}
.fm-article__p:last-child{margin-bottom:0}
.fm-article__list{margin:0 0 18px;padding:0;list-style:none}
.fm-article__list:last-child{margin-bottom:0}
.fm-article__list li{font:300 clamp(15px,.25vw + 14px,17px)/1.72 "museo-sans",sans-serif;
  color:var(--fm-ink);margin:0 0 12px;padding-left:20px;position:relative}
.fm-article__list li:last-child{margin-bottom:0}
.fm-article__list li::before{content:"";position:absolute;left:2px;top:.72em;
  width:7px;height:2px;background:var(--fm-ink)}

.fm-facts__list{margin:0;padding:0;list-style:none}
.fm-facts__list li{font:300 15px/1.5 "museo-sans",sans-serif;color:var(--fm-ink);
  margin:0 0 6px;padding-left:15px;position:relative}
.fm-facts__list li:last-child{margin-bottom:0}
.fm-facts__list li::before{content:"";position:absolute;left:0;top:.66em;
  width:6px;height:2px;background:var(--fm-muted)}
.fm-facts__para{font:300 15px/1.5 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 10px}
.fm-facts__para:last-child{margin-bottom:0}
/* a value that is a list or several paragraphs needs the label column to stop
   taking 40% of the row on a narrow tablet, or the value wraps to two words */
@media (max-width:860px){
  .fm-facts__row:has(.fm-facts__list),
  .fm-facts__row:has(.fm-facts__para){grid-template-columns:1fr;gap:5px}
}

.fm-tabs__p{font:300 15px/1.65 "museo-sans",sans-serif;color:var(--fm-ink);margin:0 0 14px}
.fm-tabs__p:last-child{margin-bottom:0}

/* ═══ D3206 BLOCK START · one page template, two columns ══════════════════════
   Everything between these two markers is D3206. It is inserted BEFORE the
   D3202 overview block on purpose: D3205 is appending at end of file on a
   parallel branch, so the two changes never touch the same lines.

   THE RULE, in the operator's words (2026-09-21T15:58Z): "the content text or
   article text should always be directly below the media area, with the other
   content area acting as a separate column. So these should be split into
   columns rather than having the content stacked below both areas." And:
   "Menu item indexes should always be at the bottom of the page, with the
   'Back to [whatever the other page is]' button at the very bottom."

   So every route now reads:

       ┌─ media column ──────────────┬─ content column ─┐
       │ feature (photo / plate /    │ title            │
       │ tabs stage / carousel /     │ intro            │
       │ zoomable map)               │ quick facts      │
       │ ARTICLE TEXT                │                  │
       └─────────────────────────────┴──────────────────┘
         menu index (child cards), full width
         ← Back to …                            ← last element

   The article is the part that moved. It used to sit full width beneath BOTH
   columns, which is what produced the defect he named: on Headquarters &
   Laboratory the grey panel ran 460 px below the plate with empty white beside
   it, and the article started below the pair instead of filling that white.

   WHICH SIDE THE MEDIA IS ON DOES NOT CHANGE. The rule is "the article goes
   under the media", not "the media moves". Headquarters & Laboratory keeps its
   plate left and its grey panel right; Impact Wall keeps its copy left and its
   photograph right. .fm-media is simply the wrapper around whichever grid cell
   holds the feature, so the same rule expresses both.

   COLUMN RATIO: 7 media : 5 content, held across the family. Thirteen of the
   fifteen two-column routes (every .fm-plain) were already exactly that; the
   stage page's fixed 348 px rail and the carousel's fixed 320 px rail were the
   two odd ones out, and they move to the majority rather than the other way
   round. A fixed sidebar beside a fluid column cannot hold a ratio.
   Consequence, measured and accepted: the Research Laboratories plate renders
   ~620 px wide at 1180 instead of ~700. Its pins are percentages OF THE PLATE
   so nothing moves in plate space; the printed room names lose 11 % of their
   rendered size and are still legible at DPR 2 (checked by eye on the raster).

   TWO COLUMNS FROM 992 px, one below it — the breakpoint .fm-plain already
   used, now the family's. 820 portrait is single column, and that is a
   measurement rather than a preference: the container's inner width there is
   ~740 px, which splits into a 295 px text column (about 40 characters, below
   the readable floor) or 207 px card tiles. Neither is worth two columns. */

/* ── D3207 · six invisible captions ───────────────────────────────────────────
   Found by looking at the FAST® Loop raster: the caption strip under the
   carousel read as a black bar with a lone red numeral and no words. Measured:
   the caption text computes to rgb(16,23,31) on rgb(16,23,31). Contrast 1:1.
   All six slides, on every build since the carousel was written — NOT caused by
   this dispatch; the before-build measures identically.

   Cause: the site theme carries `.colour-scheme p { color: inherit }` at
   specificity (0,2,0), and `.fm-car__cap { color: rgba(255,255,255,.85) }` is
   (0,1,0). Load order cannot save a weaker selector, so the caption inherited
   the light scheme's ink and painted it on its own dark bar.

   Cure: match the theme's specificity. Nothing else about the caption changes.

   The census that found it had to be rewritten first, which is the reusable
   part: a walk over LEAF ELEMENTS reported zero low-contrast text on all
   eighteen routes, because .fm-car__cap holds a <span> for the numeral and so
   is not a leaf — its own text node is not any element's whole content. Walking
   TEXT NODES and taking each one's parent's computed colour found all six. If a
   contrast census reports zero, check it can see a text node that has an
   element sibling. */
.fm-car .fm-car__cap{color:rgba(255,255,255,.85)}

/* D3207. A grey panel that has no title must not reserve room for one.
   On the FAST® Loop carousel the panel now holds only the quick facts (the
   child link and the up-link moved to the foot of the page under B4), and
   .fm-facts carries a top margin meant to separate it from a heading that is no
   longer above it — 34 px of empty grey above "TOTAL TRACK". Scoped to
   :first-child, so a panel that does keep its title is untouched. Measured
   across all eighteen routes: this is the only one affected. */
.fm-panel > .fm-facts:first-child{margin-top:0}

/* The media column. Its only job is to let the feature and the article share
   one grid cell at one width. min-width:0 because a grid child's automatic
   minimum size is its content, and an image or a scroll-snap track inside one
   will otherwise refuse to shrink and push the row wider than the page. */
.fm-media{min-width:0;display:flow-root}

/* One ratio, three grids. */
@media (min-width:992px){
  .fm-layout{grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:40px}
  .fm-carlayout{grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:40px}
}

/* The article inside a media column. The standalone version had a top rule and
   34px of margin because it followed a full-width block and needed to announce
   itself; here it follows its own picture in its own column, so the rule reads
   as a divider between two things that are one thing. Keep the measure cap —
   a media column is wide, and 68ch is the point past which a line stops being
   scannable. */
.fm-media .fm-article{margin-top:22px;padding-top:20px;max-width:68ch}

/* The menu index and the Back link: full width, under both columns, in that
   order, Back last. This supersedes the 17 September arrangement that put child
   menus directly beneath the map on hotspot pages. */
.fm-layout + .fm-children,
.fm-carlayout + .fm-children{margin-top:34px}
.fm-children + .fm-up{margin-top:26px}

/* ── the overview's two columns (D3206 A2) ────────────────────────────────────
   LEFT is a plain content area and explicitly NOT a box: no panel background,
   no border, no shadow, no red top rule. It reads top to bottom as one piece —
   strapline, title, opening paragraph, address facts, then the three longer
   paragraphs that used to sit beneath the cards. RIGHT is the eight-tile menu.

   The split is 5 text : 7 cards, the mirror of the page family's ratio, because
   here the cards are the wide element: eight 3:2 photographs want the room and
   the copy does not. At 1180 that gives the cards ~620 px, which is two tiles
   of ~294 px. Three-up would be ~195 px per tile — a photograph too small to
   read and a two-line label under it, which is the ragged-row failure §08.7
   already cured once. Two-up, four rows. */
.fm-overview{display:grid;gap:34px;align-items:start}
@media (min-width:992px){
  .fm-overview{grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(36px,3.4vw,56px)}
}
.fm-overview__copy{min-width:0}
.fm-overview__title{font:100 clamp(32px,2vw + 20px,46px)/1.14 "museo-sans",sans-serif;
  color:var(--fm-ink);margin:0 0 18px}
.fm-overview__blurb{font:300 clamp(15px,.3vw + 14px,17px)/1.68 "museo-sans",sans-serif;
  color:var(--fm-ink);margin:0 0 16px;max-width:64ch}
.fm-overview__blurb:last-of-type{margin-bottom:0}
.fm-overview .fm-facts{margin-top:26px}
/* The three long paragraphs continue the column rather than starting a new
   region, so the divider rule the standalone article carries would cut the
   column in half. They are the same voice as the paragraph above them. */
.fm-overview__copy .fm-article{margin-top:22px;padding-top:0;border-top:0;max-width:64ch}
/* (the tile grid itself is set in the D3202 block below, where it was born —
   amended in place at D3206 rather than overridden from here, because an
   override sitting above the thing it overrides does not work.) */

/* ── the format-option tag and the prototype bar are BUILD-GATED ──────────────
   Neither is emitted while PRODUCTION is true in build-v2.mjs, so these rules
   are dormant, not deleted: flip the constant and the styling is still here. */

/* ═══ D3206 BLOCK END ═════════════════════════════════════════════════════════ */

/* ── D3202 · the full-bleed aerial hero (overview only) ───────────────────────
   Ruling 25 puts the overview on the angled aerial PHOTOGRAPH with one grouped
   pin. The operator asked for it full bleed with the content in a box over it.

   THE ONE THING THIS LAYOUT EXISTS TO GET RIGHT. Full bleed means the image is
   cropped, and the crop differs at every viewport. A pin positioned as a
   percentage of the HERO BOX would drift off the loop exactly as the laboratory
   pins drifted in D3192 (§10.5): a pin is placed left:X% against its container
   while the thing it points at is X% of the PLATE, and the moment those two
   boxes differ the dot stops being where its number says it is.

   So the pin is not in the hero. It is inside .fm-aerial__stage, a box that
   carries the image's OWN aspect ratio, is scaled to cover the hero, is offset
   to hold a chosen focal point, and is clipped by .fm-aerial__bg. Percentages
   inside that box are image coordinates by construction, under any crop.

   .fm-aerial__bg is the size container rather than .fm-aerial, because
   `container-type: size` applies size containment — an element so contained
   cannot be sized by its own contents, and the hero must be able to GROW when
   the overlaid box is taller than the minimum. The bg takes its size from
   inset:0 on the hero, so containment costs it nothing and 100cqw/100cqh are
   the hero's real, grown dimensions. */
.fm-aerialsec{background:var(--fm-ink)}
/* D3203. The photograph owns the first screen. Height is the viewport
   remainder under the chrome, less a deliberate PEEK of the section beneath.
     floor 560px   — a short laptop still gets a hero, not a letterbox.
     ceiling min(760px, 86vw) — on a narrow, tall viewport the cover crop is
       HORIZONTAL, so the taller the hero the more of the image's width is cut.
       The 86vw term caps that.
   svh, not vh: on iOS vh is the LARGEST viewport, so a vh-sized hero hides
   under the address bar on first paint. dvh would resize while the bar
   retracts, which moves the pin under the reader's thumb. svh is the stable
   one. --fm-chrome-h is measured at runtime.
     D3261 NOTE: --fm-chrome-h NO LONGER EXISTS. D3211 (below) retired the
     height clamp that read it, and the script that wrote it went on running
     and writing a property nothing consulted — for three dispatches. It is
     replaced by --fm-chrome-gap, which measures a different quantity (the
     clearance RESIDUAL, not the chrome's height) and has a consumer: see the
     D3261 block at the end of this file. The paragraphs below are kept because
     they record why the numbers were what they were.

   D3206 RE-TUNED BOTH NUMBERS, because the page changed underneath them.
   (a) --fm-chrome-h fallback 122px -> 80px. That 122 was the site header (80)
       PLUS the prototype bar; the bar is gone at PRODUCTION, so the runtime
       measurement now returns the header alone and the no-JS fallback has to
       agree with it or the hero is 42px short of the fold.
   (b) --fm-peek 96px -> 132px. The peek used to have to show the top of the
       card grid; now the first thing under the photograph is the page TITLE,
       and a title half-cut by the fold is worse than no peek at all. 132px
       carries the strapline and the whole title line at both iPad
       orientations — measured, not guessed, and armed at A3.
   The 86vw clearance derivation that used to live here is retired with the
   box it protected: with nothing drawn over the photograph there is no
   overlay for the loop to be covered by. The cap stays as a plain ceiling.

   D3211 RETIRES THE HEIGHT CLAMP ALTOGETHER. Operator, 2026-09-21T17:44Z: "the
   full map isn't visible right now — I believe it cuts off at the bottom, so you
   might need to extend the crop so that the full map is visible." Measured at
   1180x820 before the change: the hero was 608 px tall against a 885 px image at
   that width, so 31 % of the photograph — including the whole depot core along
   the bottom, where five of the eight new pins live — was never on the page at
   all. A pin cropped out of the frame is not a hotspot.

   So the hero now takes the PHOTOGRAPH'S OWN RATIO and no crop in either axis:
   height = width x 0.75 at every viewport, phone included. The consequence is
   accepted by the same directive and is not a defect to be rescued: on iPad
   landscape the title and strapline fall below the first screen. The peek and the
   svh arithmetic exist to put something under the fold, and there is no fold to
   arrange any more, so both come out rather than sitting here doing nothing.

   The stage mechanism is UNTOUCHED and still load-bearing. With the hero at the
   image's own ratio the two cover terms in --fm-asw/--fm-ash are equal, both
   clamps resolve to 0, and stage == hero — but it degenerates to that by
   arithmetic, not by assumption, so a future re-crop restores image-space pins
   without anything being rewired. Pins stay percentages OF THE IMAGE either way. */
.fm-aerial{position:relative;display:block;
  aspect-ratio:var(--fm-aimg-w) / var(--fm-aimg-h);
  background:var(--fm-ink);
  --fm-afx:.66;--fm-afy:.42}
.fm-aerial__bg{position:absolute;inset:0;overflow:hidden;container-type:size;
  background:var(--fm-ink)}
.fm-aerial__stage{position:absolute;
  --fm-asw:max(100cqw,calc(100cqh * var(--fm-aimg-w) / var(--fm-aimg-h)));
  --fm-ash:max(100cqh,calc(100cqw * var(--fm-aimg-h) / var(--fm-aimg-w)));
  width:var(--fm-asw);height:var(--fm-ash);
  left:clamp(calc(100cqw - var(--fm-asw)),calc(50cqw - var(--fm-asw) * var(--fm-afx)),0px);
  top:clamp(calc(100cqh - var(--fm-ash)),calc(50cqh - var(--fm-ash) * var(--fm-afy)),0px)}
.fm-aerial__img{display:block;width:100%;height:100%;object-fit:cover}

/* The hero pin's label is authored, not placed at runtime: the placement pass
   in archetypes.js is scoped to .fm-stage and there is only one pin here, so a
   pass that resolves collisions has nothing to resolve. Below the dot is the
   one position that can reach neither the overlaid box (always to the left) nor
   the right edge of the viewport at 390. The 44px puck, the absolute label and
   data-fm-zoom are the shared .fm-pin, unchanged — §08.6 and §08.8 carry over
   without a second implementation.
   D3211 AMENDS THE LABEL RULE IN PLACE. With one pin the label could be drawn at
   every width, and it was. With EIGHT it cannot: at 820 px the eight names, up to
   240 px each on a 820 px-wide photograph, overlap each other and both edges; and
   on a touch device a tap navigates rather than reveals, so the label was never
   doing a job there in the first place. So the aerial's always-on label is scoped
   to 992 px and up — the site's existing pin-label breakpoint, not a new one.
   Below it the shared `.fm-pin__label{display:none}` applies, the dots stay, every
   button keeps its accessible name, and the eight cards under the photograph carry
   the words. The label SIDE is authored per pin in overview-aerial.json against a
   measurement, because the runtime placer in archetypes.js is scoped to .fm-stage
   and this hero is not one. */
.fm-pin--aerial{z-index:3}
@media (min-width:992px){
  .fm-pin--aerial .fm-pin__label{display:block;max-width:none;white-space:nowrap;
    font-size:14px;padding:7px 12px}
}
/* ── D3224 · the left-edge marker (Headquarters & Laboratory) ─────────────────
   Operator ruling 2026-09-21T19:27:48Z, decision 43. This facility is on Keeler
   Parkway in Pueblo, about eight miles away; the camera fit projects it 34.7 px
   inside a 2000 px frame and cannot decide whether it is in the photograph at
   all — drop one control point and the projection leaves the frame at x = -234.
   So the marker claims ONE quantity, the height, and nothing else.

   It must not read as a located pin, and the CSS is where that is enforced:
     · anchored to the EDGE. `left:0` with the translate taken off the X axis, so
       the 44px puck's left edge IS the image's left edge rather than being
       centred on a coordinate. The puck stays 44px — the touch target and the
       focus ring are the same as every other hotspot's.
     · a CHEVRON, not a dot. An outlined arrow says "that way, past the frame";
       a filled circle says "it is here". No pulse ring: the ring's whole job is
       to say look at this spot, and there is no spot.
     · a second line in words. Shape alone cannot carry "off the image", so the
       label says it, and the accessible name says it too.
   Below 992 the label is withheld by the shared rule (ruling c, 2026-09-21) and
   the chevron alone holds the edge, at the same 44px target. */
.fm-pin--edge-left{left:0;transform:translate(0,-50%);
  place-items:center start;padding-left:4px}
.fm-pin__chev{position:relative;width:13px;height:13px;
  border-left:3px solid var(--fm-accent,var(--fm-coral));
  border-bottom:3px solid var(--fm-accent,var(--fm-coral));
  transform:rotate(45deg);
  filter:drop-shadow(0 1px 4px rgba(16,23,31,.55));
  transition:transform .2s var(--fm-ease),opacity .2s var(--fm-ease)}
.fm-pin--edge:hover .fm-pin__chev,.fm-pin--edge:focus-visible .fm-pin__chev{
  transform:rotate(45deg) translate(2px,-2px)}
/* two lines, so `nowrap` has to stop applying to the box and start applying to
   each line — otherwise the sub-line runs on beside the name. */
.fm-pin--edge .fm-pin__label{white-space:normal;line-height:1.25}
.fm-pin__labelmain{display:block;white-space:nowrap}
.fm-pin__labelsub{display:block;white-space:nowrap;
  font-size:11.5px;font-weight:400;letter-spacing:.01em;
  color:var(--fm-muted);margin-top:1px}
.fm-pin--edge:hover .fm-pin__labelsub,
.fm-pin--edge:focus-visible .fm-pin__labelsub{color:rgba(255,255,255,.72)}

/* D3211. The dot is sized against the PICTURE, and below 768 the picture is only
   about 292 px tall, so a 22 px dot with its 6 px pulse ring is a 34 px mark on a
   292 px photograph — the seven of them read as blobs rather than points, and the
   two maintenance shops (13.1 px apart at 390) merge into one. The mark shrinks;
   the .fm-pin puck stays 44 px, so the TOUCH TARGET and the coordinate anchor are
   untouched and every arm above still measures the same boxes. The pulse comes off
   here too: its whole job is to draw the eye to a single mark, and seven pulsing
   rings on a small picture is noise. Scoped to the aerial — the schematic and
   laboratory plates are unaffected. */
@media (max-width:767px){
  .fm-pin--aerial .fm-pin__dot{width:13px;height:13px;
    box-shadow:0 0 0 2px rgba(249,83,65,.3),0 1px 5px rgba(16,23,31,.5)}
  .fm-pin--aerial .fm-pin__dot::after{display:none}
}

/* D3206 RETIRED IN PLACE (2026-09-21), not deleted: .fm-aerial__inner,
   .fm-aerialbox and their D3202/D3203 tuning. The box they styled no longer
   exists — its content is now .fm-overview__copy, an ordinary column under the
   photograph. The D3203 "46% not 56%" derivation (which box width kept the
   track loop clear at 820 portrait) is retired with it: nothing is drawn over
   the picture any more, so there is no clearance to compute. Kept as this note
   rather than a silent deletion so that the next seat to reach for an overlay
   finds the reason the last one came off.

   D3211 RETIRED IN PLACE: the <768 rule that made the hero stop cropping and show
   the photograph whole. It is now what the hero does at EVERY width, from the
   aspect-ratio above, so the rule would have been a second mechanism for the same
   result — and a differently-shaped one, since it moved the size container off
   `position:absolute`. One mechanism, all widths. */

/* D3202 read: "Eight areas read as two clean rows of four" — true while the
   menu had the whole page width. D3206 AMENDS IT IN PLACE (2026-09-21): the
   menu now shares the row with the content column and has about 620 px at 1180,
   so four-up would be ~140 px tiles. Two-up, four rows, from the point the page
   goes two-column. Below 992 the page is one column and the original 1-up /
   2-up ladder is unchanged. auto-fill is still avoided for the reason D3202
   gave: it leaves a ragged remainder (§08.7 P1). */
.fm-cards--menu{grid-template-columns:1fr}
@media (min-width:440px){.fm-cards--menu{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:1000px){.fm-cards--menu{grid-template-columns:repeat(2,minmax(0,1fr))}}
/* the section already carries layout--padding-y; a second top margin here
   opened ~44px of dead white between the hero and the way in. */
/* Four of the eight names wrap to two lines and four do not, so the cards ended
   at two different heights and the rows read ragged along the bottom. The label
   area reserves two lines on this grid only: 2 x 1.3em of 15px type plus the
   15px padding either side. Every other card grid on the site is unaffected. */
.fm-cards--menu .fm-card__label{min-height:69px;align-items:flex-start}
.fm-menu{margin-top:0}
/* The peek is only an invitation if something is IN it. The theme's
   layout--padding-y puts 96px of white above the eyebrow, so a 96px peek showed
   96px of nothing. Tightened here, on the overview's menu section only, so the
   peek carries the eyebrow and the top edge of the first tile row. */
.fm-menusec{padding-top:clamp(26px,3.2vw,44px)}
.fm-menu__title{font:700 12px/1 "museo-sans",sans-serif;letter-spacing:.1em;
  text-transform:uppercase;color:var(--fm-muted);margin:0 0 16px}

/* ── D3205 · a plate that names its own rooms (raiden F-1) ───────────────────
   The client's Research Laboratories artwork PRINTS "Calibration Laboratory",
   "Metallurgy Laboratory" and "Damage Prevention & Loading Services". Our pin
   labels therefore said everything twice, and the white boxes half-occluded the
   drawing's own captions. On this plate the labels are not drawn at all by
   default; the accessible name stays on the button, so a screen reader and the
   keyboard are unaffected, and the focus ring is untouched.

   A label may still appear on hover or focus, but ONLY where the runtime placer
   found a position clear of every printed name — the name rectangles are fed to
   it as keep-outs, read out of the slide XML. A pin it could not place keeps the
   fm-pin--hoveronly class, and on this plate that means no label at all rather
   than a label that would sit on the drawing's own text. */
.fm-stage--platenamed .fm-pin__label{opacity:0;transition:opacity .18s var(--fm-ease)}
.fm-stage--platenamed .fm-pin:hover .fm-pin__label,
.fm-stage--platenamed .fm-pin:focus-visible .fm-pin__label{opacity:1}
.fm-stage--platenamed .fm-pin--hoveronly .fm-pin__label,
.fm-stage--platenamed .fm-pin--hoveronly:hover .fm-pin__label,
.fm-stage--platenamed .fm-pin--hoveronly:focus-visible .fm-pin__label{display:none}

/* The marker is sized from the measurement, not from the default. The dot is
   expressed as a fraction of the STAGE so it keeps the clearance it was proved
   to have when the stage scales — --fm-dot-r is written by the builder from
   measure-lab-pins.mjs, which solved the largest radius the smallest room can
   hold (40 of 3300 plate px = 1.212 %, limited by the Calibration Laboratory,
   whose printed name spans the room's full width). The 44px puck is untouched:
   the hit target is not what was too big. */
.fm-stage--platenamed .fm-pin__dot{
  width:calc(var(--fm-dot-r, 1.212%) * 1.35);
  height:auto;aspect-ratio:1;
  min-width:10px;max-width:22px;
  box-shadow:0 0 0 3px rgba(249,83,65,.28),0 2px 8px rgba(16,23,31,.45)}
.fm-stage--platenamed .fm-pin:hover .fm-pin__dot,
.fm-stage--platenamed .fm-pin:focus-visible .fm-pin__dot{
  box-shadow:0 0 0 4px rgba(249,83,65,.3),0 3px 12px rgba(16,23,31,.5)}

/* ===== plugin supplements (nothing above this line is ours) ===== */
/* A pin whose target page is missing or unpublished is drawn as a label only: it is a <span>,
   not a button, so it must not offer a pointer. The prototype has no such state — its graph is
   whole at build time — but a CMS page can be unpublished while a pin still points at it. */
.fm-pin--static{cursor:default}
/* The prototype's cards are all the same height because its grid rows are; WordPress images
   vary, so the card stretches to its row. */
.fm-menu .fm-card{height:100%;box-sizing:border-box}
/* ── D3261, re-cured D7578 · the band between the site header and the tour ──────
   MEASURED on staging, signed in, Chromium and WebKit, 360-1920 px, every tour
   page shape (D7578, 2026-11-11):

     y   0.. 31   32px  #wpadminbar (46px at 782px and below)
     y   0.. 79   80px  header.site-header, fixed at top:0, its top under the bar
     y  80..111   32px  THE BAND: main.site-main's 80px padding, counted from a
                        document top the admin bar pushed down 32px
     y 112..      ...   the photograph

   The theme does not move its fixed header down for the admin bar, so the band
   is the admin bar's offset (32px, or 46px on a phone). Signed out there is no
   bar and no band. D3261 read the header's visible 48px as its height and
   subtracted it from the padding; staging computed 80 - 80 = 0 and the cure
   never fired.

   mxvdt_chrome_gap_script() now measures the gap itself — the tour's unpulled
   top minus the header's bottom edge — and publishes it as --fm-chrome-gap on
   #fm-tx; this rule spends it. The fallback is 0px, so with no JS, on a header
   in flow, or signed out on staging the page is exactly as it was.

   margin-top and not `position:relative;top:` — the tour has to MOVE, not be
   drawn higher while leaving its space behind. `.site-main` has padding, which
   blocks margin collapse, so the pull happens inside the padding box. */
.fm-tour{margin-top:calc(0px - var(--fm-chrome-gap, 0px))}
/* ── D5062 · the overview on MxV's new aerial: dark glass labels, and the bottom fade ──
   Kage, 2026-10-19: the overview's pin labels are no longer white — translucent dark
   frosted glass with white text. Scoped to .fm-pin--aerial, which only the overview
   draws (its located pins and its edge marker). The hotspot maps' .fm-pin__label on
   the drawn plans, and the plates' .fm-hot__pinlabel, are NOT changed here.

   CONTRAST IS MEASURED, NOT ASSUMED. dev/hero-aerial/preview.mjs photographs every
   label with its text made transparent, in Chromium and WebKit at 992, 1280, 1440
   and 1920, and reads the brightest pixel the engine painted inside each label box
   (the glass after blur and saturate, over the brightest patch of the photograph
   under it). White text must hold 4.5:1 against that pixel. The opacity below is
   chosen so it holds EVEN WHERE THE BLUR IS NOT PAINTED: headless WebKit (Playwright's,
   the engine Safari runs) computes backdrop-filter and paints none of it, measured on a
   plain test page as well as here, so its photographs are the no-blur worst case — the
   brightest roof under a label, seen through the fill alone. Where blur paints
   (Chromium; Safari itself), the margin is far wider. The @supports fallback, for an
   engine that does not even parse backdrop-filter, is a near-opaque fill.

   The hover/focus state no longer inverts (the label is already dark): it fills in,
   brightens its edge and lifts by a pixel. Coral stays the only accent, on the dot.
   `translate` is used for the lift, not `transform`, because the placement classes
   own `transform` (translateX(-50%) and friends) and would lose it.

   Focus: the shared ring is 2px of ink, which vanishes on a dark photograph. On the
   aerial it is white with ink on both sides of it (ink · white · ink), so it reads on
   the darkest and the lightest ground. */
.fm-pin--aerial .fm-pin__label{
  background:rgba(16,23,31,.62);
  -webkit-backdrop-filter:blur(12px) saturate(1.4);backdrop-filter:blur(12px) saturate(1.4);
  color:var(--fm-white);
  border:1px solid rgba(255,255,255,.16);border-radius:4px;
  box-shadow:0 2px 12px rgba(16,23,31,.28);
  transition:background-color .18s var(--fm-ease),border-color .18s var(--fm-ease),
             box-shadow .18s var(--fm-ease),translate .18s var(--fm-ease),opacity .18s var(--fm-ease)}
@supports not ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))){
  .fm-pin--aerial .fm-pin__label{background:rgba(16,23,31,.82)}
}
.fm-pin--aerial:hover .fm-pin__label,.fm-pin--aerial:focus-visible .fm-pin__label{
  background:rgba(16,23,31,.86);color:var(--fm-white);border-color:rgba(255,255,255,.34);
  box-shadow:0 6px 20px rgba(16,23,31,.45);translate:0 -1px}
/* The edge marker's second line is white too: its smaller, lighter type carries the
   hierarchy. Grey or translucent white failed 4.5:1 where the blur does not paint. */
.fm-pin--aerial .fm-pin__labelsub,
.fm-pin--edge.fm-pin--aerial:hover .fm-pin__labelsub,
.fm-pin--edge.fm-pin--aerial:focus-visible .fm-pin__labelsub{color:var(--fm-white)}
.fm-pin--aerial:focus-visible{outline:2px solid var(--fm-white);outline-offset:2px;
  box-shadow:0 0 0 2px var(--fm-ink),0 0 0 6px var(--fm-ink)}
@media (prefers-reduced-motion:reduce){
  .fm-pin--aerial .fm-pin__label{transition:none}
  .fm-pin--aerial:hover .fm-pin__label,.fm-pin--aerial:focus-visible .fm-pin__label{translate:none}
}
/* The bottom of the photograph runs into the section beneath it. Kage, 2026-10-19 10:11Z:
   the fade is white, not ink, "so it blends nicely into the layout beneath it". The
   section under the hero is .colour-scheme--light, whose background is #fff in the site
   theme (measured on the rendered page, Chromium and WebKit, 992 to 1920 px: the rows
   directly under the hero read 255,255,255 and the hero's own last row is within 1 of
   it). --fm-under carries that colour, and the ramp is white, so the fade ends in exactly
   the section's colour and there is no seam; if that section's colour ever changes, both
   change together. The ramp is longer and softer than a dark
   fade would need — eased, with no straight segment that would read as a grey band —
   and it starts low enough that the facility itself stays clear. It sits above the
   photograph and below every pin (pins are z-index 3, 6 when active). The stage equals
   the hero at every width (the hero takes the image's own ratio), so these stops are
   fractions of the image. MxV's file stays as they sent it. */
.fm-aerial{--fm-under:#fff}
.fm-aerial__bg::after{content:"";position:absolute;inset:0;z-index:2;pointer-events:none;
  background:linear-gradient(to bottom,rgba(255,255,255,0) 78%,rgba(255,255,255,.04) 82%,
    rgba(255,255,255,.14) 86%,rgba(255,255,255,.32) 90%,rgba(255,255,255,.56) 93.5%,
    rgba(255,255,255,.8) 96.5%,rgba(255,255,255,.95) 98.8%,var(--fm-under) 100%)}

/* D7578 — THE ROAD-AND-TRACK OVERLAY. MxV (Niki Toussaint, 3 Nov) asked to keep the Testing
   page's white road map and its track outline on the new aerial. includes/render/overlay.php
   draws it only for the photograph it was fitted to, inside .fm-aerial__stage between the
   photograph and the pins, so it shares the photograph's box, crop and focal transform at every
   width by construction: inset 0 in the same stage, viewBox = the image's own pixels. Stacking:
   photograph < overlay (1) < the white fade (2, .fm-aerial__bg::after) < pins (3). It is
   decoration: aria-hidden in the markup, and it never takes a pointer.
   The look is the Testing page's: roads white at 40 % (the Lottie's Lines_Opacity precomp), the
   track and the Impact Wall rail white at 100 %, every stroke the Lottie's 6 px on a 2000 px comp
   carried to this 2560 px image (7.68), so a line reads the same CSS width as it does there; the
   yellow band under the track is MxV's own mock of the track outline (image005).
   The draw-in is the Lottie's trim paths: each line's dash runs from its full length to 0 from
   its own start (--t) over its own duration (--d), with the Lottie's ease, once. Reduced motion
   gets the final drawing and no animation. */
.fm-aerial__overlay{position:absolute;inset:0;width:100%;height:100%;z-index:1;
  pointer-events:none;overflow:hidden}
/* D7578 Y-1 (LISA, 2026-11-11): the Testing page's lines are white only; the yellow in MxV's
   screenshots is a highlighter. The band stays in the asset, drawn at opacity 0 until MxV asks
   for it: set --fm-ovl-band-opacity to .6 to bring it back. */
.fm-ovl--band{stroke:var(--fm-ovl-band,#f7ca5b);opacity:var(--fm-ovl-band-opacity,0)}
.fm-ovl__line{stroke-dasharray:var(--l) var(--l);stroke-dashoffset:var(--l);
  animation:fm-ovl-draw var(--d) cubic-bezier(.6,0,.4,1) var(--t) 1 both}
@keyframes fm-ovl-draw{to{stroke-dashoffset:0}}
/* A label over the overlay: the frosted fill at .62 let the white track show through the words
   (measured on the 1440 captures: "On-track Testing" sits inside the loop it names). Over the
   overlay the fill is the hover strength, .86, so a line behind a label reads as texture, not
   as a second set of strokes through the text. Labels without the overlay are unchanged. */
.fm-aerial__stage--overlay .fm-pin--aerial .fm-pin__label{background:rgba(16,23,31,.86)}
@media (prefers-reduced-motion:reduce){
  .fm-ovl__line{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
}

/* D7899 — THE TESTING PAGE'S MAP SECTION (includes/render/layouts.php, `mxv_tour_map`). The stage is
   the overview's own markup and every rule above applies to it unchanged; this block adds only the
   section around it.
   Clearance: as the first row the section starts at the top of the document, under MxV's fixed,
   transparent 80 px header (staging, measured 2026-11-12). Its top padding is the header's bottom
   edge, measured at runtime into --fm-map-clear (0 when the section is not under the header), and it
   is painted in the header's ink, so no page background shows under the header and the photograph
   starts where the header ends. Without JavaScript: 5rem, the theme's own header clearance.
   Fade (LISA Z-3 on MxV's N-3/N-4, 11 Nov): a setting. DARK is the default and ends in #10171f,
   the measured background of the theme's dark split_content row on staging (Chromium, 390 and
   1440 px, 2026-11-12: rgb(16,23,31); the compiled rule is .colour-scheme--dark{background-color:
   #10171f}). The ramp is solid for its last 1.5 %, so the photograph's bottom pixel row IS the band's
   colour at every width (a 223 px phone photograph still ends in 3 solid pixels), with no white in
   it. WHITE is the overview page's own ramp above, untouched. */
.fm-mapsec{background:var(--fm-ink);padding-top:var(--fm-map-clear,5rem)}
.fm-mapsec--fade-dark .fm-aerial{--fm-under:#10171f}
.fm-mapsec--fade-dark .fm-aerial__bg::after{
  background:linear-gradient(to bottom,rgba(16,23,31,0) 72%,rgba(16,23,31,.06) 77%,
    rgba(16,23,31,.18) 82%,rgba(16,23,31,.38) 87%,rgba(16,23,31,.62) 91%,
    rgba(16,23,31,.84) 94.5%,rgba(16,23,31,.96) 97%,var(--fm-under) 98.5%,var(--fm-under) 100%)}

/* D7899 · THE PHOTO GRADE (Z-4). MxV, meeting of 11 Nov (Steph): the new aerial is too bright and
   washed out beside the Testing page's own hero; warm it, more saturation and contrast, an overlay
   or vignette. The grade is CSS on the stage photograph: MxV's file, the overlay asset, its key and
   the media are untouched, so this works wherever the stage is drawn.
   MEASURED, NOT EYEBALLED. The target is testing-hero-jpeg.webp as MxV's site shows it (the theme
   puts no gradient or filter on it: its dark bottom is baked into the file), read on the ground the
   two photographs share through the hero -> aerial homography (dev/overlay/data/register.json),
   hero rows 880-1700 (above its baked fade), each photograph at the size a 1440 px page shows it.
   dev/grade/measure.py derives the numbers and tests/e2e/grade.mjs holds both engines to them:
                           L*     L* p5-p95   HSV sat   a*     b*    mean R-B
     hero ground           32.6   22.9        .318      4.7   10.0   28.6
     aerial, before        54.9   28.3        .175     -1.6    7.0   13.7   (Chromium, 1440)
   The aerial is 22 L* brighter, half as saturated and olive (Lab hue 103) where the hero is
   brown (hue 64). Three parts, all under the overlay, the fade and the pins, so the white lines, the
   pins and their labels are never graded:
     1  the photograph: saturate, then contrast, then brightness (a filter list applies left to
        right; this order is the one the numbers were fitted in). Contrast and the warm layer
        raise the colour strength most (HSV saturation .175 -> .34 measured), so saturate itself
        is light. No sepia and no hue-rotate: either one would drag the sky towards orange.
     2  the vignette (stage ::after, z 0): a mild warm-dark ellipse at the corners, as asked.
     3  the warm layer (stage ::before, z 1): a warm brown multiplied over the ground, which takes
        green and blue out more than red and so turns the olive scrub brown. It starts below the
        horizon (10% -> 18% of the photo's height), so the sky stays blue. NOT mix-blend-mode hue,
        although it matched the numbers too: the far cloud shadows are saturated blue, and a hue
        layer turned them purple at half strength and orange at full (measured, rendered). Multiply
        leaves them dark blue-grey, as cloud shadow on brown ground is.
        WHY z 1 AND NOT 0: a filter makes the photograph a stacking context, painted with the
        z-index 0 layer in tree order, and ::before comes first in the tree, so at z 0 the warm
        layer is painted UNDER the filtered photograph and does nothing (measured, Chromium and
        WebKit alike). At z 1 it is still under the overlay, which is z 1 and later in the tree.
   THE SWITCH: --fm-grade. 1 is the grade as measured, 0 turns all of it off (every filter value
   resolves to 1 and both layers to opacity 0); a value between scales it. Every other value is a
   custom property below, so a re-grade is a change of numbers, not of rules. */
.fm-aerial{
  --fm-grade:1;
  --fm-grade-brightness:.76;--fm-grade-contrast:1.28;--fm-grade-saturate:1.03;
  --fm-grade-tint:174 90 72;--fm-grade-tint-opacity:.39;--fm-grade-tint-from:10%;--fm-grade-tint-to:18%;
  --fm-grade-vignette:24 14 8;--fm-grade-vignette-opacity:.22;--fm-grade-vignette-from:55%;
  --fm-grade-vignette-at:50% 45%}
.fm-aerial__img{filter:
  saturate(calc(1 + (var(--fm-grade-saturate) - 1) * var(--fm-grade)))
  contrast(calc(1 + (var(--fm-grade-contrast) - 1) * var(--fm-grade)))
  brightness(calc(1 + (var(--fm-grade-brightness) - 1) * var(--fm-grade)))}
.fm-aerial__stage::before,.fm-aerial__stage::after{content:"";position:absolute;inset:0;z-index:0;
  pointer-events:none}
.fm-aerial__stage::before{z-index:1;mix-blend-mode:multiply;
  opacity:calc(var(--fm-grade-tint-opacity) * var(--fm-grade));
  background:linear-gradient(to bottom,rgb(var(--fm-grade-tint) / 0) var(--fm-grade-tint-from),
    rgb(var(--fm-grade-tint) / 1) var(--fm-grade-tint-to))}
.fm-aerial__stage::after{
  opacity:calc(var(--fm-grade-vignette-opacity) * var(--fm-grade));
  background:radial-gradient(ellipse farthest-corner at var(--fm-grade-vignette-at),
    rgb(var(--fm-grade-vignette) / 0) var(--fm-grade-vignette-from),rgb(var(--fm-grade-vignette) / 1) 100%)}
