/* Discovery v3 -- the redesigned discovery screen.
 *
 * Lifted from design/discovery-search.html, which was reviewed screen by
 * screen, so the tokens and components here are the ones that were signed off
 * rather than a retyped approximation.
 *
 * Deliberately NOT a fourth entry in the mm_theme cookie: v3 has its own shell
 * (places/v3/base.html) and does not load magazine/otter/pebble at all, so it
 * cannot regress the pages that share those.
 *
 * Changes from the design file, all noted inline below:
 *   - .map is a live Leaflet container, not a background image
 *   - card/pin classes align with what the Django templates emit
 *
 * REQUIRES chrome-v3.css, which carries the :root tokens every rule below
 * reads. The header, the footer, the location picker and the buttons went
 * there too, so that the pages still built on Bootstrap can wear the same
 * chrome without taking the app shell with it. Load order between the two
 * does not matter: neither redefines anything in the other.
 */

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

/* Desktop lets the page grow so the footer can live below the fold; the
   mobile block re-pins this to the viewport for the app shell. */
html, body { min-height: 100%; }

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 24px;
  color: var(--on-surface);
  background: var(--surface);
  display: flex;
  flex-direction: column;
  -webkit-font-smoothing: antialiased;
}


/* ------------------------------------------------------------------ shell */
/* flex:none + an explicit height, not flex:1 -- as a flex child, flex:1 sets
   flex-basis:0 and the shell then grows to its content (1622px) instead of
   the viewport. */
.shell {
  flex: none;
  display: flex;
  position: relative;
  overflow: hidden;
  min-height: 0;
  height: calc(100vh - var(--topbar-h, 69px));
  height: calc(100dvh - var(--topbar-h, 69px));
}


/* Leaflet owns this box. Position/size only -- the tiles come from the
   library, not from a background image as in the design file. */
.map {
  position: absolute; inset: 0;
  background-color: var(--surface-container-low);
}
/* Keep the whole map below the rail/tab bar by stacking the CONTAINER, not
   its panes. Leaflet gives each pane its own z-index (tiles 200, markers 600,
   popups 700); flattening them all to one value renders markers underneath
   the tiles, which looks like "the pins never loaded". */
.map { z-index: 0; }
.map .leaflet-container { font-family: var(--font-body); }
/* {# DATA #} real Leaflet map mounts here — see places/static/css/vendor/leaflet.css */

.pin { position: absolute; transform: translate(-50%, -50%); cursor: pointer; }
.pin__dot {
  width: 40px; height: 40px;
  border-radius: var(--radius-full);
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow-md);
  transition: transform .3s;
}
.pin:hover .pin__dot { transform: scale(1.1); }
.pin--outdoor .pin__dot {
  background: var(--surface);
  border: 2px solid var(--action-orange);
  color: var(--action-orange);
}
.pin--indoor .pin__dot {
  background: var(--primary);
  border: 2px solid var(--surface);
  color: var(--on-primary);
}
.pin__label {
  position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  margin-top: 4px; padding: 4px 8px;
  background: var(--surface);
  border-radius: var(--radius-sm);
  font-size: 12px; font-weight: 700; white-space: nowrap;
  box-shadow: var(--shadow-sm);
  opacity: 0; transition: opacity .3s;
}
.pin:hover .pin__label { opacity: 1; }


/* --- Map pins ------------------------------------------------------------
   The .pin-bubble / .map-pin rules and the --category-* colours live in the
   theme stylesheets, which v3 deliberately does not load. Without these the
   markers render at zero size: present in the DOM, invisible on screen.
   Colours are mapped onto the v3 palette rather than copied from pebble. */

:root {
  --category-play:     #ffbf00;  /* sun-yellow  */
  --category-eat:      #ff7a59;  /* action-orange */
  --category-urgent:   #2ec4b6;  /* soft-teal */
  --category-learn:    #0057cd;  /* primary */
  --category-explore:  #2e9e5b;
  --category-shopping: #8b6bb1;
  --category-default:  #424655;  /* on-surface-variant */
}

.map-pin { background: none; border: 0; }

.pin-bubble {
  position: relative;
  width: 36px;
  height: 36px;
  background: var(--pin-color, var(--primary));
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid #fff;
  color: #fff;
  box-shadow: 0 4px 12px rgba(25, 27, 36, 0.18);
  transition: transform .2s;
}

.pin-bubble i { font-size: 16px; line-height: 1; }

.map-pin.selected { z-index: 30 !important; }

.map-pin.selected .pin-bubble {
  width: 44px;
  height: 44px;
  border-width: 3px;
  box-shadow: 0 6px 20px rgba(25, 27, 36, 0.3);
}

/* --- "You are here" ------------------------------------------------------
   The point the feed is measured from. Same blue as --primary, but a
   deliberately different FORM from the category pins -- a small flat dot
   rather than a 36px bubble with an icon -- so it never reads as a place you
   could visit. --category-learn happens to be this blue too, and shape is what
   keeps the two apart. */
.here-dot { background: none; border: 0; }

.here-dot__core,
.here-dot__halo {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
}
.here-dot__core {
  width: 14px; height: 14px;
  background: var(--primary);
  border: 2.5px solid #fff;
  box-shadow: 0 1px 4px rgba(25, 27, 36, 0.35);
}
/* Soft bloom, so the dot is findable on a busy basemap without being loud. */
.here-dot__halo {
  width: 36px; height: 36px;
  background: color-mix(in srgb, var(--primary) 22%, transparent);
  animation: here-pulse 2.4s ease-out infinite;
}

@keyframes here-pulse {
  0%   { transform: translate(-50%, -50%) scale(0.6); opacity: .75; }
  70%  { transform: translate(-50%, -50%) scale(1.15); opacity: 0; }
  100% { transform: translate(-50%, -50%) scale(1.15); opacity: 0; }
}

/* An indefinite pulse is exactly the motion this preference exists to stop,
   and Unterwegs is a screen people open under stress. The dot stays findable
   without it -- the halo just holds still. */
@media (prefers-reduced-motion: reduce) {
  .here-dot__halo { animation: none; opacity: .5; }
}

/* Bookable venue: same Action Orange as the card badge and the Book button,
   so the map carries the signal the rest of the page does. */
.pin-ticket {
  position: absolute;
  top: -2px; right: -2px;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--action-orange);
  border: 2px solid #fff;
  box-sizing: border-box;
}

/* Selected card, when its pin is clicked. */
.card.is-active {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* ------------------------------------------------------------------- rail */
.rail {
  position: relative;
  z-index: 10;
  width: 480px;
  flex: none;
  height: 100%;
  background: rgba(250, 248, 255, 0.95);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-right: 1px solid rgba(194, 198, 216, 0.3);
  box-shadow: var(--shadow-rail);
  display: flex;
  flex-direction: column;
}
.rail__head { flex: none; padding: 24px 24px 0; }
.rail__handle { display: none; }
.rail__scroll {
  flex: 1; overflow-y: auto;
  padding: var(--card-top-gap) 24px 24px;
}
.rail__scroll::-webkit-scrollbar { width: 6px; }
.rail__scroll::-webkit-scrollbar-track { background: transparent; }
.rail__scroll::-webkit-scrollbar-thumb {
  background-color: rgba(194, 198, 216, 0.5);
  border-radius: 10px;
}

.rail h1 {
  font-family: var(--font-display);
  font-size: 24px; line-height: 32px; font-weight: 600;
  margin: 0 0 8px;
  color: var(--on-surface);
}
.rail__lede { margin: 0 0 24px; color: var(--on-surface-variant); }

/* filter chips — pill "bubbles" of information */
/* Desktop wraps: there is no swipe here, and a hidden scrollbar made the
   later filters unreachable. The rail is tall, so rows are cheaper than
   concealed options. Mobile switches back to a swipeable single row. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding-bottom: 16px;
  margin-bottom: 8px;
}
.chip {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  padding: 6px 16px;
  border-radius: var(--radius-full);
  font-family: var(--font-label);
  font-size: 14px; line-height: 20px; font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  background: var(--surface);
  color: var(--on-surface);
  border: 1px solid var(--outline-variant);
  transition: background .3s, color .3s, border-color .3s;
}
.chip:hover { background: var(--surface-variant); }
.chip[aria-current="page"] {
  background: var(--primary-container);
  color: var(--on-primary-container);
  border-color: transparent;
  box-shadow: var(--shadow-sm);
}
/* 18px, not the 24px default: the chip is a 32px pill, and the icon has to
   sit on the label's cap height rather than tower over it. */
.chip__icon {
  margin-right: 6px;
  font-size: 18px;
  font-variation-settings: "FILL" 0, "wght" 500, "GRAD" 0, "opsz" 20;
}
.chip[aria-current="page"] .chip__icon { font-variation-settings: "FILL" 1, "wght" 500, "GRAD" 0, "opsz" 20; }
/* the revenue filter reads as an offer, not just another mood */
.chip--book {
  border-color: rgba(255, 122, 89, 0.5);
  color: #b8391c;
  background: rgba(255, 122, 89, 0.10);
  font-weight: 700;
}
.chip--book:hover { background: rgba(255, 122, 89, 0.18); }
.chip--book[aria-current="page"] {
  background: var(--action-orange);
  color: var(--cloud-white);
  border-color: transparent;
}


/* --- Refine row (distance + duration) ------------------------------------
   One ~40px row rather than eight more chips, which on the 480px rail would
   be two extra rows above the results. Same construction as the footer
   language switcher: our own pill with an invisible <select> over it, because
   a styled <select> is not portable across browsers. */
.refine {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0 0 16px;
}

.refine__control {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px 6px 12px;
  border: 1px solid var(--outline-variant);
  border-radius: var(--radius-full);
  background: var(--surface-container-lowest);
  transition: background .2s, border-color .2s;
}

.refine__control:hover { background: var(--surface-variant); }
.refine__control:focus-within { border-color: var(--primary); }

.refine__icon,
.refine__caret { color: var(--outline); font-size: 18px; flex: none; }

.refine__value {
  font-family: var(--font-label);
  font-size: 13px;
  font-weight: 700;
  line-height: 18px;
  color: var(--on-surface);
  white-space: nowrap;
}

/* Invisible but focusable: opacity, not display/visibility, so it keeps
   keyboard access and stays in the accessibility tree. */
.refine__select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  border: 0;
  margin: 0;
  padding: 0;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
}

.refine__select option { color: var(--on-surface); background: var(--surface); }

.refine__go {
  padding: 6px 14px;
  border: 0;
  border-radius: var(--radius-full);
  background: var(--primary);
  color: #fff;
  font-family: var(--font-label);
  font-weight: 700;
  cursor: pointer;
}

/* ------------------------------------------------------------------ cards */
.cards { display: grid; grid-template-columns: 1fr; gap: 16px; }

.card {
  position: relative;
  background: var(--surface-container-lowest);
  border: 1px solid rgba(194, 198, 216, 0.3);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: box-shadow .3s, transform .3s;
}
/* hover lifts 4px and spreads the shadow — the tactile feedback loop.
   Guarded by (hover: hover): on iOS an element carrying a :hover rule
   consumes the first tap to show that state, so a card needed tapping twice
   to open. Desktop keeps the effect; touch goes straight through. */
@media (hover: hover) {
  .card:hover { box-shadow: var(--shadow-md); transform: translateY(-4px); }
}
/* bookable venues get the orange edge, tying them to the ticketing CTA */
.card--partner { border-color: rgba(255, 122, 89, 0.45); }

.card__media { position: relative; height: 128px; background: var(--surface-container); }
.card--partner .card__media { height: 160px; }
.card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* gradient scrim for text legibility over photography */
.card__media::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(33, 37, 41, 0.6), transparent);
}

/* ---------------------------------------------------- iconized card ---
   A place with no photo. Instead of a 128px placeholder that is identical for
   every place in its category, the card goes horizontal: a small category tile
   beside the text. Saves ~96px of height each, which on mobile is the
   difference between seeing two cards and seeing four.

   The colours are the pin colours (--category-*), so a place looks the same in
   the list as on the map. The slug -> colour mapping is duplicated from
   CATEGORY_COLOR_VARS in map-pins.js; places/test_category_colours.py asserts
   the two stay in step, because a silent divergence here is exactly the kind
   of thing nobody notices. */
.card--icon { display: flex; align-items: stretch; gap: 0; }

.card--icon .card__icon {
  flex: none;
  width: 84px;
  display: grid;
  place-items: center;
  /* colour-mix so one variable drives both the tint and the glyph, and the
     tile stays legible on the card surface rather than shouting like a pin. */
  background: color-mix(in srgb, var(--card-cat, var(--category-default)) 12%, transparent);
  color: var(--card-cat, var(--category-default));
}
.card--icon .card__icon i { font-size: 30px; line-height: 1; }
.card--icon .card__body {
  flex: 1;
  min-width: 0;
  padding: 12px 14px;
  /* Centre the text against the icon tile. Without this the title sits at the
     top of a short card and the tile beside it looks unbalanced -- the photo
     layout has enough height that top-alignment reads fine, this one does
     not. */
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* No photo means no scrim, and the title sits on the surface, not on an image. */
.card--icon .card__media, .card--icon .card__media::after { display: none; }
/* Keep the save control clear of the icon tile. */
.card--icon .card__save { top: 8px; right: 8px; }
/* A bookable place without a photo is still bookable, so the badge stays --
   but it MUST remain absolutely positioned. The badge is a direct child of
   .card, and .card--icon is display:flex, so position:static turns it into a
   flex item wedged between the icon tile and the body.
   Icon-only here: "Tickets" does not fit over an 84px tile, and the Book
   button below already carries the word. font-size:0 collapses the text node
   while the glyph keeps its own size; the label still reaches screen readers. */
.card--icon .card__badge {
  top: 8px; left: 8px;
  padding: 4px;
  gap: 0;
  font-size: 0;
}
.card--icon .card__badge .material-symbols-outlined { font-size: 16px; }

/* Category colours, mirroring map-pins.js CATEGORY_COLOR_VARS. */
.card--icon[data-category="indoor_playground"],
.card--icon[data-category="playground"],
.card--icon[data-category="farm"]            { --card-cat: var(--category-play); }
.card--icon[data-category="cafe"],
.card--icon[data-category="restaurant"],
.card--icon[data-category="bakery"],
.card--icon[data-category="ice_cream"]       { --card-cat: var(--category-eat); }
.card--icon[data-category="toilets"]         { --card-cat: var(--category-urgent); }
.card--icon[data-category="museum"],
.card--icon[data-category="zoo"],
.card--icon[data-category="library"],
.card--icon[data-category="science_center"],
.card--icon[data-category="aquarium"],
.card--icon[data-category="historic_site"]   { --card-cat: var(--category-learn); }
.card--icon[data-category="park"],
.card--icon[data-category="hiking"],
.card--icon[data-category="landmark"],
.card--icon[data-category="theme_park"],
.card--icon[data-category="water_park"]      { --card-cat: var(--category-explore); }
.card--icon[data-category="shopping"]        { --card-cat: var(--category-shopping); }

.card__badge {
  position: absolute; top: 12px; left: 12px; z-index: 2;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px 8px;
  border-radius: var(--radius-sm);
  background: var(--action-orange);
  color: var(--cloud-white);
  font-size: 12px; font-weight: 700;
  box-shadow: var(--shadow-sm);
}
.card__badge .material-symbols-outlined { font-size: 14px; }

/* Save works without an account (local first, synced on sign-in), so it is a
   plain toggle with no auth gate in front of it. */
.card__save {
  position: absolute; top: 12px; right: 12px; z-index: 3;
  width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  border: 0;
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, 0.92);
  color: var(--on-surface-variant);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: color .3s, transform .15s, background .3s;
}
.card__save:hover { background: #fff; color: var(--action-orange); }
.card__save:active { transform: scale(.9); }
.card__save[aria-pressed="true"] { color: var(--action-orange); }
.card__save[aria-pressed="true"] .material-symbols-outlined {
  font-variation-settings: "FILL" 1;
}
.card__save .material-symbols-outlined { font-size: 20px; }

/* ticketing block — the only Action Orange inside a card */
.card__book {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid rgba(194, 198, 216, 0.4);
}
.card__price {
  font-family: var(--font-label);
  font-size: 13px;
  color: var(--on-surface-variant);
}
.card__price strong { color: var(--on-surface); font-size: 15px; }
/* sits above the card's stretched link so it is separately clickable */
.card__bookcta {
  position: relative; z-index: 2;
  text-decoration: none;
  display: inline-block;
}

.card__body { padding: 16px; }
.card__title {
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--on-surface);
  margin: 0;
  font-size: 18px; line-height: 26px;
}
.card--partner .card__title { font-size: 20px; line-height: 28px; }
.card__desc {
  font-family: var(--font-label);
  font-size: 14px; line-height: 20px;
  color: var(--on-surface-variant);
  margin: 4px 0 12px;
}
.card__meta {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-top: 12px;
}
.card__tags { display: flex; gap: 8px; flex-wrap: wrap; }
.tag {
  padding: 4px 8px;
  border-radius: var(--radius-sm);
  background: var(--surface-variant);
  color: var(--on-surface-variant);
  font-size: 12px; font-weight: 700;
}
.card__distance { color: var(--primary); font-weight: 700; white-space: nowrap; }
/* whole card is clickable without nesting interactive elements */
.card__link::after { content: ""; position: absolute; inset: 0; }
.card__link { position: static; color: inherit; text-decoration: none; }

/* ------------------------------------------------------------------ pager
   Sits below the card grid inside the rail's scroller.

   Two mechanisms in one block, because they are the same feature: real links
   to ?page=N for a visitor without JavaScript, and a sentinel the infinite
   scroll watches. discovery-v3.js adds .pager--auto once it has taken over,
   which is what hides the links -- so the markup degrades to a working pager
   rather than to nothing. */
.pager {
  margin-top: 16px;
  font-family: var(--font-label);
}

/* Zero-height: it is a scroll tripwire, not something to look at. The lead
   time comes from the observer's rootMargin, not from an element with size. */
.pager__sentinel { height: 1px; }

.pager--auto .pager__links { display: none; }

.pager__links {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

/* Only a next link (the common case): let it take the row rather than
   hugging the left edge under a full-width list. */
.pager__links > :only-child { margin-left: auto; margin-right: auto; }

.pager__link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 18px;
  border-radius: var(--radius-full);
  border: 1px solid var(--outline-variant);
  background: var(--surface-container-lowest);
  color: var(--on-surface);
  font-size: 14px; font-weight: 700;
  text-decoration: none;
  transition: background .2s, border-color .2s;
}
.pager__link:hover { background: var(--surface-variant); }
.pager__link .material-symbols-outlined { font-size: 18px; }
.pager__link--next {
  background: var(--primary-container);
  color: var(--on-primary-container);
  border-color: transparent;
}

/* The loading line and the end-of-list line share a look: both are quiet
   status text at the foot of the list, and neither is a control. */
.pager__status,
.pager__end {
  margin: 12px 0 0;
  text-align: center;
  font-size: 13px; line-height: 18px;
  color: var(--on-surface-variant);
}

/* A spinner rather than a skeleton card: the appended cards vary in height
   between the photo and icon layouts, so a placeholder that guessed wrong
   would jump the list at exactly the moment someone is reading it. */
.pager__status::before {
  content: "";
  display: inline-block;
  width: 14px; height: 14px;
  margin-right: 8px;
  vertical-align: -2px;
  border: 2px solid var(--outline-variant);
  border-top-color: var(--primary);
  border-radius: 50%;
  animation: pager-spin .7s linear infinite;
}

@keyframes pager-spin { to { transform: rotate(360deg); } }

/* Respect the OS setting: an indefinite spinner is exactly the kind of motion
   this preference exists to stop. The dot still marks the line as busy. */
@media (prefers-reduced-motion: reduce) {
  .pager__status::before { animation: none; border-top-color: var(--outline-variant); }
}

/* ------------------------------------------------------------- unterwegs
   Toilets, changing tables and playgrounds. A different screen from the feed,
   and deliberately a different visual weight: no photos, no descriptions, no
   tags. Somebody reading this is standing on a street corner with a toddler,
   so every row carries exactly three things -- what, how far, take me there --
   and nothing that costs vertical space without informing the decision. */
.needgroup { margin-bottom: 28px; }

.needgroup__head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
  font-family: var(--font-label);
  font-size: 15px; line-height: 20px; font-weight: 700;
  color: var(--on-surface);
}
.needgroup__head .material-symbols-outlined { font-size: 20px; color: var(--primary); }

/* The rung of the radius ladder this group landed on. Quiet, because it is
   context rather than a control -- but present, because the query widened
   itself and silently doing that is how you send someone on a walk they did
   not agree to. */
.needgroup__radius {
  margin-left: auto;
  font-size: 12px; font-weight: 500;
  color: var(--on-surface-variant);
  white-space: nowrap;
}

.essentials { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }

.essential {
  display: flex;
  align-items: center;
  /* The <li> is the grid item AND the flex row, so it defaults to
     min-width:auto -- its min-content, which `white-space: nowrap` on the name
     makes as wide as the untruncated text. A long one ("Behindertengerechte
     Toilette mit Wickelmöglichkeit…") then hangs over the right edge of the
     sheet on a phone. .essential__body having min-width:0 is not enough: that
     lets the body shrink, not the row around it. */
  min-width: 0;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid rgba(194, 198, 216, 0.35);
  border-radius: var(--radius-lg);
  background: var(--surface-container-lowest);
}

.essential__icon {
  flex: none;
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--primary) 10%, transparent);
  color: var(--primary);
  font-size: 20px;
}

.essential__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }

.essential__name {
  font-family: var(--font-label);
  font-size: 15px; line-height: 20px; font-weight: 700;
  color: var(--on-surface);
  text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.essential__name:hover { text-decoration: underline; }

.essential__meta {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px;
  font-family: var(--font-label);
  font-size: 12px; line-height: 16px;
  color: var(--on-surface-variant);
}
.essential__walk {
  display: inline-flex; align-items: center; gap: 3px;
  font-weight: 700; color: var(--primary);
}
.essential__walk .material-symbols-outlined { font-size: 14px; }

/* Confidence, and the honest absence of it. Deliberately not colour-coded red:
   "unconfirmed" is not an error, it is the truthful state of most OSM facility
   tags, and shouting about it would make the whole screen look broken. */
.essential__flag {
  display: inline-flex; align-items: center; gap: 3px;
  color: var(--outline);
}
.essential__flag .material-symbols-outlined { font-size: 14px; }
.essential__flag--ok { color: var(--category-play, #2e7d32); }
/* Age, shown only once it is old enough to matter. Amber rather than red:
   stale is a caveat, not an error -- the place is very likely still there,
   we just last looked a long time ago. */
.essential__flag--stale { color: #a06000; }

/* The one action that matters here. 44px, because this is tapped one-handed,
   outdoors, in a hurry. */
.essential__go {
  flex: none;
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: var(--primary-container);
  color: var(--on-primary-container);
  text-decoration: none;
  transition: background .2s;
}
.essential__go:hover { background: var(--primary); color: #fff; }

.needgroup__more {
  display: inline-flex; align-items: center; gap: 4px;
  margin-top: 10px;
  font-family: var(--font-label);
  font-size: 13px; font-weight: 700;
  color: var(--primary);
  text-decoration: none;
}
.needgroup__more:hover { text-decoration: underline; }
.needgroup__more .material-symbols-outlined { font-size: 16px; }

.needgroup__empty,
.needgroup__note {
  font-family: var(--font-label);
  font-size: 13px; line-height: 18px;
  color: var(--on-surface-variant);
  margin: 0;
}
.needgroup__note { margin-top: 8px; padding-top: 16px; border-top: 1px solid var(--outline-variant); }

/* Entry point from the feed. Sits beside the location pill on mobile, which is
   the in-the-field context this screen is for -- thumb-reachable, over the map,
   not behind a scroll of the card list. */
.mapsearch__essentials {
  flex: none;
  display: inline-flex; align-items: center; gap: 6px;
  /* 48px to match .mapsearch__field beside it -- .mapsearch is a flex row and
     two pills of different heights read as a mistake. Still above the 44px
     minimum touch target. */
  height: 48px; padding: 0 14px;
  border: 1px solid rgba(194, 198, 216, 0.3);
  border-radius: var(--radius-full);
  background: var(--surface-container-lowest);
  box-shadow: 0 2px 8px rgba(13, 110, 253, 0.10);
  color: var(--on-surface);
  font-family: var(--font-label);
  font-size: 13px; font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}
.mapsearch__essentials .material-symbols-outlined { font-size: 18px; color: var(--primary); }

/* legal links inside the mobile sheet; hidden while the footer is visible */
.sheet-legal { display: none; }

/* ---------------------------------------------------------------- tabbar */
.tabbar { display: none; }

/* ------------------------------------------------- mobile search trigger */
.mapsearch { display: none; }

/* =========================================================== geo dialog */
.scrim {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(33, 37, 41, 0.4);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  animation: fade .2s ease;
}
.scrim[hidden] { display: none; }
@keyframes fade { from { opacity: 0 } to { opacity: 1 } }

.geodialog {
  position: fixed; inset: 0; z-index: 100;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 96px var(--margin-desktop) var(--margin-desktop);
  pointer-events: none;      /* only the panel is interactive */
}
.geodialog[hidden] { display: none; }

.geodialog__panel {
  pointer-events: auto;
  width: 100%;
  max-width: 512px;
  background: var(--surface);
  border: 1px solid rgba(194, 198, 216, 0.3);
  border-radius: var(--radius-lg);
  box-shadow: 0 12px 40px rgba(25, 27, 36, 0.18), 0 4px 12px rgba(13, 110, 253, 0.08);
  overflow: hidden;
  animation: dialog-in .25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes dialog-in {
  from { opacity: 0; transform: translateY(-8px) scale(.98); }
  to   { opacity: 1; transform: none; }
}

.geodialog__handle { display: none; }

.geodialog__body { display: flex; gap: 16px; padding: 24px 24px 0; }
.geodialog__icon {
  flex: none;
  width: 48px; height: 48px;
  border-radius: var(--radius-full);
  background: rgba(13, 110, 253, 0.10);
  color: var(--primary);
  display: flex; align-items: center; justify-content: center;
}
.geodialog__icon .material-symbols-outlined { font-size: 28px; }
.geodialog h2 {
  font-family: var(--font-display);
  font-size: 24px; line-height: 32px; font-weight: 600;
  margin: 0; color: var(--on-surface);
}
.geodialog p { margin: 4px 0 0; color: var(--on-surface-variant); }

.geodialog__actions {
  display: flex; justify-content: flex-end; gap: 12px;
  padding: 24px;
}

/* ====================================================== location search */
.locsearch { display: none; }



/* ------------------------------------------------------------ responsive */
/* Tablet: rail narrows and the topbar tightens before anything collapses. */
@media (max-width: 1200px) {
  .rail { width: 400px; }
}


/* ===========================================================================
   Mobile: app shell, not a scrolled page.

   The map owns the viewport and the rail becomes a bottom sheet over it,
   following the sheet convention already used in places/ (24px top radius,
   translateY + cubic-bezier(.2,.8,.2,1), 40x4 grab handle). Three snap points
   driven by the --sheet-y custom property so the drag gesture can write an
   arbitrary value between them.
   =========================================================================== */
@media (max-width: 900px) {
  html, body { height: 100%; }
  body {
    overflow: hidden;   /* mobile stays a fixed app shell */
    /* fall back for browsers without dvh, then prefer it: avoids the sheet
       being clipped by mobile Safari's collapsing toolbar */
    height: 100vh;
    height: 100dvh;
  }


  /* Mobile: back to a fixed shell, footer hidden, sheet over the map. */
  .shell { position: relative; overflow: hidden; height: auto; flex: 1; }

  /* map fills everything behind the sheet */
  .map { position: absolute; inset: 0; }

  /* ---- the sheet ---- */
  .rail {
    /* % of shell height hidden at rest. Lowered from 54: the header chrome
       (handle + title + chips + refine) left under ~100px of cards, which
       is less than one card and made the list awkward to scroll. */
    --sheet-peek: 46;
    --sheet-y: var(--sheet-peek);
    position: absolute;
    left: 0; right: 0; bottom: 0;
    top: 0;
    width: 100%;
    height: auto;
    z-index: 40;
    background: var(--surface-container-lowest);
    border-right: 0;
    border-radius: 24px 24px 0 0;
    /* stronger than the card shadow: this floats above the map */
    box-shadow: 0 -4px 24px rgba(13, 110, 253, 0.12), 0 -1px 4px rgba(25, 27, 36, 0.06);
    transform: translateY(calc(var(--sheet-y) * 1%));
    transition: transform .3s cubic-bezier(0.2, 0.8, 0.2, 1);
    overscroll-behavior: contain;
  }
  /* while dragging, follow the finger with no easing */
  .rail.is-dragging { transition: none; }
  .rail.is-expanded { --sheet-y: 2; }
  .rail.is-collapsed { --sheet-y: 78; }
  /* Collapsed shows only the handle and the title. Hiding the chips rather
     than tuning the snap % keeps the cut clean whatever the translated
     strings wrap to. */
  .rail.is-collapsed .chips,
  .rail.is-collapsed .rail__scroll { display: none; }

  /* single swipeable row on touch, where the gesture is natural and the
     vertical space is scarce */
  .chips {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    -webkit-overflow-scrolling: touch;
  }
  .chips::-webkit-scrollbar { display: none; }
  .chip { white-space: nowrap; flex: none; }

  /* Same treatment as the chips above, for the same reason. Three controls
     (distance, duration, sort) wrap to two rows on a 360px phone, and this
     header is already eating the ~100px the sheet leaves for cards at peek.
     One swipeable row keeps it at ~40px. */
  .refine {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .refine::-webkit-scrollbar { display: none; }
  .refine__control { flex: none; }

  .rail__head { padding: 0 var(--margin-mobile) 0; }

  /* Grab handle. The bar stays 40x4 (matching places/ collection_add_sheet),
     but the HIT AREA is 44px tall -- the minimum comfortable touch target.
     At the old 10px/14px padding it was ~28px, which is why the sheet was
     hard to grab. */
  .rail__handle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 44px;
    padding: 0;
    background: none;
    border: 0;
    cursor: grab;
    touch-action: none;          /* we handle the drag ourselves */
  }
  .rail__handle:active { cursor: grabbing; }
  .rail__handle span {
    display: block;
    width: 40px; height: 4px;
    margin: 0 auto;
    border-radius: 2px;
    background: var(--outline-variant);
  }

  .rail h1 {
    font-size: 20px; line-height: 26px;
    margin-bottom: 12px;
    /* Doubles as a drag surface (see discovery-v3.js). touch-action:none is
       required or the browser scrolls instead of giving us pointermove. Safe
       here because an h1 has nothing to tap; it is NOT set on .rail__head,
       which would kill the chips' horizontal swipe. */
    touch-action: none;
    cursor: grab;
    user-select: none;
  }
  /* Hidden on mobile: the h1 already says what this list is, and at peek this
     line cost ~38px of the ~100px left for cards. */
  .rail__lede { display: none; }

  /* Once the visitor has actually used the sheet, the title has done its job
     and becomes 38px of wasted height. Set by discovery-v3.js on first drag or
     scroll, remembered for the session. */
  .rail--compact .rail__head h1 { display: none; }
  /* The h1 doubles as a drag surface, so removing it would drop the grab area
     back to 44px. Give the handle the height instead -- same target, no title. */
  .rail--compact .rail__handle { min-height: 56px; }

  .rail__scroll {
    /* The sheet is a FULL-height element pushed down by translateY, so the
       bottom var(--sheet-y)% of it sits below the shell's clip. .rail__scroll
       is flex:1 against that full height, which left its last stretch of
       content permanently below the fold AND unscrollable -- the container
       itself was not overflowing.
       --sheet-hidden is that offset in px, written by discovery-v3.js
       (setState + drag). Padding it back makes the last card reachable, and
       makes the container overflow again so the scroll-to-expand handler
       still fires. */
    padding: var(--card-top-gap) var(--margin-mobile)
             calc(24px + var(--sheet-hidden, 0px));
    /* -webkit-overflow-scrolling: touch removed. Momentum scrolling has been
       the default since iOS 13, and the property creates its own stacking
       context -- a documented cause of absolutely positioned children inside
       the scroller (here: .card__link::after, the stretched tap target)
       becoming untappable on device while working in desktop emulation. */
    overscroll-behavior: contain;
  }
  /* ---- compact cards -------------------------------------------------
     Every card goes horizontal down here, photo or no photo: the 128px hero
     (160px on a partner card) is the single biggest consumer of sheet height,
     and the sheet only offers ~100px of cards at peek. A thumbnail beside the
     text costs ~96px less per card, which is the difference between seeing
     one card and seeing three.

     This is the .card--icon layout from above, widened to apply to .card --
     which already matches the icon cards, so the two stay identical by
     construction rather than by two sets of rules agreeing. The hero survives
     on desktop, where the rail is tall and the photo earns its space. */
  .card { display: flex; align-items: stretch; }

  /* Same 84px tile the icon layout uses. height:auto so the flex stretch
     matches the body, and the partner card's taller hero is overridden with
     it -- otherwise a bookable place keeps a 160px banner. */
  .card__media,
  .card--partner .card__media {
    width: 84px;
    height: auto;
    flex: none;
  }
  /* The scrim exists to keep text legible ON TOP of a photo. Here the text is
     beside it, so the gradient is just a smudge over the thumbnail. */
  .card__media::after { display: none; }

  .card__body {
    flex: 1;
    min-width: 0;
    padding: 12px 14px;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  /* Two lines, hard. The body is ~150px narrower than the full-width layout
     the 110-character truncation was tuned for, so an untrimmed description
     wraps to four or five lines and gives back the height the thumbnail just
     saved. */
  .card__desc {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin-bottom: 0;
  }

  /* Clear of the thumbnail, as on the icon cards. */
  .card__save { top: 8px; right: 8px; }
  /* Icon-only: "Tickets" does not fit over an 84px tile, and the Book button
     below still carries the word. font-size:0 collapses the text node while
     the glyph keeps its own size, so the label still reaches screen readers. */
  .card__badge {
    top: 8px; left: 8px;
    padding: 4px;
    gap: 0;
    font-size: 0;
  }
  .card__badge .material-symbols-outlined { font-size: 16px; }

  /* room for the tab bar plus the home indicator */
  .rail__scroll::after {
    content: "";
    display: block;
    height: calc(72px + env(safe-area-inset-bottom));
  }

  /* legal links replace the footer down here; chrome-v3.css hides the footer
     itself, scoped to the app shell */
  .sheet-legal {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 16px;
    margin-top: 24px;
    padding-top: 16px;
    border-top: 1px solid rgba(194, 198, 216, 0.4);
  }
  .sheet-legal a {
    font-family: var(--font-label);
    font-size: 13px;
    color: var(--on-surface-variant);
    text-decoration: none;
  }
  .sheet-legal a:hover { text-decoration: underline; }

  /* ---- bottom tab bar ---- */
  .tabbar {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 60;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    background: rgba(255, 255, 255, 0.94);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-top: 1px solid rgba(194, 198, 216, 0.4);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .tabbar__item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 8px 4px 10px;
    text-decoration: none;
    color: var(--on-surface-variant);
    font-family: var(--font-label);
    font-size: 11px;
    font-weight: 700;
    transition: color .3s;
  }
  .tabbar__item .material-symbols-outlined { font-size: 24px; }
  .tabbar__item[aria-current="page"] { color: var(--primary); }
  /* Book stays orange even when inactive — it is the revenue surface */
  .tabbar__item--accent { color: var(--action-orange); }
  .tabbar__item--accent[aria-current="page"] { color: var(--action-orange); }

  /* ---- floating search + locate over the map ---- */
  .mapsearch {
    position: absolute;
    top: 12px; left: var(--margin-mobile); right: var(--margin-mobile);
    z-index: 30;
    display: flex;
    gap: 8px;
  }
  .mapsearch__field {
    height: 48px;
    background: var(--surface-container-lowest);
    border: 1px solid rgba(194, 198, 216, 0.3);
    border-radius: var(--radius-full);
    box-shadow: 0 2px 8px rgba(13, 110, 253, 0.10);
    cursor: pointer;
    transition: transform .15s;
  }
  .mapsearch__field:active { transform: scale(.95); }
  .mapsearch__field {
    flex: 1;
    min-width: 0;
    display: flex; align-items: center; gap: 12px;
    padding: 0 16px;
    font-family: var(--font-body);
    font-size: 16px;
    color: var(--on-surface-variant);
    text-align: left;
  }
  .mapsearch__field .material-symbols-outlined { color: var(--primary); flex: none; }
  .mapsearch__label {
    flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-weight: 600; color: var(--on-surface);
  }
  .mapsearch__caret { color: var(--outline) !important; }

  /* ---- permission ask becomes a bottom sheet ---- */
  .geodialog {
    align-items: flex-end;
    padding: 0;
  }
  .geodialog__panel {
    max-width: none;
    border: 0;
    border-radius: 24px 24px 0 0;
    padding-bottom: env(safe-area-inset-bottom);
    animation: sheet-in .3s cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  @keyframes sheet-in {
    from { transform: translateY(100%); }
    to   { transform: none; }
  }
  .geodialog__handle {
    display: block;
    width: 100%;
    padding: 10px 0 4px;
    background: none; border: 0;
  }
  .geodialog__handle span {
    display: block;
    width: 40px; height: 4px; margin: 0 auto;
    border-radius: 2px;
    background: var(--outline-variant);
  }
  .geodialog__body { padding: 16px var(--margin-mobile) 0; }
  .geodialog h2 { font-size: 20px; line-height: 28px; }
  .geodialog__actions {
    padding: 24px var(--margin-mobile) 32px;
  }
  /* full-width stacked buttons read better under the thumb */
  .geodialog__actions .uibtn { flex: 1; transform: none; padding: 12px 16px; }
  .geodialog__actions .uibtn:active { transform: scale(.97); }

  /* ---- full-screen location search ---- */
  .locsearch {
    position: fixed; inset: 0; z-index: 110;
    display: flex;
    flex-direction: column;
    background: var(--surface);
    transform: translateY(100%);
    transition: transform .3s cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  .locsearch[hidden] { display: none; }
  .locsearch.is-open { transform: none; }

  .locsearch__head {
    flex: none;
    padding: env(safe-area-inset-top) var(--margin-mobile) 16px;
    border-bottom: 1px solid rgba(194, 198, 216, 0.2);
  }
  .locsearch__bar {
    display: flex; align-items: center; justify-content: space-between;
    padding-top: 8px; margin-bottom: 16px;
  }
  .locsearch__back {
    width: 40px; height: 40px; margin-left: -8px;
    display: flex; align-items: center; justify-content: center;
    border: 0; background: none; cursor: pointer;
    border-radius: var(--radius-full);
    color: var(--on-surface-variant);
  }
  .locsearch__back:hover { background: var(--surface-container); }
  .locsearch__title {
    font-family: var(--font-display);
    font-size: 18px; font-weight: 600; color: var(--on-surface);
  }
  .locsearch__spacer { width: 40px; }

  .locsearch__input { position: relative; }
  .locsearch__input .material-symbols-outlined {
    position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
    color: var(--outline); pointer-events: none;
  }
  .locsearch__input input {
    width: 100%;
    padding: 12px 16px 12px 48px;
    background: var(--surface-container-lowest);
    border: 2px solid var(--outline-variant);
    border-radius: var(--radius-md);
    font-family: var(--font-body);
    font-size: 16px;             /* 16px min, or iOS zooms on focus */
    color: var(--on-surface);
    outline: none;
    box-shadow: var(--shadow-sm);
    transition: border-color .2s;
  }
  .locsearch__input input:focus { border-color: var(--primary); }

  .locsearch__body {
    flex: 1;
    overflow-y: auto;
    padding: 16px var(--margin-mobile) calc(24px + env(safe-area-inset-bottom));
    background: var(--surface-bright, var(--surface));
  }

  .locsearch__current {
    width: 100%;
    display: flex; align-items: center; gap: 16px;
    padding: 16px;
    margin-bottom: 24px;
    background: rgba(13, 110, 253, 0.08);
    border: 0;
    border-radius: var(--radius-md);
    cursor: pointer;
    text-align: left;
    transition: background .2s, transform .15s;
  }
  .locsearch__current:hover { background: rgba(13, 110, 253, 0.16); }
  .locsearch__current:active { transform: scale(.98); }
  .locsearch__current-icon {
    flex: none;
    width: 40px; height: 40px;
    border-radius: var(--radius-full);
    background: var(--primary);
    color: var(--on-primary);
    display: flex; align-items: center; justify-content: center;
    box-shadow: var(--shadow-sm);
  }
  .locsearch__current-title {
    display: block;
    font-family: var(--font-label);
    font-size: 16px; font-weight: 700; color: var(--primary);
  }
  .locsearch__current-sub {
    display: block; font-size: 13px; color: rgba(0, 87, 205, 0.8);
  }

  .locsearch__label {
    font-family: var(--font-label);
    font-size: 12px; line-height: 16px; font-weight: 700;
    letter-spacing: .05em; text-transform: uppercase;
    color: var(--outline);
    margin: 24px 0 12px; padding: 0 8px;
  }
  .locsearch__label:first-of-type { margin-top: 0; }

  .locsearch__list { list-style: none; margin: 0; padding: 0; }
  .locsearch__list a,
  .locsearch__list button {
    width: 100%;
    display: flex; align-items: center; gap: 16px;
    padding: 12px;
    border: 0; background: none; cursor: pointer;
    border-radius: var(--radius);
    font-family: var(--font-body);
    font-size: 16px;
    color: var(--on-surface);
    text-align: left;
    transition: background .2s, color .2s;
  }
  .locsearch__list a,
  .locsearch__list a:visited { text-decoration: none; color: var(--on-surface); }
  .locsearch__list a:hover,
  .locsearch__list button:hover { background: var(--surface-container); color: var(--primary); }
  .locsearch__list .material-symbols-outlined { color: var(--outline-variant); flex: none; }
  .locsearch__list--places button:hover { color: var(--on-surface); }
  .locsearch__list--places button:hover .material-symbols-outlined { color: var(--action-orange); }
}

/* Users who prefer reduced motion get the snap without the slide. */
@media (prefers-reduced-motion: reduce) {
  .rail, .card, .chip { transition: none; }
}
