/* ============================================================
   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) - var(--lb-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.

   That left the map being overlapped by a rail, written down here as "the
   lesser evil". CORRECTED 2026-08-06: it is not a lesser evil, it is an ad
   covering the product, and there was a third exit nobody had looked for.
   The hero now carries `google-side-rail-overlap="false"` — AdSense's own
   documented per-section opt-out — so Google is not allowed to place a rail
   over it in the first place. Applied by _tools/apply_side_rail_guard.py,
   required by check_ads.py. What it fixes, measured in production: rails at
   left/right 485px on a 2133px viewport, 160x604, straight across the bowl.

   --rail-l / --rail-w stay: the attribute binds Auto ads and only over the
   hero, so anything else fixed and edge-pinned still has to be measured, and
   they 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, §4.1 and §4.5. */
.stage {
  position: absolute;
  inset: 0;
}
.stage-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* ---------- Leaderboard en flujo POR ENCIMA del hero (v41, 2026-08-06) -------
   La unica posicion above-the-fold que existe en estas paginas sin tocar el
   producto. Por que aqui y no otra cosa:

     * El hero mide 100dvh, asi que TODO el inventario declarado vivia debajo
       del fold y solo existia si el lector bajaba. La unidad in-article que se
       midio del 29 jul al 4 ago se llevaba el 30% de las impresiones y devolvia
       el 8% de los ingresos, con un 12-19% de viewability, precisamente por
       eso. Aqui la viewability es ~100% por construccion.
     * Es la unica que rinde en MOVIL, que es el 67% de las pv y el peor RPM del
       sitio. Un side rail no existe en movil; esto si.
     * Va EN FLUJO y FUERA de .stage-hero: no puede tapar el mapa ni la
       panoramica ni con creatividad grande. Cumple la regla de la §2 por
       construccion, no por medicion.

   ALTURA RESERVADA, y esto SI se sale de la norma de "nada de min-height" que
   rige el resto del inventario. La norma es correcta debajo del fold: alli un
   hueco en cada carga sin fill es peor que el salto. Aqui es al contrario —
   un anuncio que aparece tarde EMPUJA EL MAPA HACIA ABAJO en el primer
   viewport, y ese CLS lo paga el SEO, que es el motor de trafico de estos
   hubs. Asi que la banda ocupa su alto siempre, rellene o no, y el hero cede
   exactamente ese alto via --lb-h. Coste: 90px de 100dvh en desktop (~8%).

   Tamaños FIJOS por breakpoint en vez de `data-ad-format="auto"`: es el patron
   documentado por AdSense para unidades responsive por CSS, y es el unico que
   permite reservar el alto exacto. Con "auto" Google puede devolver una
   creatividad mas alta que la banda, y entonces las salidas son empujar el
   mapa (CLS) o recortar el anuncio (problema de politica). Ninguna vale.

   MOVIL: 320x100 desde el 2026-08-28. Estuvo a 320x50 desde el 6 de agosto y
   la razon era buena MIENTRAS el hero medía 100dvh: la banda salia por encima
   del h1 y cada px suyo empujaba contenido fuera del primer viewport. Con el
   mapa en un tercio (bloque MOVIL v2 al final del fichero) esa cuenta cambia
   de signo — el hero ya no le cede alto a --lb-h en movil, asi que los 50px de
   mas no se los quita al mapa: se los quita al hueco que dejo el rediseno. Es
   el unico movimiento de inventario del cambio; la DENSIDAD no se toca (ni una
   unidad nueva), que sigue siendo terreno del panel y no del codigo. */
:root { --lb-h: 0px; }
body:has(.ad-lb) { --lb-h: 100px; }       /* móvil: 320x100 (2026-08-28) */
.ad-lb {
  display: block;
  width: 320px;
  height: 100px;
  margin: 0 auto;
}
@media (min-width: 500px) {
  body:has(.ad-lb) { --lb-h: 60px; }
  .ad-lb { width: 468px; height: 60px; }
}
@media (min-width: 800px) {
  body:has(.ad-lb) { --lb-h: 90px; }
  .ad-lb { width: 728px; height: 90px; }
}
@media (min-width: 1200px) {
  body:has(.ad-lb) { --lb-h: 90px; }
  .ad-lb { width: 970px; height: 90px; }
}
/* COLAPSO CUANDO NO RELLENA (2026-08-17) — matiza lo de arriba, no lo deroga.
   El alto se reserva mientras la unidad esta PENDIENTE, que es cuando el CLS
   importa: un anuncio que llega tarde empuja el mapa. Pero `unfilled` no es
   "todavia no", es la respuesta FINAL de Google para esta vista. Reservar
   90px para siempre por una unidad que ya dijo que no es canal muerto encima
   del hero, y estaba pasando en las 260 paginas de venue: la regla general de
   colapso es `.page ins.adsbygoogle[data-ad-status="unfilled"]` y esta unidad
   es hija de <body>, fuera de `.page`, asi que nunca la alcanzaba.

   HAY QUE QUITAR LAS DOS COSAS o no se gana nada: ocultar el <ins> sin bajar
   --lb-h deja el hueco exactamente igual (se lo queda el hero, que le cedia
   ese alto), y bajar --lb-h sin ocultar el <ins> deja una banda de 90px
   comiendose el mapa.

   Especificidad, para que no dependa del orden del fichero:
   `.ad-lb[data-ad-status="unfilled"]` es (0,2,0) contra (0,1,0) de `.ad-lb`,
   asi que estas dos reglas ganan a las de CADA breakpoint aunque las media
   queries vengan antes — las media queries no suman especificidad. El
   `!important` del display SI hace falta: la pagina lleva `style="display:block"`
   inline en el <ins>, y un inline gana a cualquier regla de hoja sin él.

   EL PRECIO, dicho claro: el colapso ES un shift hacia arriba y cuenta como
   CLS igual que el de bajada. Se acepta porque solo ocurre en las vistas SIN
   relleno (donde no hay ingreso que proteger) y porque la respuesta de Google
   llega en los primeros cientos de ms. Si el coverage de esta unidad resultase
   bajo y el CLS de campo subiera, la salida NO es volver a reservar siempre:
   es bajar el alto reservado. */
body:has(.ad-lb[data-ad-status="unfilled"]) { --lb-h: 0px; }
.ad-lb[data-ad-status="unfilled"] { display: none !important; }

/* ---------- .ad-bm — banda mid-content (ad-unit-banner-mid) ----------
   Activada el 2026-08-29 y SOLO en premier-league, via SLOTS_BY_HUB de
   _tools/ad_units.py. Aqui el CSS es global a proposito: sin markup no pinta
   nada, y el dia que se active en otro hub no hay que volver a tocar shared/.

   RECETA DEL LEADERBOARD, no la de las in-article: tamano FIJO por breakpoint
   y sin data-ad-format="auto". Con "auto" Google elige el alto y deja de
   poderse razonar sobre el hueco — que es justo lo que hay que vigilar en una
   unidad nueva que se mete en mitad del texto.

   AQUI NO SE RESERVA ALTO, al reves que en .ad-lb: esta unidad vive debajo del
   fold, donde un hueco en cada visita sin relleno es peor que el salto. El
   colapso se lo da la regla general `.page ins[data-ad-status="unfilled"]`,
   que SI la alcanza (es hija de .page, no de <body> como el leaderboard).

   Los tamanos: 300x250 hasta 500px y 336x280 a partir de ahi son las dos
   medidas de rectangulo con demanda de verdad en movil, que es el 67% de las
   pv; 728x90 desde 800px porque la columna de texto (.seo-copy, 76ch) no da
   para mas sin recortar el ancho de lectura. */
.ad-bm {
  display: block;
  width: 300px;
  height: 250px;
  margin: 22px auto;
}
@media (min-width: 500px) { .ad-bm { width: 336px; height: 280px; } }
@media (min-width: 800px) { .ad-bm { width: 728px; height: 90px;  } }

/* ---------- 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;
  /* .stage-top is a one-line flex row and every chip is `white-space: nowrap`,
     so a long tour name ("Ariana Grande - 2026") pushed itself off the right
     edge of the viewport. Between 641px and 1099px there is no .lb-chip to
     take over, so the chip clips itself with an ellipsis instead. Below 641px
     it is dropped outright - see the mobile block. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.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); }
/* Two labels for the same button: the full phrase on a desktop, just "Map" on
   a phone, where the top row also has to fit the section pill to its right
   (2026-08-12). Both spans are painted by venue.js - the 244 pages keep the
   plain "Back to map" text as the no-JS fallback - so this only picks one. */
.back-btn-short { display: none; }

/* ---------- 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.

   .venue-title-m va en la lista porque por debajo de 641px este mismo h1 se
   MUEVE fuera del hero (bloque MOVIL v2 al final del fichero): si el estilo
   colgase solo de .stage-overlay, al salir se quedaria en un h1 pelado. */
.stage-overlay h1,
.venue-title-m 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,
.venue-title-m 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;
    /* The Stadium Guide button moved to the bottom CENTRE on 2026-08-12, so
       this corner is free and the pair drops to the same 12px margin. */
    bottom: 12px;
  }
}

/* ---------- 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; }
  /* room for the Stadium Guide button, which is centred at the bottom of the
     hero below: 10px margin + a ~40px pill + a line of air. */
  .stage-overlay { padding-bottom: 62px; }
  .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: the section pill sits in the TOP-RIGHT corner, level with
     the "Map" button (2026-08-12). It used to drop to a second centred row
     (top: 68px), which ate the upper third of the panorama and read like an
     orphan heading. 16px is the button's vertical centre (12px of .stage-top
     padding + half of its 44px minimum) less half a pill; the 110px taken off
     the width is the button's own room. The top arrows never collide: .pano-nav
     only exists above 641px with a hover pointer. */
  .stage-hero:has(#back-btn:not(.hidden)) #stage-title {
    top: 16px;
    left: auto;
    right: 12px;
    transform: none;
    max-width: calc(100% - 110px);
  }
  /* centred at the bottom of the hero, hard against its edge (2026-08-12).
     Down in the right corner it landed on top of #stage-title, which starts at
     the left edge of the overlay and wraps to two lines on a narrow screen: the
     hint read as if it went under the button. Centred, each one owns its own
     line and the button is where the thumb already is. The :hover has to repeat
     the translateX or a tap knocks it off centre. */
  .scroll-hint {
    left: 50%;
    right: auto;
    bottom: 10px;
    transform: translateX(-50%);
    font-size: 0.66rem;
    padding: 11px 16px;
  }
  .scroll-hint:hover { transform: translateX(-50%) translateY(-1px); }
  /* The tour chip goes: the same fact is already in .match-badge, right above
     the title, where there is room for it. */
  .stage-top .brand-chip { display: none; }
  /* and the button says "Map" (see .back-btn-short above) */
  .back-btn-long { display: none; }
  .back-btn-short { display: inline; }
}
/* 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.

   OJO (2026-08-28): esto ya NO rige en movil. Por debajo de 641px el bloque
   MOVIL v2 del final del fichero lo pisa con `touch-action: none` — misma
   especificidad y ambas !important, asi que decide el orden. Aqui manda de
   641px para arriba, donde el mapa sigue siendo alto y el gate sigue siendo
   correcto. */
@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 inside #view3d and knows nothing about our ad
   slots. Desde el 2026-08-08 su boton de fullscreen vive ABAJO A LA DERECHA
   (decision de David: "en YouTube donde estan los botones de fullscreen? pues
   eso"), porque arriba a la derecha pasa a estar el boton de expandir el
   contenedor. Ver el bloque largo de mas abajo.

   AQUI NO VA NINGUN OFFSET POR ADS (corregido 2026-08-20). Habia un
   `margin-bottom: var(--anchor-bot-h)` heredado de cuando el boton estaba
   arriba, con la premisa de que "sobre el fullscreen se vuelve a pintar el
   anchor". La premisa es FALSA: el fullscreen de DVM es la Fullscreen API de
   verdad — `document.fullscreenElement` es su `.dvm-v3d-main` —, asi que el
   visor pasa al TOP LAYER y NADA del documento se pinta encima.
   Comprobado en vivo el 2026-08-20 sobre mlb/fenway-park: un div de prueba
   `position:fixed; z-index:2147483647` pegado al borde inferior NO gana el
   hit-test dentro del fullscreen (`elementFromPoint` devuelve la capa de DVM).
   Con el anchor de 262px que sirve Google en produccion ese margen subia el
   boton de SALIR 262px respecto al de ENTRAR — el bug que reporto David.
   El anclaje del boton en fullscreen se fija abajo, con el resto de reglas de
   posicion del wrapper. */

/* ---------- 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%; }

/* ---------- Lateral en el MARGEN, fuera de la caja de contenido (v43) -------
   Lo que David pedia: recuperar el lateral, pero abajo, en la guia. Y es la
   diferencia entre un rail de Google y uno nuestro:

     * el de Google es `position: fixed`, se coloca UNA vez al cargar y
       google-side-rail-overlap es un VETO, no un horario. No existe "solo por
       debajo del fold": si la banda fija se solapa con el hero en scrollY=0,
       Google descarta el rail entero.
     * este esta anclado a `.page`, o sea que por construccion no existe fuera
       de la guia y no puede tocar el mapa ni la panoramica.

   CORRECCION 2026-08-06 (v42 -> v43), y es la parte importante. La v42 metia el
   rail DENTRO de la caja: `.page` a flex, con el rail como segunda columna. Eso
   le quitaba 324px al ancho de contenido, asi que el texto (y el calendario, y
   related) se quedaban a 796px y, sobre todo, **descentrados respecto a la
   ventana**: la caja seguia centrada pero la lectura ya no. David lo canto a la
   primera y tiene razon — y ojo, la respuesta NO era poner otro anuncio al otro
   lado para "equilibrar" (ver mas abajo).

   Ahora el rail va FUERA de `.page`, en el margen que ya estaba vacio:

     * `.page` vuelve a ser exactamente lo que era — 1160px centrados, 1120
       utiles. CERO reflow: ni el ancho de lectura ni el calendario ni related
       cambian un pixel respecto a antes de todo esto, rellene o no.
     * el texto queda centrado en la ventana, que es el requisito.
     * el anuncio ocupa el hueco donde Google intentaba poner SU rail, que es
       justamente donde no hay nada.

   EL PRECIO, dicho claro: solo aparece si el margen da de si. Con 1160px de
   caja, un 160x600 + 12 de aire necesita 1160 + 2*172 = 1504px de ventana, y un
   300x600 necesita 1160 + 2*312 = 1784px. Los breakpoints van a 1512 y 1820.

   EL AIRE BAJO DE 24 A 12 Y EL BREAKPOINT DE 1540 A 1512 el 2026-08-11, y la
   razon es aritmetica, no estetica: a 1540 el rail no se servia casi nunca.
   El breakpoint mide VIEWPORT y GA4 reporta `screen resolution` con el escalado
   del SO ya aplicado, asi que los dos buckets de portatil mas comunes se
   quedaban a un pelo — el MacBook 14" reporta 1512 (viewport 1512 clavado, en
   macOS no hay barra de scroll que reste) y un 1920 de Windows al 125 % reporta
   1536 (viewport ~1521). Faltaban 28 y 19 px respectivamente.

   Medido sobre 16 dias de GA4 (27 jul - 11 ago, solo sesiones de hub):
   con 1540 calificaba el 15,3 % de las sesiones de hub; con 1512, el 19,1 %.
   +24 % de impresiones potenciales del rail, y casi todo sale de un solo
   bucket, el 1536. El siguiente escalon de verdad son 1470 y 1440 (otras 572
   sesiones) y esos NO se pueden capturar bajando el breakpoint: hay que
   estrechar `.page` o el rail, o sea tocar el contenido. No se hace.

   Colchon: a 1512 exactos el borde derecho del anuncio cae en 1508 — 4 px de
   aire y CERO scroll horizontal (verificado en headless a 1504/1512/1521/1536/
   1540 el 2026-08-11). A 1521 sobran 8. Por debajo NO HAY RAIL, porque la
   alternativa es volver a comerse el contenido. Un portatil de 1440 se queda
   sin el; un monitor de 1920 lo tiene (sobran 388 por lado).

   Simetria: el margen izquierdo queda vacio a proposito. La regla de la §2 del
   diseño es que si se cede ancho se cede igual por los dos lados, y aqui no se
   cede ninguno — el layout ES simetrico, el anuncio no. Meter un segundo rail
   a la izquierda NO compra simetria: el fill es por unidad e intermitente, asi
   que la mitad de las veces rellenaria uno solo y volveriamos a la asimetria,
   con el doble de canal muerto cuando no rellena ninguno. Y subiria a 5 las
   unidades por pagina, que es el patron exacto que hundio la viewability del
   29 jul al 4 ago. DECIDIDO ASI CON DAVID el 2026-08-06: un solo lateral, a la
   derecha. Si algun dia los numeros del rail derecho lo justifican, el hueco
   izquierdo esta ahi y es una regla de CSS mas un slot.

   Debajo del breakpoint la columna es `display: none`, y eso ademas hace que la
   unidad no se pida nunca: el observer del bloque 3DDV:AD:LAZY va por
   IntersectionObserver y un elemento con display:none no interseca jamas. Sin
   pushes de unidades invisibles, que salen unfilled y son mala practica. */
.ad-rail { display: none; }

@media (min-width: 1512px) {
  .page { position: relative; }
  .ad-rail {
    display: block;
    position: absolute;
    top: 0;
    left: 100%;              /* borde derecho de .page */
    margin-left: 12px;
    width: 160px;
    height: 100%;            /* recorrido para el sticky */
  }
  .ad-rail .adsbygoogle {
    position: sticky;
    top: 24px;
    display: block;
    width: 160px;
    height: 600px;
  }
}
@media (min-width: 1820px) {
  .ad-rail { width: 300px; }
  .ad-rail .adsbygoogle { width: 300px; height: 600px; }
}

.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); }

/* 3DDV:CTA-LIVE — el parrafo se hornea VACIO y lo rellena venue.js segun el
   reloj del visitante (bloque CTA-LIVE de venue.js). Sin JS no se ve nada:
   el lede ya esta completo sin el, y un parrafo vacio con margen dejaria un
   hueco que nadie entiende. */
.seo-copy .cta-live:empty { display: none; }
.seo-copy .cta-live { 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; }
/* La noche de ESTE recinto, resaltada. `is-self` la emiten los cinco
   generadores de gira de /music desde que nacio el bloque de calendario y
   hasta el 2026-08-29 NO LA ESTILABA NADIE: la copy prometia "this venue's
   own nights are highlighted" en 76 paginas y era falso.
   Se pinta con el borde que .fixture YA reserva (1px solid transparent) y un
   fondo. Nunca con padding, ni con un borde extra, ni con un ::before de caja:
   cualquiera de esos cambia el alto de la fila y mueve las unidades de abajo.
   Si color-mix no esta soportado, la declaracion se descarta y queda el
   `background: var(--card)` de .fixture — degrada al estado de hoy. */
.fixture.is-self {
  border-color: var(--acc);
  background: color-mix(in srgb, var(--acc) 12%, var(--card));
}
.fixture.is-self:hover {
  background: color-mix(in srgb, var(--acc) 18%, var(--card));
}
.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 */

/* =====================================================================
   3DDV:YT-LAYOUT:CSS — layout tipo YouTube de la pagina de venue
   =====================================================================
   Visor a la izquierda, rail de venues relacionados a la derecha. Una pagina
   lo lleva si tiene el markup `3DDV:YT-LAYOUT:RAIL`, que hornea
   _tools/apply_yt_layout.py. Sin ese markup, nada de esto aplica salvo dos
   reglas deliberadamente globales (los page-dots y las guias .ad-slot).

   Sobre la invariante de `.stage`: sigue siendo `inset: 0` y sigue centrado y
   simetrico DENTRO de su contenedor. Lo que cambia es el contenedor — el hero
   deja de ser a sangre y pasa a ser una columna. Es una decision de layout con
   contenido real al lado (David, 2026-08-07), no un inset por un anuncio, que
   es lo que se rechazo el 2026-07-29 y sigue rechazado.
   ===================================================================== */
:root{
  --yt-rail-w: 372px;   /* ancho del rail en desktop */
  --yt-gap: 20px;
  --yt-pad: 20px;
}

/* ---------- Rejilla de dos columnas ---------- */
.yt-shell{
  display:grid;
  grid-template-columns: minmax(0,1fr) var(--yt-rail-w);
  gap: var(--yt-gap);
  padding: 12px var(--yt-pad) 0;
  align-items:start;
  transition: grid-template-columns .28s ease, gap .28s ease;
}
body.yt-collapsed .yt-shell{ grid-template-columns: minmax(0,1fr) 0px; gap:0; }
body.yt-collapsed .yt-rail{ opacity:0; pointer-events:none; }

@media (min-width:1100px){
  :root{
    --yt-hero-h: calc(var(--stage-h) - var(--anchor-top-h,0px) - var(--anchor-bot-h,0px) - var(--lb-h,0px) - 12px);
  }
  .yt-shell .stage-hero{
    height: var(--yt-hero-h);
    border-radius:14px;
    border:1px solid var(--line-soft);
  }
  .yt-rail{ height: var(--yt-hero-h); }
}

/* Los puntos de pagina fuera (decision de David, 2026-08-07): con el rail
   ocupando el lateral ya no aportan nada y competian por el mismo borde.
   Aqui se ocultan por CSS; cuando esto se gradue a shared/, quitar tambien
   la inyeccion en venue.js en vez de taparla. */
.page-dots{ display:none !important }

/* ---------- El rail ---------- */
.yt-rail{
  position:relative;
  overflow-y:auto;
  overflow-x:hidden;
  scrollbar-width:thin;
  scrollbar-color: rgba(168,176,212,.35) transparent;
  transition:opacity .2s;
}
.yt-rail::-webkit-scrollbar{ width:8px }
.yt-rail::-webkit-scrollbar-thumb{ background:rgba(168,176,212,.28); border-radius:8px }

.yt-rail-top{
  position:sticky; top:0; z-index:4;
  display:flex; align-items:center; gap:8px;
  padding:2px 0 10px;
  background:linear-gradient(var(--bg) 70%, rgba(0,0,0,0));
}
.yt-rail-h{
  font-family:var(--font-display); font-weight:800; font-size:1.05rem;
  letter-spacing:.05em; text-transform:uppercase; color:var(--bright);
}
.yt-proto-tag{
  font:800 9px/1 var(--font-ui); letter-spacing:.12em; text-transform:uppercase;
  color:#0B1026; background:#F2C14E; padding:4px 6px; border-radius:4px;
}
/* Vive DENTRO del mapa, arriba a la derecha (.stage-top), desde el 2026-08-08:
   el boton que expande el CONTENEDOR pertenece al contenedor, no al rail que
   colapsa. Dos consecuencias buenas: sigue visible con el rail colapsado (o
   sea que la vuelta es el mismo boton en el mismo sitio, y por eso se borro
   la pestaña vertical .yt-restore que existia solo para eso), y el usuario no
   tiene que cruzar la pagina para deshacer lo que acaba de hacer.
   Mismo lenguaje visual que .chip y .pano-nav-step: son vecinos. */
.yt-expand{
  margin-left:auto; cursor:pointer;
  display:inline-flex; align-items:center; gap:8px;
  font:700 11px/1 var(--font-ui); letter-spacing:.06em; text-transform:uppercase;
  color:var(--ink); background:rgba(13,18,48,.82);
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
  border:1px solid var(--line); border-radius:999px; padding:10px 14px;
  box-shadow:0 4px 18px rgba(0,0,0,.35);
  transition:background .15s, border-color .15s, color .15s;
}
.yt-expand:hover{ background:var(--accent); border-color:var(--accent); color:#fff }

/* ---------- Pastillas ---------- */
.yt-grp{
  font:700 10px/1 var(--font-ui); letter-spacing:.1em; text-transform:uppercase;
  color:var(--dim); margin:16px 0 8px;
}
.yt-grp:first-of-type{ margin-top:2px }

/* Titulo de CONTINUACION: el que `venue.js` repite en la caja B cuando el
   corte de la unidad intercalada cae DENTRO de un grupo. Existe para que un
   grupo partido por un anuncio se siga entendiendo, o sea que solo tiene
   sentido si hay anuncio. Si Google no rellena, `.yt-ad:has(ins[unfilled])`
   colapsa la unidad entera y este titulo queda pegado al listado de arriba:
   el mismo texto dos veces seguidas y sin nada en medio.
   Lo vio David el 2026-08-19 en /nhl/grand-casino-arena. Salta en cualquier
   recinto con mas de PILLS_PER_AD (4) vecinos de division: la Central de la
   NHL tiene 6, la de la NBA 4, por eso no habia aparecido hasta ahora.
   Se oculta en los tres casos en los que la unidad no esta ocupando sitio.
   `:has()` es lo que permite reaccionar a un estado que llega DESPUES del
   render, la misma tecnica que `.ad-lb[data-ad-status="unfilled"]`. */
body:has(.yt-ad-inline ins[data-ad-status="unfilled"]) .yt-grp-cont,
body.yt-ads-sticky .yt-grp-cont,
body.yt-ads-none .yt-grp-cont{ display:none }
.yt-pills{ list-style:none; display:flex; flex-direction:column; gap:8px; margin:0; padding:0 }
.yt-pill > a{
  display:grid; grid-template-columns:158px 1fr; gap:11px;
  padding:6px; border-radius:12px;
  text-decoration:none; color:inherit;
  transition:background .15s;
}
.yt-pill > a:hover{ background:rgba(255,255,255,.07) }
.yt-pill > a:focus-visible{ outline:2px solid var(--bright); outline-offset:2px }
.yt-thumb{
  position:relative; aspect-ratio:16/9; border-radius:9px; overflow:hidden;
  background:#131A33; border:1px solid var(--line-soft);
}
.yt-thumb img, .yt-thumb svg{ width:100%; height:100%; object-fit:cover; display:block }
.yt-sec{
  position:absolute; right:5px; bottom:5px;
  font:700 9px/1 var(--font-ui); letter-spacing:.05em;
  background:rgba(4,8,22,.82); color:#fff; padding:4px 5px; border-radius:4px;
}
.yt-txt{ min-width:0; padding-top:2px }
.yt-name{
  font:700 .84rem/1.25 var(--font-ui); color:var(--bright);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.yt-meta{ font:500 .72rem/1.35 var(--font-ui); color:var(--muted); margin-top:5px }

/* Las guias .ad-slot del _template (cajas punteadas "AD — ANCHOR TOP",
   "AD — SIDE RAIL") eran una ayuda visual del molde para imaginar donde caian
   los overlays de Google. Ya no hacen falta: los anuncios reales se ven. */
.ad-slot{ display:none !important }

/* ---------- Anuncios del rail ----------
   Dos colocaciones, dos slots propios (regla de _tools/ad_units.py: una
   colocacion, un slot; compartirlos es lo que dejo un mes de datos ciegos):
     rail-top    7762234624  — sticky en la cabecera del rail
     rail-inline 5084100397  — una sola, tras la cuarta pastilla

   Formato HORIZONTAL y bajo: un 300x250 en un rail de 372 px se comia el
   tercio superior y empujaba las pastillas bajo el fold, que es justo lo que
   el rail viene a arreglar. El alto va RESERVADO (cero CLS), igual que hace el
   leaderboard con --lb-h.

   Que un anuncio no parezca una pastilla mas: ancho completo del rail,
   etiqueta "Advertisement" encima y separacion propia. Nunca dentro de la
   lista. */
/* El alto reservado va en el ENVOLTORIO, no en el <ins>: AdSense reescribe el
   style del <ins> (height cuando rellena, display:none cuando no), asi que un
   height puesto ahi no reserva nada — medido el 2026-08-07, las dos unidades
   median 0 px de alto sin relleno.
   Y se reserva solo mientras la unidad esta PENDIENTE: en cuanto Google dice
   data-ad-status="unfilled" el bloque desaparece entero, etiqueta incluida.
   Asi no hay salto cuando rellena ni una banda muerta con la palabra
   "Advertisement" encima de la nada cuando no. */
.yt-ad{ margin:14px 0; min-height:156px }        /* 140 de unidad + etiqueta */
.yt-ad:has(ins[data-ad-status="unfilled"]){ display:none }
.yt-ad-label{
  display:block; font:700 9px/1 var(--font-ui); letter-spacing:.14em;
  text-transform:uppercase; color:var(--dim); margin-bottom:5px;
}
.yt-ad-unit{ display:block; width:100%; min-height:140px }
/* NO es sticky, y no vuelve a serlo. Lo era (top:44px, z-index:3, fondo opaco
   var(--bg) y una sombra de 12 px hacia abajo) para ganar viewability en el
   slot rail-top, y el precio era que la lista pasaba POR DEBAJO del bloque:
   medido el 2026-08-26 en produccion a 1920 px, con el rail scrolleado solo
   120 px quedaban tapados un .yt-grp entero y una pastilla completa detras del
   fondo opaco. Y en reposo el bloque acababa a 2 px del titulo "More venues
   in ..." con la sombra pintando encima de las letras: eso es el titulo gris
   que vio David. Depende de la creatividad servida (la reserva son 140 px de
   unidad; las de chips tipo "Discover more" piden mas) y de si el rail esta
   scrolleado, de ahi el "no pasa siempre".
   Misma regla que [[adsense-side-rails-no-tapar]] llevada al rail: si un
   formato no se puede colocar sin tapar, se apaga el formato — aqui se apaga
   el sticky. Estatico no puede tapar nada en ningun scroll.
   El alto sigue RESERVADO (cero CLS) y ahora la separacion con el primer
   titulo es real: 22 px, sin sombra que la coma. */
.yt-ad-top{
  margin:2px 0 22px; min-height:156px;
}
.yt-ad-inline{ margin:20px 0 16px }

/* Comparador: ?railads=both (defecto) | sticky | inline | none
   Sirve para ver el mismo rail con y sin cada unidad. */
body.yt-ads-sticky .yt-ad-inline{ display:none }
body.yt-ads-inline .yt-ad-top{ display:none }
body.yt-ads-none .yt-ad{ display:none }

/* ---------- Botones de fullscreen de DVM ----------
   Los pinta DVM dentro de #view3d con su propio CSS y desentonaban (chrome de
   terceros sobre la panoramica). Aqui SOLO se repintan: azul corporativo
   translucido, borde --line, pildora, hover --accent. Mismo lenguaje que
   .zoom-btn y .pano-nav-step.

   ESTE BLOQUE NO TOCA LA POSICION — ni position, ni top/right, ni margin, ni
   transform: aqui solo se repinta. La posicion la ponen las reglas de
   `.dvm-v3d-interface-wrapper` de mas abajo. (Antes esta nota remitia a un
   `margin-top: var(--anchor-top-h)` que ya no existe: el boton se mudo abajo
   el 2026-08-08 y el offset por ads en fullscreen se retiro el 2026-08-20.)

   DOM real de DVM, inspeccionado en vivo el 2026-08-07 con una pano abierta:
     <div class="dvm-v3d-interface-elem dvm-v3d-button dvm-v3d-button-fullscreen
                 dvm-v3d-toggle-fullscreen">
       <span class="dvm-v3d-icon-1-5x dvm-v3d-icon-white">
         <svg stroke="currentColor" fill="none">…
   Dos consecuencias:
     * .dvm-v3d-button cubre tambien -home y -toggle-vr, que hoy van ocultos.
       Si algun dia se muestran, saldran con el mismo estilo, que es lo que se
       quiere.
     * El icono es de TRAZO y usa currentColor, asi que hereda `color` solo.
       NO forzar fill en los <path>: rellenaria un icono que es de linea. */
/* ABAJO A LA DERECHA, no arriba (2026-08-08). Arriba a la derecha vive ahora
   #yt-expand, que expande el CONTENEDOR; el fullscreen del visor 3D va donde
   lo pone YouTube. La esquina esta libre en pano: .zoom-ctrl se oculta y
   .scroll-hint se desvanece cuando #back-btn deja de estar hidden, asi que
   aqui no choca con nada. Se ataca el wrapper POR SU CONTENIDO (:has) y no
   por la clase .dvm-v3d-top-right: si DVM cambia de esquina el boton, la
   regla lo sigue. El `right` copia el de .zoom-ctrl / .scroll-hint para que
   los tres compartan el mismo borde y esquiven un side rail igual. */
#view3d .dvm-v3d-interface-wrapper:has(.dvm-v3d-button-fullscreen){
  top: auto !important;
  bottom: 18px !important;
  right: calc(18px + var(--rail-w, 0px)) !important;
  left: auto !important;
}

/* EN FULLSCREEN, 18px A SECAS (2026-08-20). Las compensaciones de arriba son
   para el hero, donde el rail y el anchor de Google si conviven con el visor.
   En el fullscreen propio de DVM el visor esta en el top layer y no hay ni un
   ad que esquivar (nota larga en el bloque de `#view3d .dvm-v3d-main-fullscreen`
   de mas arriba), asi que compensar deja el boton de SALIR desplazado respecto
   al de ENTRAR: eso era el bug. Mismo borde en los dos modos = mismo anclaje.
   El `bottom` se repite aunque hoy coincida: si manana cambia el del hero,
   este no debe seguirlo. */
#view3d .dvm-v3d-main-fullscreen .dvm-v3d-interface-wrapper:has(.dvm-v3d-button-fullscreen){
  bottom: 18px !important;
  right: 18px !important;
}
/* La excepcion es el fullscreen "fake" de DVM (`.dvm-v3d-main-fullscreen-fake`,
   el `position:fixed` que usa cuando NO hay Fullscreen API — iPhone). Ahi el
   visor NO esta en el top layer, el anchor y el rail SI se pintan encima y la
   compensacion es correcta, asi que ahi se conserva. Va DESPUES de la regla de
   arriba a proposito: misma especificidad, gana por orden. */
#view3d .dvm-v3d-main-fullscreen-fake .dvm-v3d-interface-wrapper:has(.dvm-v3d-button-fullscreen){
  bottom: calc(18px + var(--anchor-bot-h, 0px)) !important;
  right: calc(18px + var(--rail-w, 0px)) !important;
}

#view3d .dvm-v3d-button{
  /* DVM pinta sus botones con `opacity: .75`, asi que el mismo
     rgba(13,18,48,.82) salia lavado (~.61 de alfa efectiva) y el boton de
     fullscreen se veia gris azulado al lado de .pano-nav-step, que va opaco.
     Se fuerza opacity:1: el color lo pone SOLO el background de abajo.
     (Comprobado en vivo el 2026-08-08 sobre yankee-stadium.) */
  opacity: 1 !important;
  background: rgba(13,18,48,.82) !important;
  border: 1px solid var(--line) !important;
  border-radius: 999px !important;
  color: var(--ink) !important;
  box-shadow: 0 4px 18px rgba(0,0,0,.35) !important;
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  transition: background .15s, border-color .15s, color .15s;
}
#view3d .dvm-v3d-button:hover{
  background: var(--accent) !important;
  border-color: var(--accent) !important;
  color: #fff !important;
}

/* ---------- Movil: el rail baja y se vuelve UNA sola tira ----------
   Decision 2026-08-07: en movil (67% de las pv) los dos grupos con sus dos
   titulos metian ~700 px entre el mapa y la guia, o sea que retrasaban la
   seo-copy y las dos unidades in-article. Aqui van los 8 venues en UNA tira
   horizontal: los `ul` de cada grupo pasan a `display: contents` y sus `li`
   caen como hijos directos del carrusel, sin duplicar el HTML ni ramificar el
   JS por ancho de pantalla (asi girar el movil no rompe nada).

   Los titulos de grupo se ocultan: en una tira horizontal partirian el scroll
   y el nombre del venue ya dice lo que hace falta.

   Y NINGUN anuncio en el rail en movil: un lateral no cabe, el
   leaderboard-top ya cubre el above-the-fold de movil y meter una unidad mas
   aqui solo empujaria la guia mas abajo. OJO: `.yt-ad{display:none}` de aqui
   abajo NO basta para la unidad inline — ver la regla de especificidad al final
   de este bloque. */
@media (max-width:1099px){
  .yt-shell{ display:block; padding:0 }
  .yt-rail{
    height:auto; overflow:visible;
    padding:18px 0 6px;
    border-bottom:1px solid var(--line-soft);
  }
  /* El h2 a la DERECHA (David, 2026-08-12): el asa de plegado del anchor de
     Google se pinta abajo-a-la-izquierda, FUERA de la banda que --anchor-bot-h
     reserva, asi que en el peek del hero se comia "MORE IN 3D". A la derecha
     no hay nada que lo tape. */
  .yt-rail-top{ position:static; background:none; padding:0 16px 12px;
                justify-content:flex-end }
  .yt-expand{ display:none }
  .yt-ad{ display:none }

  /* Una sola tira: #yt-pills-root es el carrusel y todo lo que hay dentro
     (los dos contenedores de pastillas y sus <ul> por grupo) pasa a
     `display: contents`, asi que los <li> caen como hijos directos del flex.
     Cero HTML duplicado y cero ramas por ancho de pantalla en el JS. */
  #yt-pills-root{
    display:flex; gap:12px;
    overflow-x:auto; -webkit-overflow-scrolling:touch;
    scroll-snap-type:x mandatory;
    padding:0 16px 12px;
    scrollbar-width:none;
  }
  #yt-pills-root::-webkit-scrollbar{ display:none }
  #yt-pills-root .yt-grp{ display:none }
  #yt-pills-root > div,
  #yt-pills-root .yt-pills{ display:contents }

  /* ...pero el envoltorio del anuncio NO. `#yt-pills-root > div` (1,0,1) le
     ganaba a `.yt-ad{display:none}` (0,1,0) de arriba, asi que el wrapper caia
     en `display:contents` y sus hijos — la etiqueta y el <ins> — se convertian
     en items del flex: el anuncio salia como UNA PASTILLA MAS del carrusel
     (medido 372x179 el 2026-08-11). Un anuncio con la misma forma que un enlace
     de navegacion es layout enganoso, o sea riesgo de politica, no un detalle
     estetico. Se reafirma aqui con (1,1,0) en lugar de tocar la regla de
     arriba: la de arriba dice "los contenedores de pastillas desaparecen" y esa
     intencion no cambia.
     Al quedarse sin caja, el bloque 3DDV:AD:LAZY lo SACA del DOM en la carga,
     asi que tampoco se come el push de la siguiente unidad. */
  #yt-pills-root > .yt-ad{ display:none }
  .yt-pill{ flex:0 0 224px; scroll-snap-align:start }
  .yt-pill > a{ grid-template-columns:1fr; gap:8px; padding:0 }
  .yt-rail .related-all{ padding:0 16px }
}
/* ---------- La pildora del hub sube a la banda del leaderboard ----------
   David, 2026-08-08. La píldora ("MLB · 3D Seat Views") no dice nada del mapa:
   dice en que hub estas. Sale del contenedor y se va arriba del todo, a la
   izquierda del leaderboard, y su hueco lo ocupa #yt-expand.

   El markup lo hornea _tools/apply_yt_layout.py: un <div class="lb-band"> con
   una COPIA de la píldora (.lb-chip), JUSTO ANTES del bloque del leaderboard y
   FUERA de el. Fuera a proposito: ad_units.apply() reescribe ese bloque entero
   cada vez que corre, asi que cualquier markup nuestro metido dentro dura
   hasta el siguiente migrate_lazy_ads.py.

   Copia y no mudanza, tambien a proposito: por debajo de 1100px no hay Expand
   map que poner en su lugar (no hay rail que colapsar) ni hueco al lado del
   banner, asi que ahi la de dentro del mapa se queda tal cual y esta no se
   pinta. Solo una de las dos se ve a la vez -> display:none, o sea que
   tampoco hay texto duplicado para un lector de pantalla.

   ALTO CERO: la banda no ocupa nada en el flujo y la píldora se centra sobre
   el anuncio con `top: --lb-h/2`. Cualquier px de mas aqui es CLS en el primer
   viewport, que es justo lo que compra ese --lb-h.

   El `max-width` es la guarda de politica de AdSense: la píldora nunca puede
   morder el anuncio, asi que se le da como mucho el hueco libre a su
   izquierda (mitad de la ventana menos media unidad menos el margen). Si un
   hub de nombre largo no cupiese, se recorta con puntos suspensivos: feo,
   pero nunca encima de la creatividad. */
:root{ --lb-w: 320px }
@media (min-width:500px){  :root{ --lb-w: 468px } }
@media (min-width:800px){  :root{ --lb-w: 728px } }
@media (min-width:1200px){ :root{ --lb-w: 970px } }

.lb-band{ position:relative; height:0 }
.lb-chip{ display:none }
@media (min-width:1100px){
  /* Si el leaderboard sale unfilled y colapsa (ver el bloque de .ad-lb) no hay
     banda donde poner la pildora: se apaga la de fuera y vuelve la de DENTRO
     del mapa. Sin esto la .lb-chip se quedaria con top:0 y translateY(-50%),
     o sea medio cuerpo sobre el hero, y la marca del hub desapareceria de la
     pagina — porque esta regla la escondia sin condicion. */
  body:not(:has(.ad-lb[data-ad-status="unfilled"])) .stage-top .brand-chip{ display:none }
  /* El sufijo solo cuando sobra sitio: a 1750px el hueco libre a la izquierda
     del leaderboard son 358px y ahi cabe hasta "Premier League · 3D Seat
     Views" (286). Por debajo, la pildora dice el nombre del hub y ya. */
  .lb-suffix{ display:none }
  body:has(.ad-lb) .lb-chip{
    display:inline-flex;
    position:absolute; top:calc(var(--lb-h) / 2); left:20px;
    transform:translateY(-50%);
    max-width:calc(50vw - var(--lb-w) / 2 - 32px);
    overflow:hidden; text-overflow:ellipsis;
  }
  body:has(.ad-lb[data-ad-status="unfilled"]) .lb-chip{ display:none }
}
@media (min-width:1750px){ .lb-suffix{ display:inline } }

/* 3DDV:YT-LAYOUT:CSS:END */

/* =====================================================================
   3DDV:TOUCH-GATE:CSS — en tactil, el mapa solo se maneja con DOS dedos
   =====================================================================
   El bug (David, 2026-08-07, primera vez que abria una pagina en el movil):
   arrastrar con un dedo sobre el mapa hacia panning del mapa y la pagina no
   scrolleaba. Con el hero ocupando casi toda la pantalla, el lector quedaba
   atrapado en el visor sin manera evidente de bajar. Es el problema clasico
   del mapa a pantalla completa, y la solucion es la de siempre: el gesto de
   un dedo pertenece a la PAGINA.

     un dedo   -> scroll vertical de la pagina (y el toque suelto sigue
                  abriendo la seccion: se bloquea el ARRASTRE, no el TAP)
     dos dedos -> panning y pinch-zoom del mapa, como hasta ahora

   `touch-action: pan-y` es la mitad que no se puede hacer con JS: le dice al
   navegador que el desplazamiento vertical es suyo y lo resuelve sin esperar
   a que el JS decida, asi que un `preventDefault()` de DVM ya no puede
   secuestrarlo. Lo que `touch-action` NO sabe expresar es "segun cuantos
   dedos", y de eso se encarga el bloque 3DDV:TOUCH-GATE:JS.

   SOLO EL MAPA (#viewer). En la panoramica (#view3d) mirar alrededor con un
   dedo es el producto, asi que ahi no se toca nada. */
#viewer { touch-action: pan-y; }
/* 3DDV:TOUCH-GATE:CSS:END */

/* =====================================================================
   COMPARTIR — el boton en .stage-top y su popover de redes
   =====================================================================
   FUERA de las marcas 3DDV a proposito: este bloque es a mano y las de arriba
   (YT-LAYOUT:CSS, TOUCH-GATE:CSS) las reescribe un script. Por el mismo motivo
   la unica regla de aqui que toca un selector ajeno (.yt-expand, mas abajo)
   esta tambien fuera: si alguien regenera ese bloque, esta override sobrevive.

   Sitio: DENTRO del mapa, arriba a la derecha, pegado a #yt-expand. Cada
   control donde se aplica su accion, y este comparte lo que hay en el stage.
   En pano se queda en el mismo sitio: cambia el payload, no la posicion.
   Mismo lenguaje visual que .chip, .yt-expand y .pano-nav-step: son vecinos. */
.share-wrap { position: relative; display: inline-flex; }

.share-btn {
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 8px;
  font: 700 11px/1 var(--font-ui); letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink); background: rgba(13, 18, 48, .82);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  border: 1px solid var(--line); border-radius: 999px; padding: 10px 14px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, .35);
  transition: background .15s, border-color .15s, color .15s;
}
.share-btn:hover { background: var(--accent); border-color: var(--accent); color: #fff; }
.share-btn:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }
.share-btn svg { flex: none; }

/* Margen automatico SOLO donde hace falta, y por eso no lo lleva de serie.
   .stage-top reparte el espacio libre entre TODOS los `margin-left:auto`, asi
   que ponerlo siempre mandaria .brand-chip a la mitad de la barra entre 641 y
   1099px, donde hoy va pegada a la derecha. Quien arrastra al boton:
     >=1100px          -> el propio .share-wrap (auto), ver el bloque de
                          min-width:1100 justo debajo: desde el 28 ago el
                          boton va DELANTE de #yt-expand y hereda su auto
     641-1099, mapa    -> .brand-chip (auto), idem
     641-1099, pano    -> no queda nadie: .chip-link y .brand-chip se ocultan
     <=640px           -> tampoco: ahi se ocultan las dos y #yt-expand */
@media (max-width: 1099px) {
  .stage-top:has(#back-btn:not(.hidden)) .share-wrap { margin-left: auto; }
}

/* ORDEN: compartir a la IZQUIERDA, expand a la DERECHA (David, 28 ago 2026).
   El swap es de DOM (venue.js hace insertBefore de .share-wrap ante #yt-expand),
   pero el que empujaba al grupo contra el borde derecho era el
   `margin-left:auto` de .yt-expand: dejandolo ahi, el auto cae DETRAS del boton
   de compartir y este se quedaria pegado a la pildora del hub, a la izquierda
   de la barra. Asi que el auto se muda al primero del grupo y se le quita al
   segundo. Solo hace falta de 1100 para arriba: por debajo .yt-expand es
   display:none y quien manda es el bloque de max-width:1099 de aqui arriba.
   La override de .yt-expand va, como la de `font-size:0` de mas abajo, FUERA de
   las marcas 3DDV: el bloque YT-LAYOUT:CSS lo reescribe un script. */
@media (min-width: 1100px) {
  .stage-top .share-wrap { margin-left: auto; }
  .stage-top .yt-expand  { margin-left: 0; }
}

/* ---------- Popover (la replica de la hoja nativa, en escritorio) ---------- */
.share-pop {
  position: absolute; top: calc(100% + 8px); right: 0;
  width: 272px; max-width: calc(100vw - 32px);
  z-index: 30;
  background: rgba(9, 13, 36, .96);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border: 1px solid var(--line); border-radius: 14px;
  padding: 12px;
  box-shadow: 0 18px 48px rgba(0, 0, 0, .5);
  text-align: left;
  /* visibility, no [hidden]: sale del orden de tabulacion y del arbol de
     accesibilidad igual, pero deja transicionar la apertura.
     `visibility 0s` con RETARDO solo al cerrar (aqui .15s, en .is-open 0s) y
     no `visibility .15s`, que es lo obvio y esta mal: con la propiedad
     interpolandose, en el instante 0 del cambio el valor computado sigue
     siendo `hidden`, asi que el `focus()` del primer item — que corre en el
     mismo tick que la clase — lo rechazaba y el foco se quedaba en el boton.
     Medido el 28 ago 2026, no deducido. Asi el popover se ve al instante al
     abrir y la desaparicion espera a que acabe el fundido. */
  opacity: 0; visibility: hidden; transform: translateY(-6px);
  transition: opacity .15s, transform .15s, visibility 0s .15s;
}
.share-pop.is-open {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity .15s, transform .15s, visibility 0s 0s;
}

/* .stage-top va a z-index 10 y .pano-nav a 12: sin subir la barra mientras el
   popover esta abierto, las flechas de la pano se pintan por encima de el. Lo
   pone y lo quita el JS; se revierte al cerrar. */
body.share-open .stage-top { z-index: 40; }

.share-pop-h {
  font: 700 10px/1 var(--font-ui); letter-spacing: .12em; text-transform: uppercase;
  color: var(--dim); margin: 2px 2px 10px;
}
.share-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.share-item {
  cursor: pointer; text-align: left;
  display: flex; align-items: center; gap: 9px;
  width: 100%; min-height: 38px;
  font: 600 12px/1.2 var(--font-ui); color: var(--bright);
  background: transparent; border: 1px solid transparent; border-radius: 9px;
  padding: 7px 8px;
  transition: background .12s, border-color .12s;
}
.share-item:hover { background: rgba(255, 255, 255, .07); border-color: var(--line); }
.share-item:focus-visible { outline: 2px solid var(--sky); outline-offset: 1px; }
.share-item-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Copiar enlace: fila entera, separada de la rejilla. Es la salida universal
   (vale para cualquier app que no este arriba), asi que no compite por sitio
   con las redes. */
.share-copy { margin-top: 8px; border-color: var(--line); background: rgba(255, 255, 255, .04); }
.share-copy.is-done { border-color: var(--sky); color: var(--sky); }

/* El icono es un cuadrado con el color de la marca y su inicial. Sin SVG de
   marca por una razon concreta: los paths de los logos son propiedad de cada
   red y dibujarlos "de memoria" da glifos que no son el logo. Color + nombre
   identifican igual de bien y no hay nada que mantener. Los dos que no son
   marca (correo y enlace) si llevan su icono, dibujado aqui. */
.share-ico {
  flex: none; width: 22px; height: 22px; border-radius: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  background: #2A3161; color: #fff;
  font: 800 11px/1 var(--font-ui); letter-spacing: 0;
}
.share-ico svg { width: 14px; height: 14px; }
.share-ico.is-whatsapp { background: #25D366; }
/* X sobre fondo oscuro: su color de marca es negro y a 22px sobre el panel
   del popover (#090D24) no se veia el cuadrado — comprobado en captura. Se
   invierte, que es la version que la propia marca usa sobre oscuro. */
.share-ico.is-x        { background: #F2F4FB; color: #0B0E1C; }
.share-ico.is-facebook { background: #1877F2; }
.share-ico.is-telegram { background: #26A5E4; }
.share-ico.is-reddit   { background: #FF4500; }
.share-ico.is-linkedin { background: #0A66C2; font-size: 9px; }
.share-ico.is-email    { background: #3B4382; }
.share-ico.is-copy     { background: #3B4382; }

@media (prefers-reduced-motion: reduce) {
  .share-pop, .share-btn, .share-item { transition: none; }
}

/* ---------------------------------------------------------------------
   FIX de las TRES colisiones medidas en la fase 0 (28 ago 2026)
   ---------------------------------------------------------------------
   Los numeros son medidos en pantalla, no estimados:

   1. 1100px, PANO. `.pano-nav` esta centrada en el stage (176-533) y hoy
      #yt-expand empieza en 573: 40px de aire. Con el boton, Expand se iba a
      459 y la flecha `>` se pintaba encima de "EXPAND".
   2. 1100px, MAPA, leaderboard unfilled (que es cuando vuelve .brand-chip
      DENTRO del mapa). El chip del tour pasaba de 258 a 159px y "EXPAND" se
      partia en dos lineas al quedarse en 83px.
   3. <=640px, PANO. #stage-title (la pastilla de seccion) vive en top:16px;
      right:12px y el boton le caia ENCIMA: 41px de solape.

   Para 1 y 2, una sola regla: por debajo de 1200px los DOS controles de la
   derecha pasan a icono. Medido despues del cambio: Expand vuelve a 573-612
   (los mismos 40px de aire que antes del boton), el chip del tour recupera sus
   258px enteros y Expand vuelve a una linea. Coste neto del boton: cero.
   1200 y no 1100 porque a 1100 justo (donde .yt-expand todavia se pinta: se
   oculta a partir de max-width:1099px) es donde aprieta. */
@media (max-width: 1199px) {
  .share-btn { padding: 10px 12px; }
  .share-btn .share-label { display: none; }
  /* Override deliberada de un selector del bloque YT-LAYOUT, desde fuera de
     sus marcas: `font-size:0` apaga el texto ("<-> Expand", que llega por
     innerHTML desde venue.js y cambia al pulsar) y el ::before repone la
     flecha. El nodo de texto sigue en el DOM, asi que un lector de pantalla
     sigue oyendo la etiqueta entera. */
  .yt-expand { font-size: 0; padding: 10px 12px; }
  .yt-expand::before { content: "\2194"; font: 700 13px/1 var(--font-ui); }
}

/* Movil: 44px de objetivo tactil, como .chip y .back-btn, y el ancho exacto
   del boton es lo que se le cede a la pastilla de seccion mas abajo. */
@media (max-width: 640px) {
  .share-wrap { margin-left: auto; }
  .share-btn { width: 44px; min-height: 44px; padding: 10px 0; justify-content: center; }
  /* Colision 3. El 64 sale de la cuenta, no de la vista: 12px de padding de
     .stage-top + 44 del boton + 8 de aire. Y el ancho que se le quita a la
     pastilla es el mismo 110 de antes (el hueco del boton "Map" a su
     izquierda) mas esos 64. */
  .stage-hero:has(#back-btn:not(.hidden)) #stage-title {
    right: 64px;
    max-width: calc(100% - 174px);
  }
  .share-pop { width: 262px; }
}

/* =====================================================================
   MOVIL v2 — EL MAPA EN UN TERCIO Y LOS GESTOS SON DEL MAPA (2026-08-28)
   =====================================================================
   David, sobre las landings de hub en el movil: "es toscar". Tres cambios,
   y el tercero es consecuencia aritmetica de los dos primeros.

   1. EL VISOR BAJA A UN TERCIO DE PANTALLA. Hasta hoy el hero medía 100dvh
      menos el peek: el mapa ERA la pagina y para llegar al contenido habia
      que salir de el. Ahora mide clamp(200px, 34dvh, 320px) — mucho mas
      horizontal que alto — y los otros dos tercios son contenido, que es
      donde el pulgar espera poder scrollear.

   2. SE INVIERTE EL GATE TACTIL DEL 7 DE AGOSTO. Aquel decia "un dedo es de
      la pagina, dos dedos son del mapa" y existia por UNA razon: con el mapa
      a pantalla completa, un arrastre que hiciera panning dejaba al lector
      atrapado en el visor sin manera evidente de bajar. Con el mapa en un
      tercio esa razon desaparece — siempre hay pagina fuera del mapa — y el
      gate pasa a ser el problema: dentro del visor el gesto natural no hace
      lo que dice. La regla nueva:

        dentro del mapa  -> 1 o 2 dedos arrastrando = panning
                            pinch de 2 dedos        = zoom
                            toque suelto            = abre la vista 3D
        fuera del mapa   -> scroll de pagina

      `touch-action: none` sobre #viewer le quita al navegador el scroll
      vertical que `pan-y` le reservaba, y el gate de 3DDV:TOUCH-GATE:JS se
      apaga por matchMedia — NO se borra: de 641px para arriba el mapa sigue
      siendo alto y el gate sigue siendo la respuesta correcta.

      EL PRECIO, dicho claro: con el mapa en un tercio las secciones salen
      pequenas y en la mayoria de recintos habra que hacer zoom antes de
      poder tocar una. Aceptado por David ("deberia ser obvio"), y por eso
      los +/- de .zoom-ctrl se quedan donde estan: es la unica afordancia de
      zoom que no hay que adivinar.

   3. EL H1 SALE DEL HERO. No es gusto, es aritmetica. En un iPhone 15
      (390x844) el hero pasa a medir 287px, y el chrome que vive dentro ocupa
      68px arriba (.stage-top con su chip de 44px) y ~141px abajo
      (.match-badge 34 + h1 45 + los 62px de padding que reservaban el boton
      centrado): quedaban 78px de mapa util, o sea una rendija. Sacando el
      titulo a una banda propia justo debajo del visor y retirando el boton
      de la guia quedan ~186px, que es el mapa mas grande que cabe en el
      tercio. El nodo lo mueve venue.js (bloque MOVIL:TITULO:JS) y VUELVE al
      hero por encima de 640px.

   LA INVARIANTE DEL STAGE SIGUE INTACTA: `.stage` es `inset: 0` y el hero es
   simetrico y a sangre. Lo que se cede es ALTO, igual que --peek y igual que
   la banda del leaderboard. Nada de esto es un inset lateral.

   TABLET (641-1099px) NO SE TOCA. Alli el mapa sigue a 100dvh y el gate de
   dos dedos sigue vigente, que es la combinacion coherente. Bajar tambien la
   tablet es una decision aparte y hay que medirla: el alto sobra y el
   problema del atrapamiento es mucho menor.
   ===================================================================== */
@media (max-width: 640px) {
  /* --- 1. El tercio -------------------------------------------------
     Sin restar --anchor-top-h / --anchor-bot-h: con el hero de alto fijo y
     en flujo, la banda de los anchor ads ya la reserva el padding del body,
     y restarla otra vez dejaria el mapa en nada (un anchor de 262px contra
     un hero de 287). Y sin --lb-h por lo mismo: el leaderboard ya no le come
     alto al mapa, que es justo lo que permite subirlo a 320x100.
     El `min-height: 480px` del bloque de arriba se anula aqui a proposito:
     era el suelo que impedia que los anchors dejasen una rendija de mapa, y
     ese calculo ya no aplica. */
  .stage-hero {
    height: clamp(200px, 34dvh, 320px);
    min-height: 0;
  }

  /* --- 2. Los gestos son del mapa ------------------------------------
     Va al final del fichero y con !important porque tiene que ganarle a
     `@media (pointer: coarse) { #viewer, #viewer * { touch-action: pan-y
     !important } }`: misma especificidad y las dos !important, asi que
     decide el orden en el fichero. El #view3d no se toca — alli mirar
     alrededor con un dedo siempre fue el producto. */
  #viewer, #viewer * { touch-action: none !important; }

  /* --- 3. El chrome que ya no cabe -----------------------------------*/
  /* El titulo se va abajo (venue.js). Se oculta AQUI y no despues del
     movimiento para que no haya salto: en el primer pintado ya no esta.
     En cuanto el JS lo saca del hero deja de casar con este selector y
     aparece en su banda. Sin JS el h1 sigue en el DOM (crawlers y lectores
     de pantalla lo ven), solo que no se pinta. */
  .stage-hero .stage-overlay h1,
  .stage-hero .stage-overlay .match-badge { display: none; }

  /* el scrim de 170px existia para dar contraste al h1 sobre zonas claras
     del mapa; sin h1 delante solo oscurece un tercio del visor */
  .stage-overlay::before { display: none; }

  /* y sin el boton centrado abajo, el padding que le reservaba sitio */
  .stage-overlay { padding: 0 12px 12px; }

  /* El boton "Stadium guide" fuera. Era el UNICO aviso de que habia
     contenido debajo cuando el mapa medía 100dvh — ese era literalmente su
     comentario en el bloque de .scroll-hint. Ahora la guia se ve sola desde
     el primer viewport, y en un hero de 287px ese boton costaba 62px de
     mapa por decir algo que ya se ve. */
  .scroll-hint { display: none; }

  /* La linea de estado ("Click a section…" / "Section 408"), en pastilla
     como en la pano: sin scrim detras, el texto plano no se lee sobre las
     zonas claras del mapa. Los 76px que se le quitan al ancho son la
     columna de .zoom-ctrl (44 + 12 de margen + aire). */
  #stage-title {
    display: inline-block;
    margin: 0;
    max-width: calc(100% - 76px);
    font-size: 0.72rem;
    background: rgba(13, 18, 48, 0.82);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 6px 12px;
    text-shadow: none;
  }
}

/* --- 3b. El titulo, ya fuera del hero --------------------------------
   Lo inserta venue.js justo detras de .stage-hero, o sea ANTES del rail de
   "More in 3D": el h1 pertenece al mapa que tiene encima, no al carrusel de
   recintos relacionados.

   Fuera de movil el elemento se queda VACIO y `display: none` lo saca del
   flujo. Esto no es cosmetico: en escritorio .yt-shell es un grid de dos
   columnas exactas, y un tercer hijo con caja se llevaria una fila entera.
   Un elemento en display:none no es item del grid, asi que no la ocupa. */
.venue-title-m { display: none; }
@media (max-width: 640px) {
  .venue-title-m { display: block; padding: 14px 16px 4px; }
  .venue-title-m:empty { display: none; }   /* por si el JS no llego a moverlo */
  .venue-title-m .match-badge { margin-bottom: 6px; }
  .venue-title-m h1 {
    /* mas grande que en el hero: aqui no compite con el mapa por el sitio */
    font-size: clamp(1.5rem, 6.6vw, 2rem);
    filter: none;                 /* ya no hay mapa detras contra el que destacar */
  }
}

/* =====================================================================
   MOVIL v2.1 — PASADA DE DISENO SOBRE EL TERCIO (2026-08-28)
   =====================================================================
   David, viendo el resultado del bloque anterior: "errores de bulto en
   diseno". Tres cosas, y todas van del mismo problema: en un hero de 287px
   cada pieza de chrome que se pinta sobre el mapa cuesta un porcentaje del
   producto, y el rail de debajo se estaba quedando con el espacio que
   deberia ver la guia.
   ===================================================================== */
@media (max-width: 640px) {

  /* --- 4. La pastilla de vuelta, ESTRECHA ----------------------------
     "← All NFL stadiums" ocupaba 190px de los 390 de ancho: la mitad de la
     fila de arriba, y esa fila se pinta ENCIMA del mapa. La etiqueta corta
     ("← NFL") la hornea venue.js con el nombre del hub, que saca de la
     .brand-chip (bloque 3DDV:MOBILE-V2:CHIP). Mismo patron que
     "Back to map" -> "Map": el texto largo se queda en el DOM como fallback
     sin JS y el aria-label nunca cambia, asi que un lector de pantalla sigue
     oyendo la frase entera.

     La ALTURA no baja de 44px: es el objetivo tactil minimo y David pidio
     estrecha, no baja. Lo que se recorta es el padding lateral. */
  .hub-long { display: none; }
  .hub-short { display: inline; }
  .stage-top .chip-link { padding: 10px 13px; }

  /* --- 5. La instruccion es ONBOARDING, no chrome --------------------
     "Click a section to see the view from your seat" vivia permanentemente
     sobre la esquina inferior izquierda del mapa. Pero es una instruccion:
     en cuanto el lector toca el mapa ya ha cumplido, y a partir de ahi solo
     tapa. Se encoge, y se va sola en el primer contacto con el visor (clase
     .map-touched, la pone venue.js).

     El :not(:has(…)) NO es decorativo: en la panoramica este mismo elemento
     es la pastilla de orientacion ("View from Section 408"), y la regla de
     la pano solo fija posicion — no opacidad — asi que sin acotar aqui el
     estado de mapa, abrir una vista dejaria la pastilla invisible. */
  #stage-title {
    font-size: 0.66rem;
    padding: 5px 11px;
    transition: opacity 0.3s, visibility 0.3s;
  }
  #stage-title::before { width: 6px; height: 6px; margin-right: 6px; }
  .stage-hero.map-touched:not(:has(#back-btn:not(.hidden))) #stage-title {
    opacity: 0;
    visibility: hidden;
  }

  /* --- 6. El rail "More in 3D", en pastilla de LISTA -----------------
     Medido antes: 328px de los 844 de un iPhone 15, o sea que debajo del
     mapa lo primero y casi lo unico que habia era el carrusel de recintos
     relacionados, y la guia arrancaba en el pixel 781. Un modulo de
     navegacion lateral no puede pesar mas que el contenido de la pagina.

     La tarjeta pasa de VERTICAL (miniatura 224x126 + nombre + meta) a
     HORIZONTAL de lista (miniatura de 72px al lado del nombre). Se cae la
     linea .yt-meta y el numero de seccion sobreimpreso: a este tamano no se
     leen y no son lo que decide el clic — el clic lo decide el nombre del
     recinto. El fondo tenue le devuelve el borde de tarjeta que la
     miniatura grande daba sola.

     Especificidad, para que no dependa de nada: estas reglas viven DESPUES
     del bloque 3DDV:YT-LAYOUT:CSS en el fichero y tienen su misma
     especificidad, asi que ganan por orden. Ninguna toca el desktop. */
  .yt-rail { padding: 12px 0 4px; }
  .yt-rail-top { padding: 0 16px 8px; }
  .yt-rail-h { font-size: 0.8rem; }

  /* 3DDV:MOBILE-V2:RAIL (2026-09-02) — al bajar el rail dentro de #guide, su
     <h2> "More in 3D" pasa a casar con `.seo-copy h2` (0,1,1), que le gana a
     `.yt-rail-h` (0,1,0): salia con la tipografia de titulo de seccion, en
     color de texto, con 14px de margen... y con el KICKER del ::before, que
     imprime `--kicker-text` ("NFL · 3D Seat Views") encima del rail. Eso es
     CONTENIDO fuera de sitio, no un detalle estetico. Medido: la tira pasaba
     de 128px a 167px. Aqui se le devuelve lo suyo con la especificidad justa,
     sin tocar `.seo-copy h2`, que sigue diciendo lo que quiere decir. */
  .seo-copy .yt-rail-h {
    font-weight: 800;
    font-size: 0.8rem;
    letter-spacing: 0.05em;
    line-height: normal;
    color: var(--bright);
    margin-bottom: 0;
  }
  .seo-copy .yt-rail-h::before { content: none; }
  #yt-pills-root { padding: 0 16px 10px; gap: 10px; }
  .yt-pill { flex: 0 0 208px; }
  .yt-pill > a {
    grid-template-columns: 72px 1fr;
    gap: 9px;
    padding: 6px;
    align-items: center;
    background: rgba(255, 255, 255, 0.04);
    border-radius: 10px;
  }
  .yt-thumb { border-radius: 7px; }
  .yt-sec { display: none; }
  .yt-txt { padding-top: 0; }
  .yt-name { font: 700 0.72rem/1.25 var(--font-ui); }
  .yt-meta { display: none; }
  .yt-rail .related-all { padding: 0 16px; font-size: 0.72rem; margin: 0; }
}

/* ---------------------------------------------------------------------
   MOVIL v2.1 (cont.) — dos cosas que solo se ven en la captura
   --------------------------------------------------------------------- */
@media (max-width: 640px) {
  /* La PRIMERA pastilla del carrusel salia CORTADA por la izquierda. No es
     el padding: es `scroll-snap-type: x mandatory`, que alinea el item con
     el borde del scroll-port y se come los 16px de padding del contenedor al
     hacer el snap inicial. `scroll-padding` es la propiedad que le dice al
     snap donde esta de verdad el borde util. Venia de antes del rediseno,
     pero con la tarjeta pequena canta mucho mas. */
  #yt-pills-root { scroll-padding-left: 16px; scroll-padding-right: 16px; }

  /* La miga de pan se comia 71px en dos renglones justo donde ahora empieza
     a verse la guia. Misma informacion, la mitad de alto. */
  .breadcrumbs { margin: 0 0 12px; font-size: 0.72rem; line-height: 1.3; }
  .breadcrumbs ol { gap: 2px 6px; }
  .seo-copy { padding-top: 18px; }
}

/* 3DDV:QUIZ-ENTRY:START (generado por _tools/apply_quiz_entry.py -- no editar a mano) ------------------------------
   La puerta de entrada al quiz, al final de la lectura de una pagina de
   recinto. Toma el acento del recinto (--acc), asi que se lee como parte de la
   pagina y no como un banner. Sin imagen: cero peticiones extra en las 48
   paginas que lo llevan hoy (32 de la NFL + 16 de la Premier). Esta hoja la
   cargan TODOS los hubs, asi que el bloque se escribe una sola vez.
   El modulo vive en la columna de contenido; el rail no se toca. */
.quiz-entry {
  margin: 34px 0 8px;
  padding: 22px 24px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--acc);
  border-radius: var(--radius);
  background: var(--panel);
}
.qe-kicker {
  font-family: var(--font-display);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--acc);
  margin: 0 0 6px;
}
.quiz-entry h2 {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  line-height: 1.1;
  font-size: clamp(1.25rem, 2.4vw, 1.6rem);
  color: var(--ink);
  margin: 0 0 8px;
}
.qe-sub {
  color: var(--muted);
  font-size: 0.9rem;
  margin: 0 0 16px;
  max-width: 56ch;
}
.qe-cta {
  display: inline-block;
  min-height: 44px;
  line-height: 24px;
  padding: 10px 18px;
  border-radius: 12px;
  background: var(--acc);
  color: #0D1230;
  font-weight: 700;
  font-size: 0.9rem;
  text-decoration: none;
  transition: transform 0.15s, filter 0.15s;
}
.qe-cta:hover { filter: brightness(1.08); transform: translateY(-1px); }
@media (max-width: 640px) {
  .quiz-entry { margin: 26px 0 4px; padding: 18px 16px; }
  .qe-sub { font-size: 0.86rem; }
}
@media (prefers-reduced-motion: reduce) {
  .qe-cta { transition: none; }
  .qe-cta:hover { transform: none; }
}
/* 3DDV:QUIZ-ENTRY:END */

/* =====================================================================
   3DDV:TIX — el CTA de entradas (TicketNetwork / Impact). Fase 1: /mlb
   =====================================================================
   Copiado de _tools/_mockup-affiliate-cta.html, donde se midio contra la
   pagina real con AdSense sirviendo. NO se rediseña aqui: si hay que cambiar
   una medida, se cambia en el mockup, se vuelve a medir y se copia.

   Los cuatro emplazamientos y lo que costo cada uno (medido a 1440, DESIGN 0):
     A  .tix-pano   pildora dentro del stage, centro-abajo   0 px de documento
     B  .tix-row    boton por fila del calendario            0 px de alto de fila
     C  .tix-bar    barra de proximo partido, antes de .seo-copy   +132 px
     D  .tix-hint   boton sobre el mapa, junto al de la guia  0 px de documento

   EL VERDE ES SOLO PARA ENTRADAS. --accent (#0089F7) ya es el boton primario y
   --sky todo enlace interno: si "comprar entradas" usa uno de los dos, deja de
   distinguirse de "ir a otra pagina nuestra".

   Y el foco NO puede ser el global: el anillo --sky sobre el verde da un
   contraste de 1,34 (invisible, calculado en DESIGN 6). Por eso estos botones
   llevan su propio anillo oscuro (6,57) mas un halo verde, que ademas se ve
   sobre la foto de la panoramica.

   En movil A y C son LA MISMA BARRA, no dos: con las dos salian dos botones
   verdes a 100 px uno de otro. `body.tix-pano-on` la conmuta. D no existe.
   ===================================================================== */
:root{
  --tix:      #10B981;   /* verde accion: SOLO entradas, en todo el sitio */
  --tix-hi:   #34D399;   /* hover, y texto de la variante contorno */
  --tix-ink:  #04231A;   /* texto sobre el verde (6,57 : AA) */
  --tix-ring: #04231A;   /* anillo de foco propio */
}

/* --- A. la panoramica: el numero 1 ---------------------------------- */
.tix-pano{
  position: absolute; z-index: 13;
  left: 50%; transform: translateX(-50%); bottom: 18px;
  display: inline-flex; align-items: center; gap: 10px;
  max-width: min(460px, calc(100% - 220px));
  background: var(--tix); color: var(--tix-ink);
  font-family: var(--font-ui); font-weight: 800; font-size: .78rem;
  letter-spacing: .04em; padding: 13px 22px; border-radius: 999px;
  text-decoration: none; box-shadow: 0 6px 22px rgba(0, 0, 0, .45);
  transition: background .15s, transform .15s;
}
.tix-pano:hover{ background: var(--tix-hi); transform: translate(-50%, -1px); }
.tix-pano:focus-visible{
  outline: 2px solid var(--tix-ring); outline-offset: 3px;
  box-shadow: 0 0 0 5px rgba(52, 211, 153, .5);
}
.tix-pano .tix-sub{ font-weight: 600; opacity: .78; font-size: .72rem; }
/* Desaparece con la panoramica, mismo patron que .scroll-hint. */
.stage-hero:not(:has(#back-btn:not(.hidden))) .tix-pano{ display: none; }

/* --- B. una accion por partido, en el calendario -------------------- */
/* La tercera columna de .fixture (`auto`) esta vacia: el boton entra ahi y la
   fila sigue midiendo 52 px. Contorno y no solido: 15 pildoras verdes rellenas
   serian una valla publicitaria. */
.tix-row{
  justify-self: end;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 34px; padding: 0 14px; border-radius: 999px;
  background: transparent; border: 1px solid var(--tix); color: var(--tix-hi);
  font-family: var(--font-ui);
  font-weight: 700; font-size: .72rem; letter-spacing: .04em;
  text-transform: uppercase; text-decoration: none; white-space: nowrap;
  transition: background .15s, color .15s;
}
.tix-row:hover{ background: var(--tix); color: var(--tix-ink); }
.tix-row:focus-visible{ outline: 2px solid var(--tix-hi); outline-offset: 2px; }

/* --- C. barra de proximo partido, primer bloque del contenido ------- */
.tix-bar{
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin: 0 0 22px; padding: 16px 18px;
  background: linear-gradient(180deg, rgba(16, 185, 129, .10), rgba(16, 185, 129, .04));
  border: 1px solid rgba(16, 185, 129, .38); border-radius: var(--radius);
}
.tix-bar .tix-txt{ flex: 1 1 260px; min-width: 0; }
.tix-bar .tix-h{ font-weight: 700; color: var(--ink); font-size: .95rem; }
.tix-bar .tix-m{ color: var(--muted); font-size: .82rem; }
.tix-bar .tix-go{
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--tix); color: var(--tix-ink);
  font-family: var(--font-ui); font-weight: 800; font-size: .78rem;
  letter-spacing: .04em; padding: 12px 20px; border-radius: 999px;
  text-decoration: none; transition: background .15s;
}
.tix-bar .tix-go:hover{ background: var(--tix-hi); }
.tix-bar .tix-go:focus-visible{
  outline: 2px solid var(--tix-ring); outline-offset: 3px;
  box-shadow: 0 0 0 5px rgba(52, 211, 153, .5);
}

/* --- D. el boton sobre el MAPA, junto al de la guia del recinto ----- */
/* Se lee como HERMANO de `.scroll-hint`, no como un boton nuevo: mismo tamano
   de letra, mismo tracking, mismas mayusculas, mismo radio y la misma sombra de
   18 px que existe para leerse sobre zonas claras del plano. Lo unico que
   cambia es el color, porque el verde es SOLO para entradas.
   `right` y `bottom` los pone venue.js contra la caja REAL de la guia: su ancho
   cambia con el sustantivo del recinto ("Ballpark/Arena/Stadium guide") y su
   `right` ya lleva `var(--rail-w)` para esquivar el rail de anuncios.
   Desaparece con la panoramica, igual que la guia y por la misma regla. */
.tix-hint{
  position: absolute; z-index: 12;
  right: 18px; bottom: 18px;              /* lo recalcula venue.js */
  display: inline-flex; align-items: center;
  background: var(--tix); color: var(--tix-ink);
  font-family: var(--font-ui); font-size: .72rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
  padding: 12px 20px; border-radius: 999px; text-decoration: none;
  box-shadow: 0 4px 18px rgba(0, 0, 0, .35);
  transition: background .15s, transform .15s;
}
.tix-hint:hover{ background: var(--tix-hi); transform: translateY(-1px); }
.tix-hint:focus-visible{
  outline: 2px solid var(--tix-ring); outline-offset: 3px;
  box-shadow: 0 0 0 5px rgba(52, 211, 153, .5);
}
.stage-hero:has(#back-btn:not(.hidden)) .tix-hint{ display: none; }

/* --- la disclosure del footer, solo donde hay boton ----------------- */
.tix-disc{ color: var(--dim); font-size: .72rem; margin-top: 10px; }

/* --- movil ---------------------------------------------------------- */
/* A no va ENCIMA de la pano: la pano solo mide 390x286 y un overlay de 47 px se
   comeria el 16 % de la imagen en el 67 % del trafico. Va en flujo, pegado
   debajo del stage, donde hay 458 px visibles sin scrollear.
   En movil A es HERMANO de .stage-hero, no hijo, asi que el selector
   descendente de arriba no lo alcanza (DESIGN 7). Lo conmuta la clase del
   <body> que pone el modulo 3DDV:TIX. */
@media (max-width: 640px){
  .tix-pano{
    position: static; transform: none; left: auto; bottom: auto;
    display: none; justify-content: center; width: calc(100% - 32px);
    max-width: none; margin: 12px 16px 0; border-radius: 14px;
    min-height: 44px; box-sizing: border-box;
  }
  body.tix-pano-on .tix-pano{ display: flex; }
  .tix-pano:hover{ transform: none; }
  /* Una sola barra de entradas: con la pano abierta, C se retira. */
  body.tix-pano-on .tix-bar{ display: none; }
  .tix-bar .tix-go{ min-height: 44px; box-sizing: border-box; }
}

/* El calendario colapsa a UNA columna en 620 px, no en 640 (la regla de
   `.fixture` de mas arriba). El boton tiene que seguir ESE breakpoint y no el
   nuestro, o entre 621 y 640 le aplicariamos una rejilla de dos columnas a una
   fila que todavia tiene tres, y se come la columna de la fecha.

   Y va a la DERECHA, ocupando las DOS lineas, no dentro de una de ellas. Es la
   correccion medida el 3 sep: con `.fx-teams` a todo el ancho (lo que hacia el
   mockup) el boton cae en una TERCERA linea y la fila pasa de 73 a 121 px.
   Aun asi la fila crece, porque la columna de los equipos se estrecha: por eso
   B va apagado en movil (TIX_ROW_MOBILE). Estas reglas quedan escritas para el
   dia que se encienda. */
@media (max-width: 620px){
  .fixture:has(.tix-row){ grid-template-columns: 1fr auto; }
  .fixture:has(.tix-row) .fx-date{ grid-column: 1; grid-row: 1; }
  .fixture:has(.tix-row) .fx-teams{ grid-column: 1; grid-row: 2; }
  .fixture:has(.tix-row) .tix-row{
    grid-column: 2; grid-row: 1 / span 2; align-self: center; min-height: 44px;
  }
}

@media (prefers-reduced-motion: reduce){
  .tix-pano, .tix-row, .tix-bar .tix-go, .tix-hint{ transition: none; }
  .tix-pano:hover{ transform: translateX(-50%); }
  .tix-hint:hover{ transform: none; }
}
/* 3DDV:TIX:CSS:END */
