/* coffeehouses.org — design tokens and layout.
 *
 * Colour is addressed ONLY through semantic tokens; no component names a raw hex.
 * The same token set drives light and dark, so contrast stays AA in both and a theme
 * change is one block, not a sweep through every rule.
 *
 * Contrast (WCAG 2.2 AA: 4.5:1 body text, 3:1 large text and UI boundaries) is
 * verified in both themes — see scripts/check_contrast.py.
 */

:root {
  /* Palette — warm neutrals with a coffee-ish accent. Raw values live ONLY here. */
  --c-espresso-950: #17110d;
  --c-espresso-900: #241a14;
  --c-espresso-800: #33251c;
  --c-espresso-700: #4a372a;
  --c-espresso-500: #7a5a43;
  --c-crema-100: #f7f1e8;
  --c-crema-200: #ece0d1;
  --c-crema-300: #d9c7b2;
  --c-white: #ffffff;
  --c-ink-900: #1c1917;
  --c-ink-700: #3f3a36;
  --c-ink-600: #5c554f;
  --c-accent-700: #8a4b1f;
  --c-accent-600: #a4581f;
  --c-accent-300: #e8a86a;
  --c-accent-200: #f2c99b;
  --c-positive-700: #2f6b3d;
  --c-positive-300: #8fd2a1;
  --c-warn-700: #8a5a12;
  --c-warn-300: #e7bd72;

  /* Semantic tokens — light theme is the default. */
  --bg: var(--c-crema-100);
  --bg-raised: var(--c-white);
  --bg-sunken: var(--c-crema-200);
  --text: var(--c-ink-900);
  --text-muted: var(--c-ink-600);
  --text-on-accent: var(--c-white);
  --border: var(--c-crema-300);
  --border-strong: var(--c-espresso-500);
  --accent: var(--c-accent-700);
  --accent-hover: var(--c-accent-600);
  --focus-ring: var(--c-accent-700);
  --tag-bg: var(--c-crema-200);
  --tag-text: var(--c-espresso-800);
  --positive: var(--c-positive-700);
  --warn: var(--c-warn-700);

  /* Layering scale. Vendored Leaflet positions its own panes from 200 up to 1000
     (.leaflet-top/.leaflet-bottom), and we do not control those values — so anything
     that must sit above a map has to clear 1000. Keep every z-index in the site
     referencing this scale rather than inventing numbers. */
  --z-header: 2000;      /* header + its dropdown, above any map */
  --z-skip-link: 2100;   /* must beat everything, including the header */

  --radius: 10px;
  --radius-sm: 6px;
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --measure: 68ch;
  --font-body: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --font-display: ui-serif, Georgia, Cambria, "Times New Roman", serif;

  /* Tell the browser this page themes itself.
     Without `color-scheme`, Chrome's auto-dark-mode decides a light page on a
     dark-mode OS needs darkening and inverts it — so choosing Light on a dark
     machine produced a dark page whose tokens said cream (#f7f1e8 rendering as
     rgb(25,27,28)). Declaring it also fixes native form controls and scrollbars. */
  color-scheme: light dark;
}

/* Dark theme: the system preference is the default signal... */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --bg: var(--c-espresso-950);
    --bg-raised: var(--c-espresso-900);
    --bg-sunken: var(--c-espresso-800);
    --text: var(--c-crema-100);
    --text-muted: var(--c-crema-300);
    --text-on-accent: var(--c-espresso-950);
    --border: var(--c-espresso-700);
    --border-strong: var(--c-crema-300);
    --accent: var(--c-accent-300);
    --accent-hover: var(--c-accent-200);
    --focus-ring: var(--c-accent-300);
    --tag-bg: var(--c-espresso-800);
    --tag-text: var(--c-crema-200);
    --positive: var(--c-positive-300);
    --warn: var(--c-warn-300);
  }
}

/* ...and the explicit control wins in BOTH directions, so a light-mode choice on a
   dark-mode OS is honoured rather than silently reverting. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: var(--c-espresso-950);
  --bg-raised: var(--c-espresso-900);
  --bg-sunken: var(--c-espresso-800);
  --text: var(--c-crema-100);
  --text-muted: var(--c-crema-300);
  --text-on-accent: var(--c-espresso-950);
  --border: var(--c-espresso-700);
  --border-strong: var(--c-crema-300);
  --accent: var(--c-accent-300);
  --accent-hover: var(--c-accent-200);
  --focus-ring: var(--c-accent-300);
  --tag-bg: var(--c-espresso-800);
  --tag-text: var(--c-crema-200);
  --positive: var(--c-positive-300);
  --warn: var(--c-warn-300);
}

:root[data-theme="light"] {
  color-scheme: light;
  --bg: var(--c-crema-100);
  --bg-raised: var(--c-white);
  --bg-sunken: var(--c-crema-200);
  --text: var(--c-ink-900);
  --text-muted: var(--c-ink-600);
  --text-on-accent: var(--c-white);
  --border: var(--c-crema-300);
  --border-strong: var(--c-espresso-500);
  --accent: var(--c-accent-700);
  --accent-hover: var(--c-accent-600);
  --focus-ring: var(--c-accent-700);
  --tag-bg: var(--c-crema-200);
  --tag-text: var(--c-espresso-800);
  --positive: var(--c-positive-700);
  --warn: var(--c-warn-700);
}

*, *::before, *::after { box-sizing: border-box; }

/* The `hidden` attribute must always win. A component rule like `.card { display: flex }`
   is author CSS and outranks the user-agent `[hidden] { display: none }`, so without
   this the filter's `card.hidden = true` sets the attribute but the card keeps
   rendering (display:flex) — the list looks unfiltered. `!important` restores the
   intended precedence (normalize.css ships this exact rule for the same reason). */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  /* The page body must never scroll sideways; wide children scroll themselves. */
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; display: block; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-hover); }

:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

.wrap { width: min(100% - 2rem, 74rem); margin-inline: auto; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: var(--z-skip-link);
  background: var(--accent); color: var(--text-on-accent);
  padding: var(--space-3) var(--space-4); border-radius: 0 0 var(--radius) 0;
}
.skip-link:focus { left: 0; }

/* ---- header ------------------------------------------------------------- */

.site-header {
  background: var(--bg-raised);
  border-bottom: 1px solid var(--border);
  /* Establishes a stacking context ABOVE Leaflet's panes, so the jump-menu dropdown
     is never painted under a map. Without this the panel's own z-index competes
     directly with Leaflet's 200-1000 in the root context and loses. */
  position: relative;
  z-index: var(--z-header);
}
.header-inner {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4);
  padding-block: var(--space-4);
}
.brand { text-decoration: none; color: var(--text); margin-inline-end: auto; }
.brand-name {
  display: block; font-family: var(--font-display);
  font-size: 1.4rem; font-weight: 700; line-height: 1.15;
}
.brand-tagline { display: block; font-size: 0.875rem; color: var(--text-muted); }

.site-nav ul {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  list-style: none; margin: 0; padding: 0;
}
.site-nav a {
  display: block; padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm); text-decoration: none; color: var(--text);
  /* 44px minimum touch target. */
  min-height: 44px; display: flex; align-items: center;
}
.site-nav a:hover { background: var(--bg-sunken); }
.site-nav a[aria-current="page"] {
  background: var(--bg-sunken); font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--accent);
}

/* The appearance control lives in the FOOTER (Brian, 2026-07-24). It gets a visible
   label there — the header version was icon-only for space, but the footer has room,
   and a labelled control is clearer than three unexplained glyphs. */
.theme-control { margin-top: var(--space-4); }
.theme-control fieldset {
  border: 0; margin: 0; padding: 0;
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
}
.theme-legend {
  float: none; padding: 0; font-size: 0.8125rem; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.theme-buttons { display: flex; gap: 2px; background: var(--bg-sunken);
  padding: 3px; border-radius: var(--radius-sm); border: 1px solid var(--border); }
/* Deliberately smaller than the site-wide 44px touch target (Brian, 2026-07-25): this is
   a low-frequency footer control, not a primary action, and three 44px squares dominated
   the footer. 32px still clears WCAG 2.2 AA SC 2.5.8 (Target Size Minimum) with margin —
   that floor is 24x24 CSS px, so do NOT shrink these further. */
.theme-buttons button {
  min-width: 32px; min-height: 32px;
  background: transparent; border: 0; border-radius: var(--radius-sm);
  color: var(--text-muted); font-size: 0.875rem; cursor: pointer;
}
.theme-buttons button:hover { color: var(--text); }
.theme-buttons button[aria-pressed="true"] {
  background: var(--bg-raised); color: var(--text);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.16);
}
/* Without JS the control cannot work, so don't present a dead one. */
.no-js .theme-control { display: none; }

/* Share bar (2026-07-25) — mirrors briangreenberg.net. Intent links only, no vendor
   SDKs; the CSP would block them anyway. Pills are standalone controls, not inline
   links, so SC 2.5.8's inline exception does not apply: keep them >= 24px. */
.share-bar {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  margin-top: var(--space-5); padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.share-label {
  font-size: 0.8125rem; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.share-bar .pill {
  display: inline-flex; align-items: center; min-height: 32px;
  padding: 0 var(--space-3); gap: 0.4rem;
  font: inherit; font-size: 0.8125rem; text-decoration: none; cursor: pointer;
  color: var(--text); background: var(--bg-raised);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.share-bar .pill:hover { border-color: var(--accent); background: var(--bg-sunken); }
.pillfav { width: 14px; height: 14px; border-radius: 3px; flex: none; }

/* ---- hero (homepage) ----------------------------------------------------- */

/* Reproduces the old site's rotating masthead. The image is decorative, so it is a
   background rather than a content <img>: no alt text to invent, and the heading still
   carries the meaning. aspect-ratio reserves the space up front, so the page never
   shifts when the image arrives. */
.hero {
  position: relative;
  margin-block: var(--space-5) var(--space-6);
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--border);
}
.hero-banner {
  aspect-ratio: 1500 / 367;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  background-color: var(--bg-sunken);   /* holds the space before the image paints */
}
/* Scrim: the banners are arbitrary photographs, so the title needs a guaranteed
   contrast floor rather than luck with whichever image loads. */
.hero-text {
  position: absolute; inset-inline: 0; bottom: 0;
  padding: var(--space-5) var(--space-4) var(--space-4);
  background: linear-gradient(to top, rgb(0 0 0 / 0.78), rgb(0 0 0 / 0.45) 55%, transparent);
  color: #fff;
}
.hero-text h1 { margin: 0; color: #fff; }
.hero-tagline { margin: var(--space-1) 0 0; color: #f2ece4; font-size: 1.05rem; }

@media (max-width: 640px) {
  /* A 4:1 strip is too short to hold text on a phone; give it more height. */
  .hero-banner { aspect-ratio: 3 / 2; }
}

/* ---- typography --------------------------------------------------------- */

h1, h2, h3 { font-family: var(--font-display); line-height: 1.2; }
h1 { font-size: clamp(1.8rem, 1.3rem + 2vw, 2.6rem); margin-block: var(--space-6) var(--space-3); }
h2 { font-size: clamp(1.35rem, 1.1rem + 1vw, 1.75rem); margin-block: var(--space-6) var(--space-3); }
h3 { font-size: 1.15rem; margin-block: var(--space-5) var(--space-2); }
.lede { font-size: 1.15rem; color: var(--text-muted); max-width: var(--measure); }

/* ---- cards / grid ------------------------------------------------------- */

.grid {
  display: grid; gap: var(--space-5); padding: 0; list-style: none;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 19rem), 1fr));
}

.card {
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
  display: flex; flex-direction: column;
  /* Anchor for the stretched-link overlay below. */
  position: relative;
  transition: border-color 0.12s ease, transform 0.12s ease;
}
/* Whole-tile click target (Brian, 2026-07-24): the title stays the ONE real link
   (screen readers announce a single link per card), and its ::after overlay stretches
   the clickable area over the entire card. No nested links exist in a card, so nothing
   is trapped under the overlay. */
.card-title a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}
.card:hover { border-color: var(--border-strong); }
.card:focus-within { border-color: var(--accent); }
@media (hover: hover) {
  .card:hover { transform: translateY(-2px); }
}
/* The card is a flex column, so .card-media is a flex item — and a flex item's default
   `min-height: auto` lets the image's intrinsic height override `aspect-ratio`
   entirely. Without `min-height: 0` a portrait photo renders full height and the grid
   goes ragged. `overflow: hidden` then keeps the cover crop inside the rounded corner. */
.card-media {
  aspect-ratio: 3 / 2;
  background: var(--bg-sunken);
  flex: 0 0 auto;
  min-height: 0;
  overflow: hidden;
}
.card-media img { width: 100%; height: 100%; object-fit: cover; }
.card-body { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); flex: 1; }
.card-title { margin: 0; font-size: 1.15rem; font-family: var(--font-display); }
.card-title a { text-decoration: none; color: var(--text); }
.card-title a:hover { color: var(--accent); text-decoration: underline; }
.card-meta { font-size: 0.875rem; color: var(--text-muted); margin: 0; }
.card-desc { margin: 0; font-size: 0.95rem; }

/* ---- tags / chips ------------------------------------------------------- */

.tags { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; margin: 0; padding: 0; }
.tag {
  display: inline-block; padding: 2px var(--space-2);
  background: var(--tag-bg); color: var(--tag-text);
  border-radius: 999px; font-size: 0.8125rem; text-decoration: none;
  border: 1px solid transparent;
}
a.tag:hover { border-color: var(--border-strong); color: var(--tag-text); }
.tag--venue { background: transparent; border-color: var(--border-strong); }

.qualifier {
  display: inline-flex; align-items: center; gap: var(--space-1);
  font-size: 0.8125rem; color: var(--warn);
}

.status-note {
  border-left: 3px solid var(--border-strong);
  background: var(--bg-sunken);
  padding: var(--space-3) var(--space-4);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-size: 0.9375rem; color: var(--text-muted);
  margin-block: var(--space-5);
}

/* ---- entry -------------------------------------------------------------- */

.entry { max-width: var(--measure); }
.entry-body :is(p, ul, ol, blockquote) { max-width: var(--measure); }
.entry-body img { border-radius: var(--radius); margin-block: var(--space-5); }
.entry-body blockquote {
  margin-inline: 0; padding-inline-start: var(--space-4);
  border-inline-start: 3px solid var(--border-strong); color: var(--text-muted);
}

/* ---- entry gallery (ADR-0012) ------------------------------------------- */
/* auto-fill rather than a fixed column count, so the grid reflows to one column on a
   narrow screen without a breakpoint and never forces a horizontal scroll. */
.gallery { margin-block-start: var(--space-7); }
.gallery h2 { margin-block-end: var(--space-4); }
.gallery-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr));
}
.gallery-grid img {
  width: 100%; height: 100%;
  /* A fixed ratio reserves the box before the image loads, so lazy-loaded photos
     further down the grid cannot shift the page as they arrive. */
  aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: var(--radius); border: 1px solid var(--border);
  background: var(--bg-sunken);
}

.entry-facts {
  display: grid; gap: var(--space-3) var(--space-5);
  grid-template-columns: max-content 1fr;
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--space-4);
  margin-block: var(--space-5); font-size: 0.9375rem;
}
.entry-facts dt { color: var(--text-muted); }
.entry-facts dd { margin: 0; }

@media (max-width: 480px) {
  .entry-facts { grid-template-columns: 1fr; gap: var(--space-1) 0; }
  .entry-facts dt { margin-top: var(--space-3); }
}

/* ---- filters ------------------------------------------------------------ */

.filters-disclosure {
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius); margin-block: var(--space-5);
}
.filters-summary {
  cursor: pointer; padding: var(--space-4);
  font-weight: 600; list-style: none;
  display: flex; align-items: center; gap: var(--space-2);
  min-height: 44px;
}
.filters-summary::-webkit-details-marker { display: none; }
.filters-summary::after {
  content: "▾"; margin-inline-start: auto; color: var(--text-muted);
  transition: transform 0.15s ease;
}
.filters-disclosure[open] .filters-summary::after { transform: rotate(180deg); }
.filters-summary:hover { color: var(--accent); }
/* Without JS the checkboxes are inert (filtering is JS); hide the control and let the
   /locations/ and /amenities/ pages do the filtering, same choice as .theme-control. */
.no-js .filters-disclosure { display: none; }


.filters {
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--space-4); margin-block: var(--space-5);
}
.filters fieldset { border: 0; margin: 0 0 var(--space-4); padding: 0; }
.filters legend { font-weight: 600; padding: 0 0 var(--space-2); }
.filter-options { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.filter-options label {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3); min-height: 44px;
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: 999px; font-size: 0.875rem; cursor: pointer;
}
.filter-options input:checked + span { font-weight: 600; }
.filter-options label:has(input:checked) {
  border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent);
}

/* ---- map ---------------------------------------------------------------- */

.map-shell {
  height: min(70vh, 34rem); border-radius: var(--radius);
  border: 1px solid var(--border); overflow: hidden; background: var(--bg-sunken);
  margin-block: var(--space-5);
}
.map-shell:empty::after {
  content: "Map requires JavaScript. Every place is also listed below.";
  display: flex; align-items: center; justify-content: center;
  height: 100%; padding: var(--space-5); color: var(--text-muted); text-align: center;
}
/* Approximate a dark basemap: OSM raster tiles have no dark variant, so the choice is
   a filter or a second tile provider. Filtering keeps the third-party surface to one. */
:root[data-theme="dark"] .leaflet-tile-pane,
:root:not([data-theme]) .leaflet-tile-pane {
  filter: none;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .leaflet-tile-pane {
    filter: invert(1) hue-rotate(180deg) brightness(0.92) contrast(0.9);
  }
}
:root[data-theme="dark"] .leaflet-tile-pane {
  filter: invert(1) hue-rotate(180deg) brightness(0.92) contrast(0.9);
}

/* Map pins are drawn in CSS, not fetched — Leaflet's default marker is three PNGs,
   and the strict CSP plus the vendoring rule makes "no extra binaries" the cheaper
   answer. Using the accent token means the pin is legible on both basemaps. */
.map-pin { background: none; border: 0; }
.map-pin-dot {
  display: block; width: 18px; height: 18px; border-radius: 50%;
  background: var(--accent);
  border: 3px solid var(--bg-raised);
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.45);
}
.leaflet-marker-icon:focus-visible .map-pin-dot,
.leaflet-marker-icon:hover .map-pin-dot { background: var(--text); }
.leaflet-popup-content a { font-weight: 600; }

/* ---- locations index ----------------------------------------------------- */

.loc-index { display: flex; flex-direction: column; gap: var(--space-6); }
.loc-country { }
.loc-heading {
  display: flex; align-items: baseline; gap: var(--space-2);
  margin-block: var(--space-5) var(--space-3);
}
.loc-heading--sub { margin-block: var(--space-4) var(--space-2); }
.loc-link { text-decoration: none; }
.loc-link:hover { text-decoration: underline; }
.loc-count {
  font-size: 0.8125rem; color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.loc-branch { padding-inline-start: var(--space-4); border-inline-start: 1px solid var(--border); }
.loc-city { padding-inline-start: var(--space-4); }

.loc-plain, .loc-neighborhoods {
  list-style: none; margin: 0; padding: 0;
}
.loc-plain li { padding-block: var(--space-1); }
/* 14 Chicago neighbourhoods read badly as one long column — flow them into columns
   that collapse to a single file on narrow screens. */
.loc-neighborhoods {
  display: grid; gap: var(--space-1) var(--space-5);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr));
}
.loc-neighborhoods li { padding-block: var(--space-1); }

/* ---- header jump menu ---------------------------------------------------- */

/* The panel is anchored to the HEADER CONTAINER, not to the button.
   Anchoring to the button breaks on mobile: the header wraps, the button lands on the
   left, and a right-aligned panel then extends off the left edge of the screen (only
   the counts stayed visible). `.header-inner` is `.wrap`, i.e. width:min(100% - 2rem,
   74rem) centred — so its edges are always >=1rem inside the viewport. Right-aligning
   the panel to that box, at a width that can never exceed it, makes overflow
   impossible at any viewport width, on either side, with no JavaScript. */
.header-inner { position: relative; }
.jump-menu { position: static; }
.jump-summary {
  cursor: pointer; list-style: none;
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: 44px; padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-sunken); color: var(--text); font-size: 0.9375rem;
}
.jump-summary::-webkit-details-marker { display: none; }
.jump-summary::after { content: "▾"; color: var(--text-muted); }
.jump-summary:hover { border-color: var(--border-strong); }
.jump-menu[open] .jump-summary { border-color: var(--accent); }

.jump-panel {
  position: absolute;
  /* Both offsets relative to .header-inner: pinned to its right edge, never wider
     than the container itself. */
  inset-inline-end: 0;
  top: calc(100% + var(--space-3));
  z-index: 50;
  width: min(20rem, 100%);
  max-height: min(60vh, 26rem); overflow-y: auto; overscroll-behavior: contain;
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--space-3);
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.28);
}
.jump-hint {
  margin: 0 0 var(--space-2); font-size: 0.8125rem; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.jump-list { list-style: none; margin: 0; padding: 0; }
.jump-list li { display: flex; align-items: baseline; gap: var(--space-2); }
.jump-list a {
  flex: 1; display: block; padding: var(--space-2); min-height: 44px;
  border-radius: var(--radius-sm); text-decoration: none; color: var(--text);
  display: flex; align-items: center;
}
.jump-list a:hover { background: var(--bg-sunken); color: var(--accent); }
.jump-all {
  margin: var(--space-2) 0 0; padding-top: var(--space-2);
  border-top: 1px solid var(--border); font-size: 0.9375rem;
}

@media (max-width: 640px) {
  .jump-label { display: none; }  /* icon-only on small screens to protect the nav */
}

/* Leaflet paints its container #ddd, which shows through wherever there are no tiles
   — above the northernmost Mercator tile at low zoom, that reads as a white bar across
   the top of the map. Our own .map-shell background sits BEHIND Leaflet's container, so
   it never wins. Overriding the container itself makes the empty area match the site in
   both themes. Note it is deliberately NOT inside the inverted .leaflet-tile-pane, so
   the dark-mode tile filter does not flip it back to light. */
.leaflet-container { background: var(--bg-sunken); }

/* Keep the zoom buttons legible against either theme, and inset so they sit over the
   map imagery rather than hard against the rounded corner. */
.leaflet-top.leaflet-left { margin: var(--space-2); }
.leaflet-bar a,
.leaflet-bar a:hover {
  background: var(--bg-raised);
  color: var(--text);
  border-bottom-color: var(--border);
}
.leaflet-bar a:hover { background: var(--bg-sunken); }
.leaflet-bar { border-color: var(--border); box-shadow: 0 1px 5px rgb(0 0 0 / 0.4); }
.leaflet-control-attribution {
  background: color-mix(in srgb, var(--bg-raised) 85%, transparent);
  color: var(--text-muted);
}
.leaflet-control-attribution a { color: var(--accent); }

/* ---- places near me ------------------------------------------------------ */

.nearme-controls { margin-block: var(--space-4) var(--space-2); }
.nearme-button {
  cursor: pointer; font: inherit; font-size: 0.9375rem;
  min-height: 44px; padding-inline: var(--space-4);
  border: 1px solid var(--border-strong); background: var(--bg-raised);
  color: var(--text);
}
.nearme-button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.nearme-button:disabled { opacity: 0.6; cursor: progress; }

.nearme-output:empty { display: none; }
.nearme-output {
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--space-4);
  margin-block: var(--space-3);
}
.nearme-summary { margin: 0 0 var(--space-3); color: var(--text-muted); font-size: 0.9375rem; }
.nearme-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.nearme-list li { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }
.nearme-list a { font-weight: 600; }
.nearme-distance { color: var(--text-muted); font-size: 0.875rem; font-variant-numeric: tabular-nums; }

/* The visitor's own position, deliberately distinct from a place pin. */
.map-pin-dot--you {
  background: var(--text);
  border-color: var(--accent);
}

/* ---- location tree ------------------------------------------------------ */

.location-tree ul { list-style: none; padding-inline-start: var(--space-5); }
.location-tree > ul { padding-inline-start: 0; }
.location-tree li { margin-block: var(--space-2); }
.location-count { color: var(--text-muted); font-size: 0.875rem; }

/* ---- pagination / footer ------------------------------------------------ */

.pagination { display: flex; gap: var(--space-3); justify-content: space-between;
  margin-block: var(--space-6); }

.site-footer {
  margin-top: var(--space-7); padding-block: var(--space-6);
  border-top: 1px solid var(--border); background: var(--bg-raised);
  color: var(--text-muted); font-size: 0.9375rem;
}
.footer-links { margin-block: var(--space-2); }
.footer-note { font-size: 0.8125rem; }

/* Wide content scrolls inside its own container, never the page. */
.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ---- analytics consent banner ------------------------------------------- */
/* Same component as gsysd.com and briangreenberg.net, mapped onto THIS site's tokens.
   Only these six lines differ per site. Every token verified to exist before use — the
   check whose absence made this render light-in-dark-mode on gsysd. No hex fallbacks:
   a wrong token name must break visibly. */
.consent-banner {
  --consent-bg: var(--bg-raised);
  --consent-fg: var(--text);
  --consent-muted: var(--text-muted);
  --consent-border: var(--border);
  --consent-accent: var(--accent);
  --consent-accent-fg: var(--text-on-accent);

  position: fixed; inset-inline: 0; bottom: 0; z-index: var(--z-header);
  background: var(--consent-bg); color: var(--consent-fg);
  border: 1px solid var(--consent-border); border-bottom: 0;
  box-shadow: 0 -4px 24px rgb(0 0 0 / 0.35);
  padding: var(--space-4) var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-2);
  width: min(100% - 1.5rem, 46rem); margin-inline: auto;
  border-radius: var(--radius) var(--radius) 0 0;
}
.consent-title { margin: 0; font-size: 1rem; font-weight: 600; color: var(--consent-fg);
  font-family: var(--font-display); }
.consent-text { margin: 0; font-size: 0.9rem; color: var(--consent-muted); max-width: 62ch; }
.consent-actions { display: flex; flex-wrap: wrap; gap: var(--space-3);
  align-items: center; margin-top: var(--space-1); }
.consent-btn {
  font: inherit; font-size: 0.9375rem; min-height: 44px; padding-inline: var(--space-4);
  border-radius: var(--radius-sm); cursor: pointer;
  border: 1px solid var(--consent-border); background: transparent; color: var(--consent-fg);
}
.consent-btn:hover { border-color: var(--consent-accent); }
.consent-btn--accept {
  background: var(--consent-accent); color: var(--consent-accent-fg);
  border-color: var(--consent-accent); font-weight: 600;
}
.consent-link { font-size: 0.875rem; margin-inline-start: auto; }
@media (max-width: 480px) {
  .consent-actions { flex-direction: column; align-items: stretch; }
  .consent-link { margin-inline-start: 0; text-align: center; }
}
