:root {
  /* CyL purple sourced from a flag-color reference (flagcolorcodes.com),
     not an official Junta style guide - used only as a UI accent (topbar,
     headings), never for the linked/missing marker semantics below. */
  --cyl-purple: #742c64;
  --cyl-purple-dark: #4f1e44;

  /* Not Bootstrap's default danger-red/success-green - "missing" isn't an
     error, it's an invitation to help, and the alarm-red read wrong before
     it even read dated. Teal/terracotta carries the same signal more
     honestly and sits better next to photography. */
  --color-linked: #1c8a6d;
  --color-missing: #c2703d;

  --bg: #faf8f6;
  --ink: #2b2521;
  --ink-soft: #6b625c;
  --card: #ffffff;
  --border: #e8e2db;

  --font-display: 'Fraunces', Georgia, serif;
  --font-body: 'Inter', system-ui, sans-serif;
}

* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { display: flex; flex-direction: column; font-family: var(--font-body); background: var(--bg); color: var(--ink); }

#topbar {
  flex: 0 0 auto;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  background: var(--cyl-purple);
  color: white;
  box-shadow: 0 1px 8px rgba(0,0,0,0.25);
  z-index: 600;
}
#topbar .brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 20px;
  letter-spacing: 0.2px;
  color: inherit; /* now a real <a> (see index.html) - undo the default link blue/underline, it's still just header text */
  text-decoration: none;
  opacity: 1;
  transition: opacity 0.15s ease;
}
#topbar .brand:hover { opacity: 0.85; }
#logo-mark { height: 42px; width: auto; display: block; }

/* --- Loading screen (first paint - see index.html comment) ------------ */
#loading-screen {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  background: var(--cyl-purple);
  color: white;
  transition: opacity 0.4s ease;
}
#loading-screen.hidden { opacity: 0; pointer-events: none; }
#loading-logo { height: 64px; width: auto; }
#loading-spinner {
  width: 30px;
  height: 30px;
  border: 3px solid rgba(255,255,255,0.3);
  border-top-color: white;
  border-radius: 50%;
  animation: loading-spin 0.8s linear infinite;
}
@keyframes loading-spin { to { transform: rotate(360deg); } }
#loading-screen p { font-size: 14px; color: rgba(255,255,255,0.85); margin: 0; }

/* --- Welcome modal (shown once, first visit - see app.js) ------------- */
#welcome-modal {
  position: fixed;
  inset: 0;
  z-index: 2900;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(43, 37, 33, 0.55);
  padding: 20px;
}
#welcome-modal.open { display: flex; }
#welcome-card {
  background: var(--card);
  border-radius: 16px;
  max-width: 380px;
  padding: 28px 26px;
  text-align: center;
  box-shadow: 0 12px 40px rgba(0,0,0,0.3);
}
/* The logo's linework is white/light-gold, drawn for the purple topbar -
   invisible on this card's white background without a matching backing. */
#welcome-logo-badge {
  width: 68px;
  height: 68px;
  border-radius: 50%;
  background: var(--cyl-purple);
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 14px;
}
#welcome-logo { height: 40px; width: auto; }
#welcome-card h2 { font-family: var(--font-display); font-size: 20px; font-weight: 600; margin: 0 0 10px; color: var(--cyl-purple-dark); }
#welcome-card p { font-size: 14px; line-height: 1.5; color: var(--ink-soft); margin: 0 0 20px; }
#welcome-close-btn {
  background: var(--cyl-purple);
  color: white;
  border: none;
  padding: 10px 22px;
  border-radius: 22px;
  font-size: 14px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}
#welcome-close-btn:hover { background: var(--cyl-purple-dark); }
#welcome-about-link { display: block; margin-top: 14px; font-size: 12.5px; color: var(--ink-soft); }

#topbar button {
  background: rgba(255,255,255,0.14);
  border: none;
  color: white;
  padding: 8px 14px;
  border-radius: 18px;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s ease;
}
#topbar button:hover { background: rgba(255,255,255,0.26); }

/* Always visible now, not just on mobile - Estadísticas/Acerca de/Cómo
   contribuir/language all live behind this + showMenuPanel() on every
   screen size, so the header stays down to logo + search everywhere. */
#menu-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  flex-shrink: 0;
  font-size: 17px;
}

#search-wrap { position: relative; flex: 1 1 auto; max-width: 420px; margin: 0 20px; }
#search-input {
  width: 100%;
  padding: 8px 14px;
  border-radius: 18px;
  border: none;
  background: rgba(255,255,255,0.16);
  color: white;
  font-family: var(--font-body);
  font-size: 13.5px;
  outline: none;
  transition: background 0.15s ease;
}
#search-input::placeholder { color: rgba(255,255,255,0.7); }
#search-input:focus { background: rgba(255,255,255,0.28); }

/* Both hidden on desktop by default - #search-wrap is always-expanded
   there, so neither the collapsed-icon trigger nor the collapse-back
   button is relevant. Mobile CSS below turns both on. */
#search-toggle-btn, #search-close-btn { display: none; }

#search-results {
  display: none;
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--card);
  border-radius: 12px;
  padding: 6px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.25);
  z-index: 700;
}
#search-results.open { display: block; animation: search-results-in 0.14s ease; }
@keyframes search-results-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}

.search-section-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 10px 10px 4px;
}
/* A rounded "card" per row, not an edge-to-edge banner - #search-results'
   own 6px padding is what leaves room for this to read as a floating
   highlight rather than butting against the dropdown's own corners. */
.search-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 8px;
  border-radius: 9px;
  cursor: pointer;
  color: var(--ink);
  font-size: 13.5px;
}
.search-row:hover, .search-row.highlighted { background: var(--bg); }

/* Thumbnail and emoji-fallback tile share this wrapper so both sit in the
   exact same slot regardless of which one a given row uses - mixed
   results (some monuments with a photo, some without, plus municipalities/
   provinces that never have one) still line up into one clean grid. */
.search-row-media { position: relative; flex-shrink: 0; }
.search-row-thumb,
.search-row-icon {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.search-row-thumb { object-fit: cover; }
.search-row-icon { font-size: 18px; }
/* Tinted, not solid - a full-saturation background behind an emoji reads
   as a warning badge, not a neutral "no photo yet" placeholder. Same
   green/terracotta as the map's own marker colors and .list-row-icon,
   just at low opacity as a tile background instead of a small solid dot -
   consistent color language, calmer at this larger size. */
.search-row-icon.linked { background: rgba(28,138,109,0.14); }
.search-row-icon.missing { background: rgba(194,112,61,0.14); }
.search-row-icon.neutral { background: rgba(116,44,100,0.1); }
/* Small corner status dot on the media tile, monuments only (statusClass
   is null for province/municipality rows - see searchRowHtml()) - same
   colors as everywhere else this signal shows (map markers, list rows),
   just echoed at a glance for anyone who already knows that color
   language. A white ring (border, var(--card)) keeps it legible over a
   busy photo, not just a flat colored circle merging into the thumbnail. */
.search-row-dot {
  position: absolute;
  bottom: -2px;
  right: -2px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 2px solid var(--card);
}
.search-row-dot.linked { background: var(--color-linked); }
.search-row-dot.missing { background: var(--color-missing); }

.search-row-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.search-row-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.search-row-sub { color: var(--ink-soft); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-empty { padding: 14px; color: var(--ink-soft); font-size: 13px; }

@media (max-width: 768px) {
  /* Single compact row by default: logo, search icon, menu icon - giving
     search a permanent full-width second row (an earlier attempt) made
     the header taller than one row, eating map space for no benefit most
     of the time search isn't even in use. Instead #search-wrap stays
     collapsed until #search-toggle-btn is tapped (app.js toggles
     .search-active on #topbar), at which point it's the only thing
     visible in the row - full width for that one moment, not always. */
  #topbar { height: 52px; padding: 0 12px; }
  #logo-mark { height: 32px; }
  #topbar .brand { font-size: 17px; }
  #menu-btn { width: 36px; height: 36px; font-size: 15px; }
  #search-toggle-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    padding: 0;
  }

  #search-wrap { display: none; }
  #topbar.search-active .brand,
  #topbar.search-active #menu-btn,
  #topbar.search-active #search-toggle-btn { display: none; }
  #topbar.search-active #search-wrap {
    display: block;
    flex: 1 1 auto;
    max-width: none;
    margin: 0;
  }
  #search-close-btn {
    display: block;
    position: absolute;
    right: 4px;
    top: 50%;
    transform: translateY(-50%);
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: none;
    background: rgba(255,255,255,0.25);
    color: white;
    font-size: 13px;
  }
  #search-input { padding-right: 38px; } /* room for #search-close-btn */
}

#app { display: flex; flex: 1 1 auto; min-height: 0; width: 100%; }

/* position: relative so #shuffle-btn/#locate-btn (position: absolute)
   anchor to the map's own visible area specifically - NOT #app, which
   spans the map AND the panel side-by-side, so anchoring there would
   measure from the panel's edge once it's open instead of the map's. */
#map-wrap { flex: 1 1 auto; height: 100%; min-width: 0; position: relative; }
#map { width: 100%; height: 100%; background: var(--bg); }

/* Detail panel: a real sibling of the map, not a floating overlay on top
   of it - the map shrinks to make room (desktop), or stacks below it as a
   bottom sheet (mobile). Either way it's part of the page layout. */
#panel {
  width: 0;
  height: 100%;
  overflow: hidden;
  transition: width 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
  background: var(--card);
  box-shadow: -4px 0 24px rgba(0,0,0,0.12);
  flex-shrink: 0;
  position: relative;
}
#panel.open { width: 420px; }

#panel-content {
  width: 420px;
  height: 100%;
  overflow-y: auto;
  animation: fade-in 0.35s ease;
}
@keyframes fade-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

#panel-handle { display: none; }

#panel-close {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 10;
  border: none;
  background: rgba(255,255,255,0.85);
  backdrop-filter: blur(6px);
  border-radius: 50%;
  width: 32px;
  height: 32px;
  font-family: inherit;
  font-size: 15px;
  cursor: pointer;
  box-shadow: 0 1px 6px rgba(0,0,0,0.25);
}
#panel-close:hover { background: white; }

/* "Written pages" - About/Contribute/Stats/Privacy/Imprint - replace the
   map entirely instead of sharing the screen with it, on both desktop and
   mobile. Same #panel element as the regular map-side panel/bottom sheet,
   just stretched to fill #app completely; openPanel()/closePanel() in
   app.js add/remove .page-mode on #app based on whether
   currentPanelState.type is one of PAGE_PANEL_TYPES. Specificity (an #app
   ancestor plus its own class, on top of #panel/#panel-content's own ID)
   is what lets this win over the plain #panel.open{width:420px} above and
   every wider-breakpoint variant of it further down, without needing
   !important anywhere. */
#app.page-mode #map-wrap { display: none; }
#app.page-mode #panel.open { width: 100%; border-radius: 0; box-shadow: none; }
#app.page-mode #panel-content { width: 100%; }
#app.page-mode #panel-handle { display: none; }
/* Full viewport width still reads badly as a single text column - cap it
   like a normal article/docs page and center it, independent of #panel's
   own (now 100%) width. .hero gets the same cap so its icon/title sits
   above the text column it belongs to instead of spanning the full page
   width above a much narrower column - it's a sibling of .panel-body, not
   a descendant, so needs its own copy of the rule. */
#app.page-mode .panel-body,
#app.page-mode .hero { max-width: 720px; margin: 0 auto; }

@media (max-width: 768px) {
  /* Mobile's #panel.open is normally a 68vh bottom sheet (see below) -
     page-mode wants the full height instead, same as desktop already gets
     for free (#panel is 100% tall at rest; only its *width* toggles there). */
  #app.page-mode #panel.open { height: 100%; }
}

@media (max-width: 768px) {
  #app { flex-direction: column; }
  #map-wrap { flex: 1 1 auto; min-height: 0; width: 100%; }
  #panel {
    width: 100% !important;
    height: 0;
    transition: height 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
    border-radius: 18px 18px 0 0;
  }
  #panel.open { height: 68vh; }
  #panel-content { width: 100%; }
  #panel-handle {
    display: block;
    width: 40px;
    height: 4px;
    border-radius: 2px;
    background: var(--border);
    margin: 10px auto 0;
  }
}

/* Marker icons: emoji-in-a-circle, colored by linkage status */
.monument-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid white;
  box-shadow: 0 2px 6px rgba(0,0,0,0.35);
  font-size: 14px;
  transition: transform 0.12s ease;
}
.monument-icon:hover { transform: scale(1.15); }
/* Color alone isn't enough - ~8% of men are red-green colorblind (moot
   here since we dropped red/green, but the principle still applies to any
   two hues). A solid vs. dashed ring gives a second, non-color channel. */
.monument-icon.linked { background: var(--color-linked); border-style: solid; }
.monument-icon.missing { background: var(--color-missing); border-style: dashed; }

/* Cluster bubbles: color communicates the linked/missing ratio of the
   monuments inside, computed per-cluster in app.js - so the map still
   tells the "coverage gap" story even zoomed out. */
.cluster-icon div {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 3px solid white;
  box-shadow: 0 2px 10px rgba(0,0,0,0.35);
  color: white;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 13px;
}

/* --- Hero: the photo (or gradient fallback) at the top of every panel --- */

.hero {
  position: relative;
  height: 260px;
  overflow: hidden;
  background: linear-gradient(135deg, var(--cyl-purple) 0%, var(--cyl-purple-dark) 100%);
}
.hero-placeholder-icon {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 110px;
  line-height: 1;
  opacity: 0.28;
  /* translateY(-15%) - not padding-bottom, which this used to be - nudges
     the icon above dead-center, out of the scrim/title's way at the
     bottom. padding-bottom looked identical everywhere this shipped
     narrow (sidebar panel, or mobile's full-width-but-short bottom sheet)
     but is a CSS trap: percentage padding resolves against the
     *containing block's width*, even on the vertical axis - once a
     written page (About/Contribute/...) went full-width in page mode, 15%
     of a 1400px+ hero was a couple hundred px, shoving the icon mostly
     above the (overflow:hidden) top edge. transform percentages resolve
     against the element's own box instead, so this reads the same at any
     width - combined with the existing rightward nudge (most emoji glyphs
     render with extra right-side bearing inside their own character cell,
     so true flex-centering still looks slightly left-of-center), in one
     transform since an element only gets the one. */
  transform: translate(0.06em, -15%);
  pointer-events: none;
}
.hero-fallback::before {
  /* subtle texture so a flat gradient doesn't feel like an empty state */
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 25% 20%, rgba(255,255,255,0.12), transparent 55%);
}
/* Shorter than a real photo hero: pages that only ever show this icon
   placeholder (About/Contribute/Stats/menu, or a monument with no photo
   anywhere) don't need as much vertical space as an actual Commons photo,
   which stays at its own full size (.hero's own height/aspect-ratio rules,
   untouched) - this only overrides the fallback case, same specificity as
   .hero so it needs to come after it in source order to win. */
.hero-fallback { height: 180px; }
.hero-fallback .hero-placeholder-icon { font-size: 78px; }
.hero a { display: block; height: 100%; position: relative; }
/* goToIndex() now hides #main-image-link (the <a> above) while a video is
   showing instead - same reasoning as .hero-video[hidden] just below: the
   display:block right above would otherwise out-cascade the hidden
   attribute's own (lower-priority, browser-default) display:none. */
.hero a[hidden] { display: none; }
.hero-img {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.4s ease;
}
.hero a:hover .hero-img { transform: scale(1.04); }

/* Portrait photos (towers, standalone statues, ...) lose most of their
   subject when force-cropped into this wide box - cover just cuts off the
   top and bottom. For those, object-fit switches to contain (the JS in
   applyHeroOrientation() decides which - see there) so the whole photo is
   visible, and a blurred, brightness-dimmed copy of the same photo fills
   the empty sides behind it rather than leaving bare letterbox bars. Not
   used for landscape photos, which already fill the box well as-is. */
.hero-img-bg {
  display: none;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(28px) brightness(0.55) saturate(1.15);
  transform: scale(1.15); /* the blur softens past its own edges - scale up so that never shows as a thin unblurred rim */
}
.hero-portrait .hero-img { object-fit: contain; background: transparent; }
.hero-portrait .hero-img-bg { display: block; }

/* A video in the gallery (see VIDEO_EXTENSIONS/isVideoFile() in app.js) -
   positioned exactly like .hero-img-bg (absolute, filling .hero) since
   it's a sibling of #main-image-link now, not nested inside it (see the
   comment on #main-video in heroBlock()). display: block is needed since
   goToIndex() toggles this element's hidden attribute to show/hide it, and
   the [hidden] rule right below has to come *after* this one - author
   rules of equal specificity are decided by source order, and the
   hidden="" attribute's own display:none only comes from the browser's
   default (lowest-priority) stylesheet, so any later same-specificity
   author rule would otherwise silently win and defeat it. */
.hero-video {
  display: block;
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: black;
}
.hero-video[hidden] { display: none; }
.hero-portrait .hero-video { object-fit: contain; }

/* The neighbor photo peeking in from off-screen while wireHeroSwipe() in
   app.js is live-tracking a horizontal drag - mirrors #main-image-link's
   own bg+fg pair (.hero-img-bg/.hero-img) so a portrait photo is already
   letterboxed with its blurred backdrop *during* the drag, not just once
   goToIndex() commits the swap and applyHeroOrientation() takes over for
   real. .hero-drag-preview is the wrapper the JS transforms as one unit -
   sized/positioned exactly like #main-image-link so it tiles edge-to-edge
   with it as both get dragged; z-index 1 on the wrapper keeps the whole
   thing under .hero-scrim (2) as it slides past the title/gradient. */
.hero-drag-preview {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}
.hero-drag-preview-bg,
.hero-drag-preview-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.hero-drag-preview-bg {
  display: none; /* shown only once wireHeroSwipe()'s decideOrientation() finds this preview is a portrait photo */
  filter: blur(28px) brightness(0.55) saturate(1.15);
  transform: scale(1.15); /* same over-scale as .hero-img-bg - the blur softens past its own edges otherwise */
}
.hero-drag-preview-img.portrait { object-fit: contain; background: transparent; }

.hero-scrim {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 2; /* above .hero-img (1) and .hero-img-bg - without this it fell
                 behind the photo once .hero-img became a positioned/
                 z-indexed element for the portrait-photo backdrop trick */
  padding: 40px 22px 18px;
  background: linear-gradient(to top, rgba(0,0,0,0.78) 0%, rgba(0,0,0,0.35) 55%, transparent 100%);
  color: white;
}
.hero-kicker {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  opacity: 0.9;
  margin-bottom: 4px;
}
.hero-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 24px;
  line-height: 1.15;
  margin: 0;
  text-shadow: 0 1px 8px rgba(0,0,0,0.3);
}

/* Swipe indicator - mobile-only (see the @media 768px block below), and
   only ever populated (see updateHeroDots() in app.js) when there's
   actually more than one photo to swipe between. Sits near the top, not
   the bottom, since .hero-scrim/.hero-title already own the bottom edge. */
.hero-dots {
  position: absolute;
  top: 12px;
  left: 0;
  right: 0;
  z-index: 3;
  display: none;
  justify-content: center;
  gap: 6px;
  pointer-events: none;
}
.hero-dots:empty { display: none !important; }
.hero-dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.5); box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
.hero-dot.active { background: white; }

@media (max-width: 768px) {
  /* pan-y, not none: a vertical drag through the hero should still scroll
     the page normally (mobile's bottom-sheet panel can be short enough that
     the hero itself sits near a scroll boundary) - only horizontal gestures
     are ours to interpret as a swipe, per wireHeroSwipe() in app.js. Kept
     here, next to .hero-dots' own base rule, rather than in the earlier
     global mobile block - .hero-dots{display:flex} needs to out-cascade
     the unconditional .hero-dots{display:none} above it, which only works
     if this comes after it in source order (equal specificity, so it's
     purely a source-order tiebreak either way). */
  .hero { touch-action: pan-y; }
  .hero-dots { display: flex; }
}

.panel-body { padding: 20px 22px 28px; }
.panel-body .meta { color: var(--ink-soft); font-size: 13px; margin-bottom: 4px; }
.panel-body h3 {
  font-family: var(--font-display);
  font-weight: 600;
  margin: 24px 0 8px;
  font-size: 16px;
  color: var(--cyl-purple-dark);
}

.source-list { margin: 0; padding-left: 20px; font-size: 13px; line-height: 1.8; color: var(--ink); }
.source-list a { color: var(--cyl-purple); }

/* Cross-links between the "written pages" (About/Contribute/Stats/Privacy/
   Imprint - see pageNavHtml() in app.js) - first thing in .panel-body so
   it reads as this page's own sub-nav, not part of the page content
   proper. Buttons, not <a href>, matching how the rest of the app's
   navigation (list rows, menu) is JS-driven rather than real links. */
.page-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 18px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}
.page-nav-link {
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  padding: 6px 12px;
  border-radius: 14px;
  border: 1.5px solid var(--border);
  background: var(--card);
  color: var(--ink-soft);
  cursor: pointer;
}
.page-nav-link:hover { border-color: var(--cyl-purple); color: var(--ink); }
.page-nav-link.active { background: var(--cyl-purple); border-color: var(--cyl-purple); color: white; cursor: default; }

.stats-mission {
  border-left: 3px solid var(--cyl-purple);
  padding-left: 12px;
  margin: 6px 0 18px;
  font-style: italic;
  color: var(--ink);
}

.stats-numbers { display: flex; gap: 10px; margin: 4px 0 6px; }
.stat-box {
  flex: 1 1 0;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 10px;
  text-align: center;
}
.stat-value { font-family: var(--font-display); font-size: 22px; font-weight: 600; color: var(--cyl-purple-dark); }
.stat-pct { font-size: 13px; font-weight: 500; color: var(--ink-soft); }
.stat-label { font-size: 11.5px; color: var(--ink-soft); margin-top: 2px; line-height: 1.3; }

/* renderPhotoStats() in app.js - deliberately its own callout, not a
   fourth .stat-box, since (unlike the other three) it isn't refreshed by
   every `make build`, just whenever someone runs `make photo-stats` by
   hand - .stats-photos-note carries that as-of date so it never reads as
   equally live as the figures above it. */
.stats-photos {
  margin: 10px 0 6px;
  padding: 14px 16px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  text-align: center;
}
.stats-photos-value { font-family: var(--font-display); font-size: 28px; font-weight: 600; color: var(--cyl-purple-dark); }
.stats-photos-label { font-size: 13px; color: var(--ink-soft); margin-top: 2px; }
.stats-photos-note { font-size: 11.5px; color: var(--ink-soft); margin-top: 8px; line-height: 1.4; }

.stats-chart-wrap { position: relative; }
.stats-chart { width: 100%; height: auto; margin-top: 6px; display: block; cursor: crosshair; }
.stats-chart-axis { display: flex; justify-content: space-between; font-size: 11px; color: var(--ink-soft); margin-top: 2px; }
.stats-chart-legend { display: flex; gap: 16px; font-size: 12.5px; color: var(--ink-soft); margin-top: 8px; }
.legend-swatch { display: inline-block; width: 12px; height: 3px; margin-right: 5px; vertical-align: middle; }
.legend-swatch.linked { background: var(--color-linked); }
.legend-swatch.image { background: var(--cyl-purple); }
.stats-chart-empty { font-size: 13px; color: var(--ink-soft); font-style: italic; margin-top: 4px; }

/* Hover overlay: a per-point invisible hit-area, a vertical guide line and
   two dots (one per series) that only appear while hovering, plus a
   floating tooltip anchored above the guide line - all pure SVG/CSS, no
   charting library. */
.stats-chart-hit { fill: transparent; }
.stats-chart-guide { stroke: var(--ink-soft); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0; }
/* r is intentionally not set here - it's a CSS-stylable SVG2 geometry
   property, so a value here would override the JS setAttribute('r', ...)
   calls that actually show/hide the dot (CSS always wins over presentation
   attributes). Starts at the SVG default (0, i.e. invisible). */
.stats-chart-dot { stroke-width: 2; fill: var(--bg, #fff); transition: r 0.1s ease; }
.stats-chart-dot.linked { stroke: var(--color-linked); }
.stats-chart-dot.image { stroke: var(--cyl-purple); }
.stats-chart-tooltip {
  /* Fixed, not absolute: #panel/#panel-content clip overflow (rounded
     corners, scroll), which silently cropped the tooltip whenever it had
     to render above the chart or near the panel's edge. Fixed positioning
     escapes that (nothing between here and the viewport sets a transform/
     filter, which would otherwise re-establish a containing block) - JS
     computes left/top in viewport coordinates and clamps them to stay
     fully on-screen. */
  position: fixed;
  left: 0;
  top: 0;
  transform: translateX(-50%);
  background: var(--cyl-purple-dark);
  color: #fff;
  padding: 7px 10px;
  border-radius: 8px;
  font-size: 12px;
  line-height: 1.5;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.1s ease;
  z-index: 2000;
}
.stats-chart-tooltip.above { transform: translate(-50%, calc(-100% - 10px)); }
.stats-chart-tooltip.below { transform: translate(-50%, 10px); }
.stats-chart-tooltip.visible { opacity: 1; }
.stats-chart-tooltip .tt-date { font-weight: 600; margin-bottom: 2px; }
.stats-chart-tooltip .tt-row { display: flex; align-items: center; gap: 5px; }
.stats-chart-tooltip .tt-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.stats-chart-tooltip .tt-dot.linked { background: var(--color-linked); }
.stats-chart-tooltip .tt-dot.image { background: var(--cyl-purple); }
.stats-chart-tooltip .tt-delta { color: #c9e8dc; }
.stats-chart-tooltip .tt-delta.down { color: #f0c8c8; }

.list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border); }
/* .list-row is a real <a href="?id=..."> now (see monumentListItemHtml()
   in app.js), not a plain click-only <li> - reset the default link look
   since visually it should still read as one plain row, same as before
   this was a real link. */
.list-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 2px;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  font-size: 13.5px;
  color: inherit;
  text-decoration: none;
  transition: background 0.12s ease;
}
.list-row:hover { background: var(--bg); }
/* Keyboard focus needs its own visible ring - browsers don't draw one on
   an <a> the way they do on a <button>/<input> by default. -2px offset
   (inset, not outset) so it doesn't get clipped by .panel-body's own
   overflow or bleed into the next row. */
.list-row:focus-visible { outline: 2px solid var(--cyl-purple); outline-offset: -2px; border-radius: 4px; }
.list-row-icon {
  width: 22px; height: 22px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px;
  flex-shrink: 0;
}
.list-row-icon.linked { background: var(--color-linked); }
.list-row-icon.missing { background: var(--color-missing); }
.list-row-icon-plain { width: 22px; text-align: center; font-size: 16px; flex-shrink: 0; }
.list-row-thumb {
  width: 26px; height: 26px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}
.list-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-row-stat { color: var(--ink-soft); font-size: 12.5px; flex-shrink: 0; }

/* First thing in .panel-body now (right under the hero photo) - tighter
   top margin than a mid-content element would want, so it reads as
   attached to the photo above rather than floating in the body padding. */
.image-caption { font-size: 11.5px; color: var(--ink-soft); margin: -6px 0 14px; line-height: 1.4; }
.image-caption:empty { display: none; }
.image-caption a { color: var(--ink-soft); }

.gallery { display: flex; gap: 8px; overflow-x: auto; margin: 12px 0 4px; padding-bottom: 4px; }
.gallery img {
  height: 68px;
  width: 68px;
  object-fit: cover;
  border-radius: 8px;
  cursor: pointer;
  border: 2px solid transparent;
  flex-shrink: 0;
  transition: transform 0.15s ease, border-color 0.15s ease;
}
.gallery img:hover { border-color: var(--cyl-purple); transform: translateY(-2px); }
/* Whichever photo is currently in the hero slot - stays in the gallery
   rather than being excluded from it, marked instead of removed, so "which
   one am I looking at" is always visible at a glance. */
.gallery img.active { border-color: var(--cyl-purple); cursor: default; }

/* Wraps each thumbnail (see renderGallery() in app.js) purely as an anchor
   for .video-thumb's play-icon overlay below - sizing/hover/border all
   stay on the <img> itself, unchanged; this shrinks to fit it exactly. */
.gallery-thumb { position: relative; display: inline-flex; flex-shrink: 0; }
.video-thumb::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 0;
  height: 0;
  /* a solid CSS triangle, not an emoji/icon font glyph - crisp at any
     size and never waits on a font to load */
  border-style: solid;
  border-width: 7px 0 7px 11px;
  border-color: transparent transparent transparent white;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.6));
  pointer-events: none; /* clicks fall through to the <img> underneath, same goToIndex() as any other thumbnail */
}

/* Appended after the thumbs, only when fetchGalleryFiles() found more than
   the 12 shown - links out to the full Commons category instead of trying
   to fetch/paginate everything ourselves. Sized to match .gallery img so it
   sits in the scroll strip as just one more tile, not a different kind of
   thing. */
.gallery-more {
  height: 68px;
  min-width: 68px;
  max-width: 90px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 4px 8px;
  border-radius: 8px;
  border: 2px solid var(--border);
  background: var(--bg);
  color: var(--ink-soft);
  font-size: 11px;
  font-weight: 600;
  line-height: 1.25;
  text-decoration: none;
  flex-shrink: 0;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.gallery-more:hover { border-color: var(--cyl-purple); color: var(--cyl-purple); }

.extract { font-size: 14.5px; line-height: 1.6; margin: 14px 0; color: var(--ink); }
.text-license { font-size: 11px; color: var(--ink-soft); margin: -8px 0 14px; line-height: 1.4; }
.text-license a { color: var(--ink-soft); }

.read-more-link {
  display: inline-block;
  font-size: 13px;
  font-weight: 500;
  color: var(--cyl-purple);
  text-decoration: none;
  margin: -6px 0 12px;
}
.read-more-link:hover { text-decoration: underline; }

.link-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 7px 14px;
  border-radius: 18px;
  font-size: 13px;
  font-weight: 500;
  text-decoration: none;
  border: 1.5px solid var(--border);
  background: var(--card);
  color: var(--ink);
  transition: border-color 0.15s ease, transform 0.1s ease;
}
.badge svg { flex-shrink: 0; }
.wiki-w-icon {
  font-family: var(--font-display);
  font-weight: 700;
  font-style: normal;
  font-size: 14px;
  line-height: 1;
  flex-shrink: 0;
}
.badge:hover { transform: translateY(-1px); }
/* jcyl/wikidata/wikipedia/share-btn/filter-link deliberately share one
   neutral color - previously each had its own (blue for Wikidata,
   near-black for Wikipedia, purple for share), which read as arbitrary
   rather than meaningful next to each other. The brand marks (Wikidata's
   tricolor SVG, Wikipedia's serif "W") keep their own identity regardless -
   only the button text/border is unified. */
.badge.wikidata,
.badge.wikipedia,
.badge.jcyl,
.badge.share-btn,
.badge.filter-link {
  border-color: var(--border);
  color: var(--ink-soft);
}
/* font-family: inherit only, NOT the "font" shorthand - "font: inherit"
   resets size/weight/line-height too, silently overriding the 13px/500
   .badge sets above and making this one button look different from its
   sibling badges (buttons don't inherit font-family by default, unlike
   every other element, so *something* here is still required). */
.badge.share-btn { font-family: inherit; cursor: pointer; }

.facts {
  margin: 4px 0 10px;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.fact-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 8px 0;
  font-size: 13.5px;
}
.fact-row + .fact-row { border-top: 1px solid var(--border); }
.fact-icon { flex-shrink: 0; font-size: 13px; }
.fact-label { color: var(--ink-soft); flex: 0 0 auto; min-width: 90px; }
.fact-value { color: var(--ink); font-weight: 500; }

/* Generic "this monument is missing X, here's how to add it" callout -
   originally just the no-photo case (still .contribute-cta in the DOM,
   renamed from upload-cta since it's now shared), reused as-is for the
   no-Wikipedia-article case below. Purely a visual pattern, nothing
   upload-specific in it. */
.contribute-cta {
  margin-top: 12px;
  padding: 14px;
  background: #f5f0f7;
  border: 1px solid #e5d5e2;
  border-radius: 10px;
}
.contribute-cta-text { font-size: 13.5px; color: var(--ink); margin-bottom: 10px; line-height: 1.5; }
.contribute-cta-btn.badge { background: var(--cyl-purple); color: white; border-color: var(--cyl-purple); }
.contribute-cta-btn.badge:hover { background: var(--cyl-purple-dark); }

.missing-note {
  font-size: 13.5px;
  color: #8a4a1f;
  background: #fbf0e6;
  border: 1px solid #f0d9c2;
  border-radius: 10px;
  padding: 11px 14px;
  margin-top: 10px;
}

.loading { color: var(--ink-soft); font-size: 13px; margin-top: 12px; }

#locate-btn {
  position: absolute;
  bottom: 24px;
  right: 16px;
  z-index: 500;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: none;
  background: rgba(255,255,255,0.92);
  backdrop-filter: blur(8px);
  box-shadow: 0 4px 16px rgba(0,0,0,0.2);
  font-size: 18px;
  cursor: pointer;
  transition: transform 0.12s ease;
}
#shuffle-btn {
  position: absolute;
  bottom: 78px;
  right: 16px;
  z-index: 500;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: none;
  background: rgba(255,255,255,0.92);
  backdrop-filter: blur(8px);
  box-shadow: 0 4px 16px rgba(0,0,0,0.2);
  font-size: 18px;
  cursor: pointer;
  transition: transform 0.12s ease;
}
#shuffle-btn:hover { transform: scale(1.08) rotate(-8deg); background: white; }

/* Stacked above shuffle/locate, same 54px rhythm (78 - 24) those two
   already use. */
#filter-btn {
  position: absolute;
  bottom: 132px;
  right: 16px;
  z-index: 500;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: none;
  background: rgba(255,255,255,0.92);
  backdrop-filter: blur(8px);
  box-shadow: 0 4px 16px rgba(0,0,0,0.2);
  font-size: 18px;
  cursor: pointer;
  transition: transform 0.12s ease;
}
#filter-btn:hover { transform: scale(1.08); background: white; }
#filter-btn.active { background: var(--cyl-purple); color: white; }

.filter-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: 8px;
  background: var(--cyl-purple);
  color: white;
  font-size: 10px;
  font-weight: 600;
  line-height: 16px;
  text-align: center;
}

#filter-panel {
  display: none;
  position: absolute;
  bottom: 132px;
  right: 68px; /* clears #filter-btn to its left, rather than overlapping it */
  width: 260px;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--card);
  border-radius: 12px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.25);
  z-index: 700;
}
#filter-panel.open { display: block; }

#filter-panel-header,
.filter-section-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 12px 14px 8px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--ink-soft);
}
/* Estado sits above Categorías in the same panel - a divider between them
   reads as two distinct groups instead of one long list with an odd gap. */
#filter-panel-header { border-top: 1px solid var(--border); margin-top: 4px; }
#filter-panel-actions { display: flex; gap: 10px; flex-shrink: 0; }
#filter-panel-actions button {
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
  color: var(--cyl-purple);
  cursor: pointer;
}
#filter-panel-actions button:hover { color: var(--cyl-purple-dark); }

.filter-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 14px;
  cursor: pointer;
  color: var(--ink);
  font-size: 13.5px;
}
.filter-row:hover { background: var(--bg); }
.filter-row input[type="checkbox"] { flex-shrink: 0; accent-color: var(--cyl-purple); }
.filter-row-icon { font-size: 15px; flex-shrink: 0; }
.filter-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filter-row-count { color: var(--ink-soft); font-size: 12px; flex-shrink: 0; }
.filter-row.unchecked .filter-row-name,
.filter-row.unchecked .filter-row-count { opacity: 0.5; }

/* Picture of the week - docked flush to the map's own bottom edge, full
   width, no rounded corners/gap floating it over the map: a shelf that's
   clearly part of the map chrome, not a card that landed adrift in open
   space (that first version - a rounded floating card centered in empty
   map area - read as lost/arbitrary; anchoring to the edge fixes that).
   Desktop-only: on mobile the map is too cramped for a docked strip, and
   the swipeable gallery already covers "surface a good photo" there.
   POTW_STRIP_HEIGHT below (116px) is echoed in the desktop-only block
   further down that lifts the zoom control/attribution/shuffle/locate/
   filter buttons clear of this strip - keep both in sync if it changes. */
#potw {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 500;
  height: 116px;
  display: flex;
  align-items: center;
  background: rgba(255,255,255,0.92);
  backdrop-filter: blur(8px);
  box-shadow: 0 -4px 16px rgba(0,0,0,0.15);
  cursor: pointer;
}
#potw:hover { background: rgba(255,255,255,0.98); }
#potw[hidden] { display: none; }
#potw img { display: block; width: 168px; height: 116px; object-fit: cover; flex-shrink: 0; }
#potw-info { padding: 0 16px; overflow: hidden; }
#potw-label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--cyl-purple);
}
#potw-name {
  font-size: 16px;
  font-weight: 600;
  color: var(--ink);
  margin-top: 3px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#potw-credit {
  font-size: 11.5px;
  color: var(--ink-soft);
  margin-top: 5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#potw-credit a { color: var(--ink-soft); }
#potw-credit:empty { display: none; }
#potw-cta {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--cyl-purple);
  margin-top: 8px;
}
#potw:hover #potw-cta { text-decoration: underline; }

/* Collaboration ask (see initPictureOfTheWeek() in app.js) - two real
   links (map/Contribute), not one destination. Was a small rounded pill
   floating in the strip's own leftover space; now it's its own full-height,
   full-width-of-its-share zone instead (flex: 1 + align-self: stretch, no
   border-radius/max-width to cap it) - the tint fills the whole part of
   the bar this content owns, right out to #potw's own right edge, rather
   than a card-shaped island with bare strip still showing around it.
   #potw-collab-text still caps its own width (so the sentence wraps at a
   sane measure instead of stretching edge to edge on a wide screen) and
   the whole thing is centered in whatever space is actually available.
   Hidden below 960px - #potw-info alone already wants most of that width,
   no good place left to put this. */
#potw-collab {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  flex: 1;
  align-self: stretch;
  margin-left: 28px;
  padding: 8px 24px;
  background: linear-gradient(135deg, rgba(116,44,100,0.09), rgba(116,44,100,0.03));
}
/* Same "big bold number" language as .stat-value/.stats-photos-value on
   the Stats page - a familiar visual cue for "here's a figure worth
   noticing" rather than a plain inline number lost in a sentence. */
#potw-collab-count {
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 700;
  color: white;
  background: var(--cyl-purple);
  border-radius: 10px;
  min-width: 40px;
  height: 40px;
  padding: 0 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
#potw-collab-text { max-width: 320px; }
/* 500, not the 600 the short label used to be - a full sentence set in a
   heavier weight starts reading as shouty rather than just noticeable. */
#potw-collab-label { font-size: 12.5px; font-weight: 500; color: var(--ink); line-height: 1.4; }
#potw-collab-links { font-size: 11.5px; margin-top: 5px; }
#potw-collab-links a { color: var(--cyl-purple); font-weight: 600; text-decoration: none; }
#potw-collab-links a:hover { text-decoration: underline; }
#potw-collab-links .sep { color: var(--ink-soft); margin: 0 3px; }
@media (max-width: 960px) {
  #potw-collab { display: none; }
}

#potw-close {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 1;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: none;
  background: rgba(0,0,0,0.35);
  color: white;
  font-size: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
#potw-close:hover { background: rgba(0,0,0,0.55); }
@media (max-width: 768px) {
  #potw { display: none; }
}
/* Lift the map's own bottom-anchored chrome (Leaflet's zoom control +
   attribution share the .leaflet-bottom corner containers) and our own
   button stack clear of the 116px strip above - only while #map-wrap
   actually carries .potw-visible (toggled in app.js alongside #potw's own
   hidden state), so once the strip is dismissed everything settles back
   down instead of leaving a gap of bare map beneath it. Desktop-only,
   matching #potw's own breakpoint - mobile (where the strip never shows,
   so the class is never added) needs no override either way. */
@media (min-width: 769px) {
  #map-wrap.potw-visible .leaflet-bottom { bottom: 116px; }
  #map-wrap.potw-visible #locate-btn { bottom: 140px; }
  #map-wrap.potw-visible #shuffle-btn { bottom: 194px; }
  #map-wrap.potw-visible #filter-btn,
  #map-wrap.potw-visible #filter-panel { bottom: 248px; }
}

#locate-btn:hover { transform: scale(1.08); background: white; }
#locate-btn.locating { animation: pulse 1s ease-in-out infinite; }
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

.you-are-here-dot {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #2a6fbb;
  border: 3px solid white;
  box-shadow: 0 1px 6px rgba(0,0,0,0.4);
  position: relative;
}
.you-are-here-dot::after {
  content: '';
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  border: 2px solid #2a6fbb;
  animation: locate-ring 1.8s ease-out infinite;
}
@keyframes locate-ring {
  0% { transform: scale(0.6); opacity: 0.8; }
  100% { transform: scale(1.8); opacity: 0; }
}

#toast {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%) translateY(20px);
  background: var(--ink);
  color: white;
  padding: 12px 20px;
  border-radius: 10px;
  font-size: 13.5px;
  max-width: 320px;
  text-align: center;
  box-shadow: 0 6px 20px rgba(0,0,0,0.3);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
  z-index: 2000;
}
#toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }

/* On big screens the map has plenty of room to spare - scale up the chrome
   around it (topbar, legend, panel) rather than leaving it looking thin
   and undersized next to all that empty map space. */
@media (min-width: 1400px) {
  #topbar { height: 66px; padding: 0 32px; }
  #topbar .brand { font-size: 25px; }
  #logo-mark { height: 50px; }
  #topbar button { font-size: 14.5px; padding: 9px 17px; }
  #menu-btn { width: 42px; height: 42px; }

  #search-wrap { max-width: 540px; margin: 0 28px; }
  #search-input { font-size: 15px; padding: 10px 18px; }
  .search-row { font-size: 14.5px; padding: 9px 10px; }
  .search-section-label { font-size: 12px; }

  #panel.open { width: 500px; }
  #panel-content { width: 500px; }

  .hero { height: 320px; }
  .hero-placeholder-icon { font-size: 140px; }
  .hero-scrim { padding: 48px 28px 22px; }
  .hero-kicker { font-size: 13px; }
  .hero-title { font-size: 30px; }
  .hero-fallback { height: 200px; }
  .hero-fallback .hero-placeholder-icon { font-size: 92px; }

  .panel-body { padding: 26px 30px 34px; }
  .panel-body .meta { font-size: 14px; }
  .panel-body h3 { font-size: 18px; margin-top: 28px; }
  .extract { font-size: 15.5px; }
  .source-list { font-size: 14px; }
  .list-row { font-size: 14.5px; padding: 12px 2px; }
  .badge { font-size: 14px; padding: 8px 16px; }

  /* A wrapping grid, not the mobile/compact horizontal scroll strip - past
     this width there's room to see several rows of photos at once instead
     of scrolling sideways through a filmstrip. auto-fill sizes the column
     count to whatever width the gallery actually has, no fixed count
     needed - it'll fit more columns once the 1800px tier below widens the
     panel further. */
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
    gap: 10px;
    overflow: visible;
  }
  /* .gallery-thumb (the grid cell) fills the cell; .gallery-thumb img
     (the actual thumbnail) then fills *that*, overriding its own base
     68x68 fixed size - same two-rule split .gallery-more already needed
     when this was still a bare <img>, just spread across the wrapper now. */
  .gallery-thumb, .gallery-more { width: 100%; height: 90px; max-width: none; min-width: 0; }
  .gallery-thumb img { width: 100%; height: 100%; }
}

/* Large desktop: the panel gets meaningfully wider still, single column
   (a two-column hero/text split was tried and reverted - it made the hero
   taller but not wider, and a landscape photo's real constraint is width;
   cover just crops harder into a narrow-but-tall box, so it read as MORE
   zoomed-in, not bigger). The hero's height switches from a fixed pixel
   number to aspect-ratio, so it actually grows in both dimensions together
   as the wider panel gives it more width, instead of staying pinned at the
   1400px tier's 320px forever while only the width around it changes. */
@media (min-width: 1800px) {
  #panel.open { width: 680px; }
  #panel-content { width: 680px; }
  .hero { height: auto; aspect-ratio: 3 / 2; }
  .hero-placeholder-icon { font-size: 170px; }
  .hero-title { font-size: 34px; }
  /* Real photos scale up with the wider panel via aspect-ratio above - the
     icon placeholder shouldn't grow the same way, it's not a photo with a
     "bigger is better" case for it. Back to a fixed, modest height. */
  .hero-fallback { height: 200px; aspect-ratio: auto; }
  .hero-fallback .hero-placeholder-icon { font-size: 100px; }
}

