/* ============================================================
   3DDV venue — SHARED stylesheet for ALL hubs (/wc, /mlb, /nfl, /music…)
   Dark theme, full-screen stage.
   Palette from 3ddigitalvenue.com (navy #141B43, blue #0089F7, sky #5AC8FA).

   Per-page override (inline <style> in each page's <head>):
     :root {
       --acc: #F87171;                            <- stadium accent
       --kicker-text: "WC26 \00B7 3D Seat Views"; <- SEO h2 kicker, per hub
     }

   DVM hooks (#stage, #viewer, #view3d, #loader, #tooltip,
   #back-btn, #stage-title) unchanged — do not rename.

   v1 — includes UX audit fixes: overlay scrim, loader label,
   AA contrast in FAQ, 44px touch targets, mobile scroll cue,
   section chip in pano, unified primary button, reduced-motion
   for modal, SEO kicker, skip link.
   ============================================================ */
:root {
  --acc: #F87171;           /* default stadium accent — override per venue */
  --kicker-text: "3D Seat Views";  /* override per hub */
  --accent: #0089F7;
  --sky: #5AC8FA;
  --bg: #0D1230;
  --panel: #10173A;
  --card: #161D45;
  --line: #262F62;
  --line-soft: #1B2148;
  --ink: #F2F4FB;
  --bright: #FFFFFF;
  --muted: #A8B0D4;
  --dim: #707894;
  --radius: 16px;
  --content-max: 1160px;
  /* --peek: how much of the content column shows under the map without
     scrolling (v35). With Google's overlay formats no longer allowed over the
     stage, there is no inventory above the fold at all — so whether a reader
     scrolls IS the revenue variable, not a UX detail. A hero at exactly
     100dvh reads as "this is the whole page"; leaving the top edge of the
     guide block visible reads as "there is more", and that is the cheapest
     lever on scroll-through we have.

     It is a VERTICAL concession, which is why it is allowed: the stage stays
     centred and symmetric (see the .stage invariant below). Reverting is one
     value — set it to 0px and the hero is full-bleed again. */
  --peek: 88px;
  --stage-h: calc(100dvh - var(--peek, 0px));
  --font-display: "Barlow Condensed", "Arial Narrow", sans-serif;
  --font-ui: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* No scroll-snap: with a 100dvh hero even "proximity" snapping fought
   the user (magnet-back when entering page 2, bounce at the footer).
   The two-page feel comes from the dots + smooth scroll instead. */
html { scroll-behavior: smooth; }
html, body { background: var(--bg); color: var(--ink); }
body {
  font-family: var(--font-ui);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  /* Google's anchor ads are position:fixed against the viewport, so without
     this they permanently cover a band of the page and the footer can never
     be scrolled into view. Padding turns that band into reserved space.
     Both vars are 0px until venue.js sees an anchor actually displayed. */
  padding-top: var(--anchor-top-h, 0px);
  padding-bottom: var(--anchor-bot-h, 0px);
  transition: padding 0.3s;
}

a { color: var(--sky); }
::selection { background: var(--accent); color: #fff; }
:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }

.hidden { display: none !important; }

/* ---------- Skip link (a11y: DVM canvas is not keyboard-operable) ---------- */
.skip-link {
  position: absolute;
  top: -48px; left: 16px;
  z-index: 60;
  background: var(--accent);
  color: #fff;
  font-size: 0.8rem;
  font-weight: 700;
  padding: 10px 16px;
  border-radius: 0 0 8px 8px;
  text-decoration: none;
  transition: top 0.15s;
}
.skip-link:focus { top: 0; }

/* ---------- Full-screen stage ----------
   The hero gives up height to Google's anchor ads instead of letting them
   paint over it. --anchor-top-h / --anchor-bot-h are published by venue.js
   from the ads actually on screen (0px when there are none, so this is a
   no-op on a page without ads), and `body` is padded by the same amounts.
   Net effect: the anchor occupies a reserved band hard against the viewport
   edge and the map ends where the ad begins.

   This replaces a pile of per-element offsets — every piece of chrome inside
   the hero used to add --anchor-bot-h to its own `bottom`, which was both
   repetitive and useless against the real problem, since the CANVAS was
   still full height underneath. Inset the hero once and the map, the
   panorama, DVM's own buttons and all of our chrome move together.

   min-height is the deliberate floor: with a 262px anchor on a short laptop
   the reserved band would leave an unusable sliver of map, so past that
   point the hero stops shrinking and the page scrolls instead. */
.stage-hero {
  position: relative;
  height: 100vh;
  height: calc(var(--stage-h) - var(--anchor-top-h, 0px) - var(--anchor-bot-h, 0px));
  min-height: 480px;
  background: var(--panel);
  border-bottom: 1px solid var(--line-soft);
  overflow: hidden;
  transition: height 0.3s;
}
/* ============================================================
   INVARIANT — `.stage` IS `inset: 0`. DO NOT MAKE IT ASYMMETRIC.
   ============================================================
   The map and the panorama are the product. They stay centred and symmetric
   in the page at every viewport size. No exceptions, not even temporary ones.

   The history, so nobody re-derives it: on 2026-07-29 the stage was inset by
   --rail-l / --rail-w so it would end where a side rail ad began. It
   "worked" and it looked terrible — Google's left rail measured 488px on a
   1609 viewport, so the stage was shoved 488px to the RIGHT with a dead navy
   band down the left and the panorama no longer centred on the page. It was
   reverted the same day.

   The general rule that came out of it: when the stage conflicts with a
   third-party element (ad, consent badge, foreign chrome) the only valid
   exits are SYMMETRIC ones — equal gutters both sides, a reserved band top or
   bottom, or giving up the element. A one-sided inset is not an option.

   So the outer strip of the map can be overlapped by a rail, and that is
   accepted as the lesser evil. --rail-l / --rail-w are still measured and
   still move OUR OWN chrome (page dots, zoom, Stadium Guide, minimap) out of
   the way. They must never move the stage. Rationale in full:
   clients/DESIGN-ad-layout-v2.md §2 and §4.1. */
.stage {
  position: absolute;
  inset: 0;
}
.stage-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* ---------- Floating top bar ---------- */
.stage-top {
  position: absolute;
  top: 0;
  left: 0; right: 0;
  z-index: 10;
  transition: top 0.3s;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  pointer-events: none;
}
.stage-top > * { pointer-events: auto; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  background: rgba(13, 18, 48, 0.82);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 8px 14px;
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.15s, border-color 0.15s;
}
a.chip:hover { color: var(--bright); border-color: var(--accent); }
.stage-top .brand-chip {
  margin-left: auto;
  color: var(--bright);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.85rem;
  letter-spacing: 0.09em;
}
.brand-dot {
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
}
/* when the pano is open, "Back to map" replaces the hub chip,
   the brand chip steps aside (DVM fullscreen control lives top-right)
   and the big title fades out so the view stays clean */
.stage-top:has(#back-btn:not(.hidden)) .chip-link,
.stage-hero:has(#back-btn:not(.hidden)) .brand-chip { display: none; }
.stage-overlay .match-badge,
.stage-overlay h1,
.stage-overlay .tagline,
.stage-overlay .stage-legend {
  transition: opacity 0.35s, visibility 0.35s;
}
.stage-hero:has(#back-btn:not(.hidden)) .stage-overlay .match-badge,
.stage-hero:has(#back-btn:not(.hidden)) .stage-overlay h1,
.stage-hero:has(#back-btn:not(.hidden)) .stage-overlay .tagline,
.stage-hero:has(#back-btn:not(.hidden)) .stage-overlay .stage-legend {
  opacity: 0;
  visibility: hidden;
}

.back-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: none;
  background: var(--accent);
  color: #fff;
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 0.72rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 9px 16px;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35); /* visible over bright panos */
  transition: background 0.15s, transform 0.15s;
}
.back-btn:hover { background: #1897FF; transform: translateY(-1px); }

/* ---------- Bottom overlay: title ---------- */
.stage-overlay {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 9;
  padding: 0 18px 18px calc(18px + max(var(--rail-l, 0px), var(--cmp-w, 0px)));
  /* clears a left side-rail ad AND Google's bottom-left consent badge */
  pointer-events: none;
  transition: padding-left 0.3s;
}
/* scrim: guarantees h1/tagline contrast over bright map areas.
   Sits above the map canvas, below the overlay text; never blocks clicks. */
.stage-overlay::before {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 170px;   /* slimmer scrim to match the smaller title */
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    to top,
    rgba(13, 18, 48, 0.88) 0%,
    rgba(13, 18, 48, 0.35) 55%,
    transparent 100%
  );
}
.stage-overlay a { pointer-events: auto; }
.match-badge {
  display: inline-block;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--acc);
  background: color-mix(in srgb, var(--acc) 14%, rgba(13, 18, 48, 0.7));
  border: 1px solid color-mix(in srgb, var(--acc) 35%, transparent);
  border-radius: 999px;
  padding: 4px 11px;
  margin-bottom: 8px;
}
/* slim title: compact size, but with character — heavy condensed italic
   (the "jersey" cut) and a bright->sky gradient across the letters.
   drop-shadow instead of text-shadow: shadows paint OVER clipped text. */
.stage-overlay h1 {
  font-family: var(--font-display);
  font-weight: 900;
  font-style: italic;
  text-transform: uppercase;
  line-height: 1;
  letter-spacing: 0.015em;
  font-size: clamp(1.35rem, 2.6vw, 2.1rem);
  color: var(--bright);                      /* fallback */
  background: linear-gradient(100deg, var(--bright) 55%, var(--sky));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 2px 10px rgba(13, 18, 48, 0.8));
  padding-right: 0.15em;                     /* italic overhang, avoids clipping */
}
.stage-overlay h1 .accent {
  display: block;
  font-size: 0.55em;
  letter-spacing: 0.14em;
  -webkit-text-fill-color: var(--sky);       /* solid sky, opts out of gradient */
  margin-top: 3px;
}
/* tagline retired from the hero (UI slimming) — hidden via CSS so
   already-migrated pages that still carry it in the overlay lose it too.
   It now lives in the guide, as a lead line under the h2/kicker. */
.stage-overlay .tagline { display: none; }
.seo-copy .tagline {
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.5;
  max-width: 60ch;
  margin: -6px 0 20px;   /* tucks under the h2 */
}
#stage-title {
  font-family: var(--font-ui);
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--sky);
  margin-top: 8px;
  text-shadow: 0 1px 12px rgba(13, 18, 48, 0.9);
}
#stage-title::before {
  content: "";
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--sky);
  margin-right: 8px;
  vertical-align: 1px;
  animation: pulse 2s ease-in-out infinite;
}
/* while the pano is open, the section indicator becomes a pill pinned to the
   TOP CENTER of the stage — it is the orientation label, so it reads like a
   header, not like leftover map copy (uses the :has pattern above).
   The overlay is stretched to the full hero so the pill can leave the bottom;
   its scrim is dropped because nothing is left down there to protect. */
.stage-hero:has(#back-btn:not(.hidden)) .stage-overlay {
  top: 0;
  padding: 0;
}
.stage-hero:has(#back-btn:not(.hidden)) .stage-overlay::before { display: none; }
.stage-hero:has(#back-btn:not(.hidden)) #stage-title {
  position: absolute;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 11;                      /* over .stage-top, never under the map */
  display: inline-block;
  margin: 0;
  max-width: calc(100% - 32px);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: center;
  background: rgba(13, 18, 48, 0.82);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 7px 14px;
  text-shadow: none;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);  /* visible over bright panos */
}
/* legend retired (UI slimming): hidden via CSS so already-migrated pages
   that still carry the element in their HTML lose it too */
.stage-legend { display: none; }
@keyframes pulse { 50% { opacity: 0.35; } }

/* ---------- Stadium Guide button (scroll cue) ----------
   With the map at 100dvh this is the ONLY signal that there is content
   below, so it reads as a solid primary button, not a hint. */
.scroll-hint {
  position: absolute;
  right: calc(18px + var(--rail-w, 0px));         /* clears a right side-rail ad */
  bottom: 18px;
  z-index: 12;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  background: var(--accent);
  color: #fff;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 12px 20px;
  border-radius: 999px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
  text-decoration: none;
  transition: background 0.15s, transform 0.15s, right 0.3s, bottom 0.3s;
}
.scroll-hint:hover { background: #1897FF; transform: translateY(-1px); }
.scroll-hint .arrow {
  color: #fff;
  font-size: 1rem;
  line-height: 1;
  animation: bob 1.8s ease-in-out infinite;
}
/* hide it while the pano is open (same pattern as the h1) */
.stage-hero:has(#back-btn:not(.hidden)) .scroll-hint {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s, visibility 0.35s;
}

/* ---------- Map zoom controls (injected by venue.js) ---------- */
.zoom-ctrl {
  position: absolute;
  right: calc(18px + var(--rail-w, 0px));
  bottom: 84px;                     /* above the Stadium Guide button */
  z-index: 12;
  display: flex;
  flex-direction: column;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
  transition: right 0.3s, bottom 0.3s;
}
.zoom-btn {
  width: 44px;
  height: 44px;
  border: none;
  cursor: pointer;
  background: rgba(13, 18, 48, 0.82);
  color: var(--bright);
  font-family: var(--font-ui);
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1;
  transition: background 0.15s, color 0.15s;
}
.zoom-btn + .zoom-btn { border-top: 1px solid var(--line); }
.zoom-btn:hover { background: var(--accent); }
.stage-hero:has(#back-btn:not(.hidden)) .zoom-ctrl { display: none; }
@media (max-width: 640px) {
  .zoom-ctrl {
    right: 12px;
    bottom: 70px;                     /* above the mobile scroll-hint */
  }
}

/* ---------- Page dots (injected by venue.js) ---------- */
.page-dots {
  position: fixed;
  right: calc(14px + var(--rail-w, 0px));   /* clears a right side-rail ad */
  top: 50%;
  transform: translateY(-50%);
  z-index: 30;
  transition: right 0.3s;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px 4px;
  background: rgba(13, 18, 48, 0.82);
  border: 1px solid var(--line);
  border-radius: 999px;
}
.page-dots .dot {
  position: relative;
  width: 22px; height: 22px;      /* generous hit area */
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
}
.page-dots .dot::before {
  content: "";
  position: absolute;
  inset: 6px;                     /* 10px visible dot */
  border-radius: 50%;
  border: 1px solid var(--muted);
  transition: background 0.2s, border-color 0.2s, transform 0.2s;
}
.page-dots .dot:hover::before { border-color: var(--bright); }
.page-dots .dot.active::before {
  background: var(--accent);
  border-color: var(--accent);
  transform: scale(1.25);
}
body:has(#back-btn:not(.hidden)) .page-dots { display: none; }  /* pano open */
@media (max-width: 640px) { .page-dots { right: 6px; } }

/* ---------- "Best view" star (injected by venue.js when
   VENUE_CONFIG.bestSection is set) ---------- */
.best-star {
  position: absolute;
  z-index: 10;
  transform: translate(-50%, -100%);   /* sits just above the section */
  pointer-events: none;                /* clicks fall through to the map */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  visibility: hidden;                  /* venue.js reveals it when anchored */
}
.best-star-icon {
  font-size: 22px;
  line-height: 1;
  color: #FFC94D;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
}
.best-star-label {
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: #FFC94D;
  background: rgba(13, 18, 48, 0.82);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px 9px;
  white-space: nowrap;
}

/* ---------- Wheel-zoom hint (injected by venue.js) ---------- */
/* Top of the map, clear of the .stage-top chrome: the centre of the stage
   belongs to the loader card and the pano that follows it, so the hint
   never lands on top of a loading state. */
.wheel-hint {
  position: absolute;
  left: 50%;
  top: 60px;
  transform: translate(-50%, -8px);
  z-index: 13;
  pointer-events: none;
  background: rgba(13, 18, 48, 0.82);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1px solid var(--line);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 0.78rem;
  font-weight: 600;
  padding: 8px 16px;
  border-radius: 999px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
  opacity: 0;
  transition: opacity 0.22s ease, transform 0.22s ease, top 0.3s;
}
.wheel-hint.show { opacity: 1; transform: translate(-50%, 0); }
/* the pano owns the whole stage: no hint over it */
.stage-hero:has(#back-btn:not(.hidden)) .wheel-hint { display: none; }
@keyframes bob { 50% { transform: translateY(4px); } }

/* mobile: keep bottom overlays clear of Google anchor ads,
   guarantee 44px touch targets and keep a path to the content below */
@media (max-width: 640px) {
  /* Smaller peek on a phone: the viewport is short, the map needs the height
     more than the desktop does, and 56px is already enough to show that the
     page continues. */
  :root { --peek: 56px; }
  .stage-overlay { padding-bottom: 30px; }
  .stage-top { padding: 12px 12px; }
  /* taller 44px chips up there: push the hint below them */
  .wheel-hint {
    top: 70px;
    font-size: 0.7rem;
    padding: 7px 14px;
    max-width: calc(100% - 24px);
    text-align: center;
  }
  .chip, .back-btn { min-height: 44px; padding: 10px 16px; font-size: 0.66rem; }
  /* narrow screens: "Back to map" already owns the top row, so the section
     pill drops just below it instead of colliding with it */
  .stage-hero:has(#back-btn:not(.hidden)) #stage-title {
    top: 68px;
    max-width: calc(100% - 24px);
  }
  .scroll-hint {                      /* lifts itself above a bottom anchor ad */
    right: 12px;
    bottom: 12px;
    font-size: 0.66rem;
    padding: 11px 16px;
  }
}
/* touch: one finger = page scroll (vertical), pinch = map zoom, and
   horizontal drag still pans the map. touch-action tells the browser to
   keep vertical panning for itself and hand everything else to DVM.
   !important beats any inline touch-action DVM may set on its elements.
   Only on the MAP (#viewer) — in the pano (#view3d) one-finger drag must
   keep rotating the view. */
@media (pointer: coarse) {
  #viewer, #viewer * { touch-action: pan-y !important; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  #stage-title::before, .scroll-hint .arrow,
  .tooltip-thumb::after { animation: none; }
  .view-modal, .view-modal-card { animation: none; }
  /* the bar keeps sweeping (it is the only progress signal) but slower,
     everything else stops moving */
  .loader-chip::before { animation: none; }
  .loader-bar i { animation-duration: 2s; }
  .loader, #view3d { transition: none; }
}

/* ---------- Loader (single loading state) ----------
   The DVM 3d_viewer paints its OWN spinner inside #view3d. To avoid two
   loaders stacked on top of each other, #view3d is kept at opacity 0
   while the pano loads (see .view3d-entering below) — DVM's spinner is
   never seen — and this one overlay owns the whole transition:
   map blurs out  ->  branded progress  ->  pano cross-fades in.
   Markup is injected by venue.js, so page HTML stays untouched. */
.loader {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9;                          /* above #view3d, below chrome */
  /* opaque on first paint: also hides the DVM map viewer's own spinner */
  background: radial-gradient(120% 90% at 50% 45%, #101A3F 0%, #090D23 100%);
  opacity: 1;
  transition: opacity 0.26s ease;
}
/* pano load: the map stays behind, softly defocused — a focus pull, so the
   user keeps context instead of staring at a black box */
.loader.loading-pano {
  background: rgba(9, 13, 35, 0.42);
  -webkit-backdrop-filter: blur(14px) saturate(0.85);
  backdrop-filter: blur(14px) saturate(0.85);
}
.loader.is-out { opacity: 0; pointer-events: none; }

.loader-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 0 24px;
  text-align: center;
}
/* section chip — tells the user WHICH view is coming */
.loader-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px;
  border-radius: 999px;
  background: rgba(13, 18, 48, 0.82);
  border: 1px solid var(--line);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bright);
}
.loader-chip::before {
  content: "";
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--acc);
  animation: loader-pulse 1.4s ease-in-out infinite;
}
/* indeterminate bar — reads as progress without faking a percentage */
.loader-bar {
  position: relative;
  width: 132px;
  height: 3px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
}
.loader-bar i {
  position: absolute;
  inset: 0 auto 0 0;
  width: 45%;
  border-radius: 999px;
  background: linear-gradient(90deg, transparent, var(--sky), var(--accent), transparent);
  animation: loader-sweep 1.15s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.loader-label {
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
/* over the blurred map the muted grey loses contrast — brighten it */
.loader.loading-pano .loader-label {
  color: var(--ink);
  text-shadow: 0 1px 8px rgba(9, 13, 35, 0.9);
}
@keyframes loader-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(322%); }
}
@keyframes loader-pulse { 50% { opacity: 0.25; } }

/* Pano cross-fade: the container is laid out (so DVM sizes its canvas
   correctly) but invisible until the view has actually been painted —
   which venue.js now verifies on the canvas itself. While it is
   transparent the loader has the MAP behind it, so the wait is a focus
   pull on the seating chart rather than a black box. */
#view3d {
  transition: opacity 0.32s ease;
}
#view3d.view3d-entering {
  opacity: 0 !important;   /* beats any inline opacity DVM may set */
  pointer-events: none;
}
/* DVM paints its own controls (fullscreen top-right) inside #view3d and knows
   nothing about our ad slots. It no longer needs an offset of its own: the
   whole hero is now inset below a top anchor (see the .stage-hero rule), so
   DVM's buttons move with it. The offset is kept only for DVM's OWN
   fullscreen mode, where the viewer escapes the hero and covers the
   viewport, and a top anchor is painted over it again. */
#view3d .dvm-v3d-main-fullscreen .dvm-v3d-button-fullscreen {
  margin-top: var(--anchor-top-h, 0px);
  transition: margin-top 0.3s;
}

/* ---------- Tooltip (card: pano thumbnail + label) ---------- */
.tooltip {
  position: absolute;
  z-index: 11;
  pointer-events: none;
  background: var(--accent);
  color: #fff;
  font-size: 0.76rem;
  font-weight: 600;
  padding: 0;
  border-radius: 10px;
  overflow: hidden;
  transform: translate(-50%, calc(-100% - 12px));
  white-space: nowrap;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
}
.tooltip .tooltip-label { padding: 6px 12px; }
.tooltip .tooltip-thumb {
  position: relative;
  width: 100%;
  min-width: 220px;
  height: 124px;                 /* landscape crop regardless of source ratio */
  background: rgba(13, 18, 48, 0.35);
}
/* spinner while the thumbnail arrives; the loaded img paints over it */
.tooltip .tooltip-thumb::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 22px; height: 22px;
  margin: -11px 0 0 -11px;
  border: 2px solid rgba(255, 255, 255, 0.35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
/* the rotation itself — without this the ring renders frozen */
@keyframes spin { to { transform: rotate(360deg); } }
.tooltip .tooltip-thumb img {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;             /* the crop */
  display: block;
}
@media (pointer: coarse) {       /* no hover on touch: never show thumbs */
  .tooltip .tooltip-thumb { display: none; }
}

/* ---------- Below the fold ---------- */
.page {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 0 20px;
}
.content { width: 100%; }

.seo-copy {
  padding: clamp(28px, 6vw, 56px) 0 40px;
  max-width: 76ch;
}
.seo-copy h2 {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  font-size: clamp(1.5rem, 3.4vw, 2rem);
  line-height: 1.05;
  color: var(--ink);
  margin-bottom: 14px;
}
/* kicker: repeats the hero badge language to stitch hero and guide together */
.seo-copy h2::before {
  content: var(--kicker-text);
  display: block;
  font-family: var(--font-ui);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  color: var(--acc);
  margin-bottom: 10px;
}
.seo-copy h3 {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  font-size: 1.3rem;
  color: var(--ink);
  margin: 34px 0 10px;
}
.seo-copy p { color: var(--muted); }
.seo-copy strong { color: var(--ink); }
.faq dt { font-weight: 600; color: var(--ink); margin-top: 18px; }
.faq dd { color: var(--muted); margin-top: 4px; }  /* --dim failed WCAG AA */

/* ---------- Footer ---------- */
.site-footer {
  border-top: 1px solid var(--line-soft);
  text-align: center;
  padding: 26px 16px 40px;
  color: var(--dim);
  font-size: 0.78rem;
}
.site-footer a { color: var(--muted); }

/* ---------- Friendly "no view" modal ---------- */
.view-modal {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(6, 9, 26, 0.72);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  animation: vm-fade 0.18s ease;
}
.view-modal.hidden { display: none; }
@keyframes vm-fade { from { opacity: 0; } to { opacity: 1; } }
.view-modal-card {
  max-width: 420px;
  width: 100%;
  background: var(--card);
  border: 1px solid var(--line);
  border-top: 3px solid var(--acc);
  border-radius: var(--radius);
  padding: 28px 26px 24px;
  text-align: center;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
  animation: vm-pop 0.2s ease;
}
@keyframes vm-pop { from { transform: translateY(8px) scale(0.98); opacity: 0; } to { transform: none; opacity: 1; } }
.view-modal-title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  font-size: 1.5rem;
  line-height: 1.05;
  color: var(--bright);
  margin-bottom: 10px;
}
.view-modal-text {
  color: var(--muted);
  font-size: 0.92rem;
  line-height: 1.55;
  margin-bottom: 20px;
}
/* primary action = --accent everywhere; --acc stays as venue accent
   (badge + modal top border) */
.view-modal-btn {
  border: none;
  cursor: pointer;
  background: var(--accent);
  color: #fff;
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 0.8rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 11px 26px;
  border-radius: 999px;
  transition: transform 0.15s, filter 0.15s;
}
.view-modal-btn:hover { transform: translateY(-1px); filter: brightness(1.08); }

/* ---------- Ad placeholders (TEMPLATE/DEV ONLY — never ship to a live venue)
   Visualize where Google Auto Ads land so layout decisions account for them:
   - anchor: fixed bottom bar (mobile always; desktop possible)
   - in-article: responsive block inside .seo-copy
   - side rails: fixed verticals on very wide viewports (>=1440px)
   Vignettes (full-screen interstitials) can't be placeholdered. ---------- */
.ad-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  background: repeating-linear-gradient(
    45deg,
    rgba(90, 200, 250, 0.06) 0 12px,
    rgba(90, 200, 250, 0.12) 12px 24px
  );
  border: 2px dashed var(--sky);
  color: var(--sky);
  font-family: var(--font-ui);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
}
.ad-slot::before { content: attr(data-label); padding: 8px; }

.ad-slot--anchor {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  height: 64px;
  z-index: 40;                 /* over content, under the modal (50) */
  background-color: rgba(13, 18, 48, 0.92);
  border-left: none; border-right: none; border-bottom: none;
}
.ad-slot--anchor.at-top {      /* Google anchor can also be pinned to the top */
  top: 0; bottom: auto;
  border-top: none;
  border-bottom: 2px dashed var(--sky);
}
.ad-slot--inline {
  min-height: 250px;
  margin: 28px 0;
  border-radius: var(--radius);
}
.ad-slot--rail {
  display: none;
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  width: 160px;
  height: 600px;
  z-index: 7;                  /* under stage-top chips and overlays */
  writing-mode: vertical-rl;
}
.ad-slot--rail.rail-left { left: 0; }
.ad-slot--rail.rail-right { right: 0; }
@media (min-width: 1440px) {
  .ad-slot--rail { display: flex; }
}

/* ---------- .ad-inflow: the one destination for a relocated in-page ad ----------
   Google drops in-page Auto ads next to whatever copy it likes, and the
   nicest copy on a venue page — h1, tagline, section pill — lives in
   `.stage-overlay`, which is absolutely positioned OVER the map. The
   observer in venue.js MOVES those units in here instead of deleting them
   (see the long note there); this is the destination.

   REPLACES the gutter docks (v34). Those were two `position: absolute`
   containers in the margins of `.page`, and they had two problems. They
   needed a 1520px viewport to exist, so on a laptop, a tablet and every
   phone the code fell through to the content column anyway — which means
   the docks were a second concept that only ran on wide desktop. And on
   wide desktop what they produced was a single ad stranded in a margin
   beside nothing, which is not a position a reader reads.

   So: one destination, IN FLOW, at every width. It sits inside `.seo-copy`
   after the opening prose, which is a real content position — the same
   place a publisher would put a leaderboard by hand.

   It cannot cover the map or the panorama at any viewport size, and that is
   structural rather than a z-index argument: `.page` begins where the hero
   ends, and this is a normal block inside it. Nothing about it is
   positioned, so nothing about it can float or follow the reader — which is
   the whole complaint about Google's own overlay formats.

   `:empty` collapse is what makes the no-ads case a true no-op: with
   nothing relocated there is no box, no margin and no reserved height. */
.ad-inflow {
  display: block;
  margin: 28px 0;
  text-align: center;
}
.ad-inflow:empty { display: none; }

/* Google sets its own width/margin on the container it injected, sized for
   where it MEANT to put the ad. Once relocated, this box is the authority. */
.ad-inflow > * {
  width: 100% !important;
  max-width: 100% !important;
  margin: 0 auto !important;
  float: none !important;
  position: static !important;   /* it was taken out of flow to sit on the map */
  inset: auto !important;
  z-index: auto !important;
}

/* ---------- Making the content column a good host for Auto ads (v35) ----------
   Decision 2026-07-29: NO manual `<ins>` units. In-page Auto ads are already
   serving in this column, so the job is not to add inventory — it is to make
   sure that whatever Google decides to drop here lands inside the column,
   centred, with sane spacing, and collapses when it does not fill. Google
   sizes the container it injects for where it MEANT to put the ad, so without
   this a unit measured for a wide slot overflows a 374px phone column.

   Scoped to `.page`, i.e. below the fold. Anything above the fold is handled
   by the relocation observer and the stage rules below.

   Deliberately NO reserved min-height. Reserving space would trade a layout
   shift for a permanent hole on every page view that does not fill, and fill
   here is intermittent. Requirement: zero impact when there are no ads. */
.page .google-auto-placed,
.page .ap_container,
.page ins.adsbygoogle:not([data-anchor-status]) {
  max-width: 100% !important;   /* beats Google's inline width on a phone */
  margin-left: auto !important;
  margin-right: auto !important;
  float: none !important;       /* Google floats some placements */
  clear: both;
}
/* Vertical rhythm, so an injected unit reads as a break in the copy rather
   than as part of the paragraph above it. */
.page .google-auto-placed,
.page .ap_container {
  margin-top: 24px !important;
  margin-bottom: 24px !important;
}
/* Unfilled units take no space at all. Google marks them itself, and this
   only ever applies BEFORE a fill — never hide a filled ad. */
.page ins.adsbygoogle[data-ad-status="unfilled"] { display: none !important; }

/* ---------- Manual in-content units: `ins.adsbygoogle.ad-inarticle` ----------
   Declared by hand in the page (markers 3DDV:AD:IN-ARTICLE-1 / -2, one above
   the FAQ heading and one after the FAQ list). They exist because Auto ads on
   its own NEVER filled this column: the stage takes the viewport, so Google
   finds no in-content insertion point it likes and only its own overlays —
   anchor, side rails, vignettes — ever appeared. Declaring the inventory is
   the only way to get an ad between the copy.

   Everything else these units need is already above: the width/centring/float
   reset, and the collapse on [data-ad-status="unfilled"]. The only gap was
   vertical rhythm, because that rule targets the containers Google INJECTS
   (.google-auto-placed / .ap_container), which is not what we author.

   Still deliberately no reserved min-height — same reasoning as above: a hole
   on every unfilled view is worse than the shift. Margins vanish with the box
   when the unit collapses. */
.page ins.adsbygoogle.ad-inarticle {
  margin-top: 28px !important;
  margin-bottom: 28px !important;
  clear: both;
}

/* `.fixture-list` is a grid, so an injected container becomes a grid item and
   would sit in a 2px-gap row pretending to be a fixture. Give it the full row
   and its own breathing room so it cannot be mistaken for a game — the
   fixtures are navigable links, and an ad that looks like one is a policy
   risk, not just a cosmetic one. */
.fixture-list > .google-auto-placed,
.fixture-list > .ap_container,
.fixture-list > ins.adsbygoogle {
  grid-column: 1 / -1;
  margin: 18px 0 !important;
}

/* ---------- Nothing renders inside the stage, ever ----------
   The observer relocates these before they can be filled, so this should
   never actually paint; it covers the frame between the node being inserted
   and the callback running, and any markup the observer does not recognise.
   Scoped to the stage only — `.ad-inflow` above, and Google's own anchors,
   side rails and vignettes, are outside it and stay fully visible.

   Note this is a LAST resort and only ever applies pre-fill: a unit that has
   already been filled is never hidden (that is a policy problem), which is
   why venue.js gates every move on `isFilled()` and why the measuring block
   cedes space to a filled overlay instead of trying to suppress it.

   `overflow: hidden` on .stage-hero already clips anything tall, but
   clipping is not enough: a 90px banner fits inside the hero perfectly well
   and lands right across the pitch. */
.stage-hero .google-auto-placed,
.stage-hero .ap_container,
.stage-hero ins.adsbygoogle:not([data-anchor-status]),
#stage .google-auto-placed,
#view3d .google-auto-placed,
#viewer .google-auto-placed,
#view3d ins.adsbygoogle,
#viewer ins.adsbygoogle {
  display: none !important;
}

/* ============================================================
   CROSS-LINKING MODULES (v19) — breadcrumbs, fixtures,
   road trips, related venues, prev/next.

   Markup is generated statically into each page by
   _tools/build_related.py (server-rendered on purpose: AI crawlers
   and Perplexity/GPTBot don't run JS, so JS-injected links wouldn't
   be seen).
   ============================================================ */

/* ---------- Breadcrumbs ---------- */
.breadcrumbs {
  margin: 0 0 22px;
  font-size: 0.8rem;
  line-height: 1.4;
}
.breadcrumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 8px;
  list-style: none;
}
.breadcrumbs li { display: flex; align-items: center; gap: 8px; color: var(--muted); }
.breadcrumbs li + li::before {
  content: "\203A";
  color: var(--dim);
  font-size: 0.95rem;
}
.breadcrumbs a { color: var(--muted); text-decoration: none; }
.breadcrumbs a:hover { color: var(--sky); text-decoration: underline; }
.breadcrumbs [aria-current="page"] { color: var(--ink); font-weight: 600; }

/* ---------- Shared section chrome for the new modules ---------- */
.fixtures, .related {
  margin: 44px 0 0;
  padding: 26px 24px;
  background: var(--panel);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
}
.fixtures h2, .related h2 {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.35rem, 3.2vw, 1.9rem);
  line-height: 1.1;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--bright);
}
.fixtures h2 { border-left: 4px solid var(--acc); padding-left: 12px; margin-left: -12px; }
.fixtures-sub {
  margin: 8px 0 18px;
  color: var(--muted);
  font-size: 0.9rem;
}
.fixtures-note {
  margin-top: 16px;
  font-size: 0.78rem;
  color: var(--dim);
}

/* ---------- Fixture list ---------- */
.fixture-list { list-style: none; display: grid; gap: 2px; }
.fixture {
  display: grid;
  grid-template-columns: 92px 1fr auto;
  align-items: center;
  gap: 14px;
  min-height: 52px;            /* 44px+ touch target */
  padding: 8px 12px;
  border-radius: 10px;
  background: var(--card);
  border: 1px solid transparent;
  transition: border-color 0.15s, background 0.15s;
}
.fixture:hover { border-color: var(--line); background: #19204C; }
.fx-date {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
}
.fx-dow { font-size: 0.7rem; color: var(--dim); letter-spacing: 0.08em; }
.fx-day { font-size: 1rem; color: var(--ink); }
.fx-teams { font-size: 0.92rem; color: var(--ink); }
.fx-vs { color: var(--dim); font-size: 0.78rem; padding: 0 4px; text-transform: uppercase; }
.fx-teams a { color: var(--sky); text-decoration: none; border-bottom: 1px solid transparent; }
.fx-teams a:hover { border-bottom-color: var(--sky); }
.fx-home { font-weight: 600; }
.fx-at { color: var(--dim); font-size: 0.78rem; padding: 0 4px; }

/* ---------- Live calendar (v20) ----------
   The whole season sits in the HTML; `is-hidden` is what keeps the block
   down to the next dozen games. build_related.py sets it for the build
   date, venue.js recomputes it against the reader's clock. Hiding with
   display:none (not visibility) so hidden rows cost no layout on pages
   carrying 81 MLB dates. */
.fixtures.is-hidden,
.fixtures .is-hidden { display: none; }

/* Neutral-site footnote reads as prose, but each game is its own span so
   played dates can drop out. The separator therefore cannot be baked into
   the markup: this puts "; " before every entry that still has a VISIBLE
   entry ahead of it, so the list re-punctuates itself as games fall away. */
.fx-abroad:not(.is-hidden) ~ .fx-abroad:not(.is-hidden)::before { content: "; "; }

/* Expanded = the full season, past games included and dimmed so the
   boundary between played and upcoming stays readable at a glance.
   Higher specificity than the rule above, so no !important needed. */
.fixtures.is-expanded .fixture-list > .fixture.is-hidden { display: grid; }
.fixtures.is-expanded .fixture.is-past { opacity: 0.52; }
.fixtures.is-expanded .fixture.is-past:hover { opacity: 1; }

.fx-more {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
  min-height: 44px;
  padding: 10px 18px;
  font: inherit;
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--sky);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.fx-more::after {
  content: "▾";
  font-size: 0.7em;
  transition: transform 0.15s;
}
.fixtures.is-expanded .fx-more::after { transform: rotate(180deg); }
.fx-more:hover { background: var(--card); border-color: var(--sky); color: var(--bright); }
.fx-more:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .fx-more, .fx-more::after { transition: none; }
}

@media (max-width: 620px) {
  .fixtures, .related { padding: 20px 16px; }
  .fixture {
    grid-template-columns: 1fr;
    row-gap: 4px;
    padding: 12px;
  }
  .fx-date { flex-direction: row; align-items: baseline; gap: 6px; }
}

/* ---------- Related venues ---------- */
.related h3 {
  margin: 22px 0 10px;
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
}
.related h3:first-of-type { margin-top: 16px; }
.venue-chips {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
  gap: 10px;
}
.venue-chips a {
  display: block;
  min-height: 44px;
  padding: 10px 14px;
  border-radius: 12px;
  background: var(--card);
  border: 1px solid var(--line-soft);
  text-decoration: none;
  transition: border-color 0.15s, transform 0.15s;
}
.venue-chips a:hover { border-color: var(--acc); transform: translateY(-1px); }
.vc-name { display: block; color: var(--ink); font-weight: 600; font-size: 0.9rem; }
.vc-meta { display: block; color: var(--dim); font-size: 0.75rem; margin-top: 2px; }
.related-all { margin-top: 22px; font-size: 0.88rem; font-weight: 600; }
.related-all a { color: var(--sky); text-decoration: none; }
.related-all a:hover { text-decoration: underline; }

/* ---------- Prev / next within the division ---------- */
.venue-nav {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin: 28px 0 0;
  font-size: 0.85rem;
}
.venue-nav a {
  color: var(--muted);
  text-decoration: none;
  padding: 10px 0;
  min-height: 44px;
  display: flex;
  align-items: center;
}
.venue-nav a:hover { color: var(--sky); }

@media (prefers-reduced-motion: reduce) {
  .venue-chips a { transition: none; }
  .venue-chips a:hover { transform: none; }
}

/* ============================================================
   MINIMAP INSIDE THE PANORAMA  (graduated 2026-07-29)
   Was _template/prototypes.css. The decision log for every number in here —
   and for the three approaches that were built and thrown away — is
   _template/PROTOTYPES.md. The JS half is the last block of venue.js.
   ============================================================ */
/* ============================================================
   "NAVIGATING…" VEIL
   A step between panoramas is close + open: without cover the user watched
   the old view, then the bare map flashing past, then a wait, then the new
   view — the machinery on display. This covers the stage from the press
   until the destination is painted, so the whole trip reads as one state.

   z-index 9 is the .loader layer, and the element is appended
   after #loader so it wins the tie and hides it. Deliberately BELOW
   .stage-top (Back to map, z-index 10) and .minimap-bar (25): a user who changes
   their mind mid-journey must still be able to leave or keep stepping.

   The card inside borrows .loader-card / .loader-chip /
   .loader-bar / .loader-label, so this surface and the loader showLoader()
   raises a moment later are the same object as far as the eye is concerned.
   ============================================================ */
.nav-veil {
  position: absolute;
  inset: 0;
  z-index: 9;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(9, 13, 35, 0.42);
  -webkit-backdrop-filter: blur(14px) saturate(0.85);
  backdrop-filter: blur(14px) saturate(0.85);
  opacity: 1;
  transition: opacity 0.26s ease;   /* must match LOADER_FADE in venue.js */
}
.nav-veil.is-out { opacity: 0; pointer-events: none; }
.nav-veil.hidden { display: none; }

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

/* ============================================================
   MINIMAP INSIDE THE PANORAMA = THE REAL DVM MAP, SHRUNK
   Bottom-left of the stage: bottom-right is taken by the zoom buttons and
   the Stadium Guide button, and the top corners by the chips.

   Earlier versions drew their own plan into an SVG — first dots, then
   rectangles from each node's `aabb`. Both were reconstructions, and the
   answer to "I want the ORIGINAL section polygons" is not a better
   reconstruction: the true outlines only exist inside DVM's own renderer
   (these maps paint to canvas, so there is no path to copy either).

   So nothing is redrawn. openPano() hides #viewer while the pano is open;
   here it is un-hidden and boxed into the corner instead. Same instance,
   already loaded and painted: real geometry, real availability colours, and
   clicking a section still opens it because that is onMapClick.
   No second viewer, no extra request, nothing to keep in sync.

   LAYOUT: the header (arrows + label) is PINNED to the bottom-left corner
   and the map box sits directly ON TOP of it. It used to be the other way
   round, with the header riding above a box anchored to the floor — so every
   time the box grew or shrank the arrows moved, and they moved away from a
   pointer travelling towards them. Now the only thing that ever changes size
   is the map, it grows up and to the right into empty stage, and nothing the
   user is aiming at moves. --mini-head keeps the two numbers in agreement.
   ============================================================ */
#stage {
  --mini-w: 148px;                /* map box at rest */
  --mini-w-open: 320px;           /* map box with the pointer on it */
  --mini-aspect: 1.5;             /* replaced with the venue's real aspect */
  --mini-edge: 16px;
  --mini-head: 40px;              /* header: 28px button + 2 x 6px padding */
}

/* ---- Desktop only, for now ----
   The whole interaction is hover: you point at the map and it grows. There
   is no touch equivalent worth inventing yet, and the previous answer — a
   permanently-open 320px box on `pointer: coarse` — put the panel straight
   over "Back to map", the one control a phone user actually needs.

   So on a phone NOTHING mounts: the minimap block checks the same media query
   before building the panel, and the rule that un-hides #viewer lives inside
   this block too. Both have to agree; un-hiding the map without the header
   and the dot would be worse than not having it. */
.minimap-bar, .minimap-dot, .pano-nav { display: none; }

@media (min-width: 641px) and (hover: hover) {

.minimap-bar { display: block; }
.minimap-dot { display: block; }
.pano-nav { display: flex; }

.stage-hero:has(#back-btn:not(.hidden)) #viewer.hidden {
  display: block !important;      /* beats .hidden above */
  inset: auto;                    /* it is .stage-canvas: absolute, inset 0 */
  left: calc(var(--mini-edge) + max(var(--rail-l, 0px), var(--cmp-w, 0px)));
  /* Sits on its own header, and both clear whatever Google has parked in the
     bottom-left: the consent badge (--cmp-w) or a left side rail (--rail-l).
     max(), not a sum, because they occupy the same strip — same expression as
     .stage-overlay's left padding, and for the same reason.

     WHY THIS IS NEEDED AT ALL: `.stage` is inset:0 by invariant, so the
     panorama is full-bleed and its chrome lands hard against the viewport
     edge, exactly where the overlay formats live. Map-mode chrome has always
     dodged the rails (.stage-overlay, .zoom-ctrl, .scroll-hint); pano-mode
     chrome did not, so the minimap sat UNDER a left rail. Reported by David
     2026-07-31. The .stage comment above claimed the rails already moved the
     minimap — they never did; the claim was aspirational. */
  bottom: calc(var(--mini-edge) + var(--mini-head));
  width: var(--mini-w);
  height: calc(var(--mini-w) / var(--mini-aspect));
  z-index: 25;                    /* with the panel, over the veil */
  border: 1px solid var(--line);
  border-bottom: none;
  border-radius: 14px 14px 0 0;   /* the header below owns the bottom corners */
  overflow: hidden;
  background: rgba(13, 18, 48, 0.82);
  box-shadow: 0 -4px 18px rgba(0, 0, 0, 0.35);
  /* ENTRY IS A FADE, NEVER A RESIZE. Note what is NOT in this transition:
     width and height. openPano() hides #viewer at the exact moment the pano is
     revealed, so with width/height animated the map visibly crawled from
     full-stage size down into the corner — 180ms of DVM re-rendering and
     re-fitting every frame, on the same frames the panorama was fading in.
     That was the stutter. Now the box SNAPS to its corner size while it is
     still invisible, gets fitted there, and only then fades in (venue.js
     adds .is-in). The size transition is added by .is-in, so it exists for
     the hover growth and never for the entry. */
  opacity: 0;
  transition: opacity 0.25s ease;
}
.stage-hero:has(#back-btn:not(.hidden)) #viewer.hidden.is-in {
  opacity: 1;
  transition: opacity 0.25s ease, width 0.12s ease, height 0.12s ease;
}

/* HOVER GROWS THE WHOLE PANEL — map AND arrows. --mini-w drives both, so one
   class on #stage resizes the pair; a header stuck at 148px under a 320px map
   read as two unrelated things.

   WHO opens it is decided in venue.js, not here, because CSS cannot
   express it: the pointer must be ON THE MAP to open it (going for an arrow
   must never resize the thing you are aiming at), but once open, moving onto
   the arrows keeps it open (otherwise the arrows shrink away mid-reach). It
   closes the instant the pointer leaves both pieces — no delay: the quarter
   second this used to wait felt like the panel was ignoring you.

   WIDTH is what grows, not transform: scale() — a scale would blow up the
   border and the label with it and the map inside would go blurry at 2x.
   Growing the box makes DVM re-render at the new size, full quality (and
   the minimap block re-fits the plan into it on every frame of the transition). */
#stage.minimap-open { --mini-w: var(--mini-w-open); }

@media (prefers-reduced-motion: reduce) {
  .stage-hero:has(#back-btn:not(.hidden)) #viewer.hidden,
  .stage-hero:has(#back-btn:not(.hidden)) #viewer.hidden.is-in,
  .minimap-bar, .pano-nav { transition: none; }
}

/* ---- The hover popup over a boxed map: label only, and on top ----
   It is the tooltip above (#tooltip, filled from DVM's `enter`
   callback), so it keeps appearing over the minimap — and it arrived with
   two problems. It is z-index 11 while the map box is 25, so it was painted
   BEHIND the box it belongs to; and it carries a pano thumbnail card sized
   for the full-size map (min-width 220px, 124px tall), which over a 148px
   minimap is bigger than the map.

   Both fixed from here, in CSS only. Note that
   `viewer.flags.automatic_hover` is NOT a way to suppress this: that flag
   governs DVM's own hover STYLING, not the `enter` callback that paints the
   popup. (It is on — that styling is what lights the polygon up; see
   styleHover in venue.js.) */
.stage-hero:has(#back-btn:not(.hidden)) #tooltip { z-index: 30; }
.stage-hero:has(#back-btn:not(.hidden)) #tooltip .tooltip-thumb { display: none; }

/* "You are here": an HTML dot pinned over the map, tracking the section's
   anchor the same way the Best-view star does. */
.minimap-dot {
  position: absolute;
  z-index: 26;                    /* over the map box */
  width: 11px; height: 11px;
  margin: -5.5px 0 0 -5.5px;      /* centred on the anchor */
  border-radius: 50%;
  background: var(--acc);
  border: 2px solid #fff;
  box-shadow: 0 0 6px var(--acc);
  pointer-events: none;           /* never eats a click on its own section */
  visibility: hidden;
}

/* ---- The header: arrows + section label ----
   Its own element rather than a wrapper around #viewer: moving an element
   DVM owns into the bar is a bigger promise than we need to make: DVM
   measures its container, and a live canvas being reparented mid-visit is a
   class of bug nobody wants to chase.

   PINNED to the corner: `bottom` and `left` never change, so the arrows do
   not travel when the map above them grows (that was the old bug — `bottom`
   used to be derived from --mini-w). The WIDTH does follow the map, because a
   148px control bar under a 320px map read as two unrelated objects. Height
   is --mini-head so the map box knows exactly where to stop (box-sizing is
   border-box, set at the top of this file). */
.minimap-bar {
  position: absolute;
  /* Must be the SAME expression as the map box above — the two are one panel
     and any drift shows as a 1px step at the seam. */
  left: calc(var(--mini-edge) + max(var(--rail-l, 0px), var(--cmp-w, 0px)));
  bottom: var(--mini-edge);
  z-index: 25;
  width: var(--mini-w);
  height: var(--mini-head);
  padding: 6px;
  background: rgba(13, 18, 48, 0.82);
  backdrop-filter: blur(8px);
  border: 1px solid var(--line);
  border-top: none;
  border-radius: 0 0 14px 14px;
  font-family: var(--font-ui);
  opacity: 0;                     /* fades in with the map box */
  transition: opacity 0.25s ease, width 0.12s ease;
}
.minimap-bar.is-in { opacity: 1; }

.minimap-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 4px;
}

.minimap-label {
  flex: 1; text-align: center;
  font: 800 0.78rem/1 var(--font-display);
  text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--bright);
}

.minimap-step {
  width: 28px; height: 28px;
  display: grid; place-items: center;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--ink);
  font-size: 1rem; line-height: 1; cursor: pointer;
}
.minimap-step:hover:not(:disabled) { border-color: var(--accent); }
.minimap-step:disabled { opacity: 0.35; cursor: default; }
.minimap-step:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }

/* ---- The same two arrows, flanking the section pill at the top ----
   The minimap's arrows are 28px in the bottom corner, which is a long way
   from where the eye is (the pill that says which section you are looking at)
   and a long way from the pointer. These are the same two controls in the
   place the user is already reading, and they step the same walk.

   They straddle #stage-title, which is centred and changes
   width with the section id, so the wrapper is measured and placed from JS
   (see positionTopNav) rather than guessed here: one absolutely positioned
   flex row, the width of the pill plus a button and a gap either side, with
   the buttons pushed to its ends. The wrapper itself is transparent to the
   pointer so it never steals a click meant for the map underneath.

   z-index 12 clears .stage-top (10) and #stage-title (11). */
.pano-nav {
  position: absolute;
  z-index: 12;
  align-items: center;
  justify-content: space-between;
  transform: translateY(-50%);    /* placed by its vertical centre */
  pointer-events: none;
  visibility: hidden;             /* until the pill has been measured */
  opacity: 0;
  transition: opacity 0.25s ease;
}
.pano-nav.is-in { opacity: 1; }

.pano-nav-step {
  pointer-events: auto;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  background: rgba(13, 18, 48, 0.82);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--ink);
  font-size: 1.1rem; line-height: 1; cursor: pointer;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
}
.pano-nav-step:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--bright);
}
.pano-nav-step:disabled { opacity: 0.35; cursor: default; }
.pano-nav-step:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }

}   /* end desktop-only block */
