/* Shared styling for the events browser. Kept as one small file — the site is
   two pages and a palette, not a design system. */

/* Layout, structure and behaviour. The palette lives in palette.css. */
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
button, input { touch-action: manipulation; }
html, body { height: 100%; margin: 0; overscroll-behavior: none; }
body {
  background: var(--ground); color: var(--text);
  font: 15px/1.6 system-ui, -apple-system, sans-serif;
}

a { color: var(--accent); }
code { color: var(--accent); font-size: 13px; }

/* ── The map-as-app layout, from the osm mirror ──────────────────────────────
   The map fills the screen; chrome floats over it. On a laptop the list takes
   space from the map (so pins are never hidden under it); on a phone there is no
   room to spare, so it covers the map instead. */
#map { position: fixed; inset: 0; background: var(--card); transition: right .25s ease; }
@media (min-width: 641px) {
  body.list-open #map { right: var(--panel-width); }
  body.list-open #dock { right: var(--panel-width); }

  /* The summary belongs to the map, so it centres on the map and not on the
     window. With the panel open it was still centred on the full width, which
     pushed it off-centre over the part of the map you can actually see and
     drifted it towards the panel edge.

     The dock already does this by having its container shrink; the summary is
     the pill itself rather than a container around it, so it moves its own
     centre by half the panel instead.

     The width has to come in with it: 72vw is most of the window, and the
     window is no longer what it is sitting in. Clamped to the visible map less
     the room the list button needs on the left — and the pill is centred, so
     that clearance is doubled to keep it symmetrical. */
  body.list-open #status {
    left: calc(50% - var(--panel-width) / 2);
    max-width: min(72vw, 460px, calc(100vw - var(--panel-width) - 128px));
  }
}

/* Floating chrome clusters, pinned to the corners clear of the notch. */
.chrome { position: fixed; z-index: 500; display: flex; align-items: center; gap: 10px; }
/* Top right: search. Above the list panel so it stays reachable while the panel
   is open. */
#actions {
  top: calc(12px + env(safe-area-inset-top));
  right: calc(12px + env(safe-area-inset-right));
  z-index: 600;
}

/* The field grows leftwards out of its own button, matched to it in height,
   radius and translucency so the pair reads as one control rather than a field
   that happens to sit nearby. Collapsed it has no width at all, so it cannot be
   tabbed into or clicked by accident. */
#actions input {
  height: 44px;
  width: 0; opacity: 0; padding: 0;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--chrome);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  color: var(--text);
  font: inherit; font-size: 15px;
  /* Strips the platform's own field styling — iOS's inner shadow, and the focus
     ring Chrome draws in its own blue. */
  -webkit-appearance: none; appearance: none;
  outline: none;
  transition: width .22s ease, opacity .22s ease, padding .22s ease;
}
#actions input::placeholder { color: var(--muted); }
#actions.search-open input { width: min(260px, 54vw); opacity: 1; padding: 0 15px; }
#actions.search-open input:focus { border-color: var(--accent); }
@media (prefers-reduced-motion: reduce) { #actions input { transition: none; } }

/* The list button, top left. */
#btn-list {
  position: fixed; z-index: 600;
  top: calc(12px + env(safe-area-inset-top)); left: calc(12px + env(safe-area-inset-left));
}
#btn-list[aria-pressed="true"] { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }

/* The running count and reach, pinned to the top centre of the map. It sits in a
   translucent pill clear of the list button, so a long line — count, places,
   distance, walk time — reads cleanly across the top
   rather than floating over the pins in the middle of the map. Centred on the
   viewport and non-interactive, so it never intercepts a map drag. */
#status {
  position: fixed; z-index: 500;
  top: calc(14px + env(safe-area-inset-top));
  left: 50%; transform: translateX(-50%);
  /* Matches the map and the dock, so the whole of the map's furniture moves as
     one when the panel slides rather than the summary jumping to its new spot. */
  transition: left .25s ease;
  color: var(--muted); font-size: 13px; pointer-events: none;
  font-variant-numeric: tabular-nums;
  background: var(--chrome); border: 1px solid var(--line); border-radius: 12px;
  padding: 6px 12px; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  /* One line, clipped, and never wide enough to reach the corner buttons. */
  max-width: min(72vw, 460px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#status b { color: var(--text); font-weight: 700; font-size: 15px; }

/* The status glyphs. Sized in em so they track the type, and nudged down a
   fraction because a stroked icon sits optically high against digits. */
.stat-i {
  width: 1.15em; height: 1.15em;
  vertical-align: -0.22em;
  margin-right: 2px;
}
#status b .stat-i { width: 1em; height: 1em; vertical-align: -0.13em; }

/* The bottom dock carries the filter bar centred on the map, with locate hung
   off its right edge — so the bar stays centred rather than shifting left of
   centre by half the button's width. */
#dock {
  position: fixed; z-index: 500;
  left: 0; right: 0; bottom: calc(20px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  pointer-events: none;   /* the dock spans the map; only its controls catch clicks */
  transition: right .25s ease;
}
.dock-row { position: relative; display: flex; align-items: center; pointer-events: auto; }
#btn-locate { position: absolute; left: calc(100% + 8px); }

/* Buttons and the translucent bar they sit in. */
/* iOS Safari draws its own button — a gradient, an inner shadow, its own
   padding — unless this is set, and it wins over a background declared after
   it. Everything here that is a <button> needs it, not only the fields. */
button { -webkit-appearance: none; appearance: none; }

.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; flex: none;
  color: var(--text); background: var(--chrome);
  border: 1px solid var(--line); border-radius: 12px; cursor: pointer;
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
}
.icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.icon-btn svg { width: 20px; height: 20px; flex: none; }
#btn-locate.on { color: var(--on-accent); background: var(--locate); border-color: var(--locate); }
#btn-locate:focus-visible { outline-color: var(--locate); }
#btn-locate.busy svg { animation: spin 1s linear infinite; }
#btn-locate.denied { opacity: .45; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { #btn-locate.busy svg { animation: none; } }

/* The filter bar: the five toggles in one translucent pill, no per-button box.
   It carries the .chrome class for its blur and border, but must NOT inherit
   .chrome's `position: fixed` — that pins it to the viewport and pulls it out of
   the dock, so it drifts off-centre when the list opens and shrinks the map.
   The dock lays it out instead; keep it in flow. */
#filters {
  position: static;
  align-items: center;
  background: var(--chrome); border: 1px solid var(--line); border-radius: 16px;
  padding: 6px; gap: 4px;
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
}
.icon-btn.filter {
  width: 40px; height: 40px; border: none; border-radius: 10px;
  background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none;
}
.icon-btn.filter svg { width: 20px; height: 20px; }
/* An active filter fills with the colour of what it draws — its category hue,
   the accent for tickets, the mark's colour for kept. Six buttons is enough that
   they have to be told apart at a glance rather than by reading the icons. */
.icon-btn.filter.on { color: var(--ground); background: var(--tint, var(--accent)); border-color: var(--tint, var(--accent)); }
/* Off, the colour survives as a hint in the icon, so the bar is still legible as
   three families when nothing is switched on. */
.icon-btn.filter { color: var(--tint, var(--text)); }

/* Event pins. The shape is an inline SVG so it needs no network and no colour
   rules here; the shadow is what lifts it off the basemap, which a flat fill
   cannot do on its own over a dark map. */
.event-pin { filter: drop-shadow(0 2px 3px var(--shadow-pin)); }
.event-pin svg { display: block; }

/* Venue clusters: a counted bubble standing in for a crowd of places when the
   view is too wide to draw each one. Muted, because venues are the backdrop.

   Barely there on purpose. A zoomed-out view can be most of the screen in
   bubbles, and these stand for venues, which are the backdrop — the events are
   what someone came for. At this weight the streets and the river read straight
   through, and the bubble registers as a count laid over the city rather than a
   disc covering it.

   No ring. It was doing the opposite of its job: a bright circle around a faint
   disc read as a control with a state — something toggled on, or selected — when
   all it marks is how many venues are underneath. The fill alone is enough to
   place the number, and the number is the content.

   The number is light rather than dark, which the transparency forces: a nearly
   clear disc over dark tiles *is* dark, so dark type disappears into it. Light
   type with a dark halo holds against both the fill and whatever the basemap
   puts behind it.

   Size is set inline from the count; only what does not vary lives here. */
.cluster div {
  border-radius: 50%;
  display: grid; place-items: center;
  background: var(--cluster-fill);
  /* Clicking one zooms into it, so it should say it is clickable. */
  cursor: pointer;
}

/* The list panel: docked right on a laptop, full width on a phone, slid in and
   out rather than always present. */
#list-panel {
  position: fixed; top: 0; bottom: 0; right: 0; z-index: 550;
  width: var(--panel-width); max-width: 100%;
  background: var(--ground); border-left: 1px solid var(--line);
  display: flex; flex-direction: column;
  transform: translateX(100%); transition: transform .25s ease;
}
#list-panel.open { transform: translateX(0); }
@media (max-width: 640px) { #list-panel { width: 100%; } }
.panel-head {
  padding: calc(64px + env(safe-area-inset-top)) 16px 12px;
  border-bottom: 1px solid var(--line); color: var(--muted); font-size: 13px; flex: none;
}
.panel-head b { color: var(--text); }
/* On a laptop the map sits beside the panel with the same count over its filter
   bar, so repeating it in the header leaves a bare figure adrift. The header
   stays — its top padding holds the first card clear of the list button — but
   its contents go. On a phone the panel covers the map and takes the count with
   it, so there it is the only one and earns its place. Mirrors the osm mirror. */
@media (min-width: 641px) { .panel-head { min-height: 0; } .panel-head > * { display: none; } }

/* The sorts-of-event row, above the list.

   Icons alone, no words. The row is the densest thing in the interface and the
   only one that would need translating; a glyph does not. The names survive as
   aria-labels and tooltips, which is where a screen reader looks anyway.

   Compact enough that six fit a phone without scrolling, which is the whole
   reason the labels went: with them the row ran off the edge. */
.categories {
  display: flex; gap: 6px; flex: none;
  padding: 0 12px 10px;
  border-bottom: 1px solid var(--line);
}

.category {
  flex: 1 1 0; min-width: 0; height: 32px;
  /* inline-flex rather than grid: WebKit gives a <button> a layout box of its
     own and does not lay its children out as a grid, so `place-items` centred
     nothing on iOS. Every button here that works uses this. */
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; border-radius: 9px;
  border: 1px solid var(--line); background: var(--card);
  color: var(--muted); cursor: pointer;
}
/* flex: none, or the icon is shrunk along with the button it sits in — these
   share the row's width, so there is always pressure to shrink. */
.category svg { width: 16px; height: 16px; flex: none; }
.category[aria-pressed="true"] { color: var(--ground); background: var(--accent); border-color: var(--accent); }
.category:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

/* The name of each sort, on demand.

   This replaces the native `title` attribute, which was doing none of the work
   it looked like it was doing: a title tooltip never appears on a touch device
   at all, and a phone is exactly where six unlabelled glyphs are hardest to
   read. It also waits about a second on a desktop, long enough that nobody
   hovering to ask "which one is theatre" waits for the answer.

   Drawn from a data attribute rather than the accessible name, so the two stay
   independent: a screen reader reads the aria-label and never this. */
.category { position: relative; }

.category::after {
  content: attr(data-label);
  position: absolute; top: calc(100% + 6px); left: 50%;
  transform: translateX(-50%);
  z-index: 3;
  padding: 4px 8px; border-radius: 7px;
  background: var(--text); color: var(--ground);
  font-size: 12px; line-height: 1; white-space: nowrap;
  box-shadow: var(--shadow-pin);
  opacity: 0; visibility: hidden;
  transition: opacity 120ms ease, visibility 120ms;
  pointer-events: none;   /* never eats the click meant for the button */
}

/* The row runs edge to edge, so a centred label on the outermost buttons hangs
   off the side. The two ends anchor to their own edge instead of the middle. */
.category:first-child::after { left: 0; transform: none; }
.category:last-child::after { left: auto; right: 0; transform: none; }

/* Hover only where hovering is real. On a touch screen :hover sticks after a
   tap, so the last sort touched would keep its label open indefinitely. */
@media (hover: hover) {
  .category:hover::after { opacity: 1; visibility: visible; transition-delay: 250ms; }
}

/* Keyboard focus, and the moment after a tap. The tap is the case that earns
   this: on a phone it turns an ambiguous glyph into a confirmation of what was
   just switched on or off. */
.category:focus-visible::after,
.category.naming::after { opacity: 1; visibility: visible; }

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

/* The basemap tiles load into this; keep it on-theme rather than Leaflet grey.
   The zoom buttons are gone (see the map constructor), so their bar styling and
   the margin that cleared the chrome for them went with them. */
.leaflet-container { background: var(--card); font: inherit; }

/* Cards lead with the flyer: the image is the reason events are the way in, the
   one thing the venue mirror does not have. */
#events {
  list-style: none; margin: 0; padding: 16px; gap: 12px;
  /* A flex column, not a grid: a grid with a fixed-height parent sizes its rows
     to share that height, so every card gets squashed to fit and the list never
     overflows to scroll. A column stacks each card at its natural height and
     lets the overflow scroll instead. */
  display: flex; flex-direction: column;
  overflow-y: auto; flex: 1; min-height: 0;   /* scrolls within the panel */
}
/* The list mixes two kinds of row — ticket cards and venue rows — and both keep
   their natural height rather than stretching to fill spare column space. Both
   share the card chrome and the chosen-item ring; only their innards differ. */
.event, .place {
  flex: none; background: var(--card); border: 1px solid var(--line);
  border-radius: 12px; overflow: hidden; text-decoration: none; color: inherit;
  cursor: pointer;
}
/* A ticket leads with its flyer — the image is the reason events are the way in,
   the one thing the venue mirror does not have. */
.event { display: grid; grid-template-columns: 96px 1fr; gap: 12px; }
.event img { width: 96px; height: 96px; object-fit: cover; background: var(--line); }
.event .no-image { display: grid; place-items: center; color: var(--muted); font-size: 24px; }
.event .body { padding: 10px 12px 10px 0; min-width: 0; }
.event .genre { color: var(--accent); font-size: 12px; }
/* A venue is text only — no flyer to lead with — so it is a shorter row, its
   kind marked by a coloured dot rather than an image. */
.place .body { padding: 10px 12px; min-width: 0; }

/* The expanded detail. Empty until a row is chosen, and collapsed entirely when
   empty so an unselected row is exactly as tall as it was. It spans the full
   width of the card rather than the text column, so an event's block is not
   squeezed into the space beside its flyer. */
/* The keep control. Sits in the corner of whatever holds it — a row, a popup —
   and is the one thing in a row that is not the row's own click target. */
.save {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  width: 34px; height: 34px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer;
  color: var(--muted);
}
.save:hover, .save[aria-pressed="true"] { color: var(--favourite); }

/* In a popup the mark sits in the corner on its own, where the close button used
   to be — and on an event that corner is over the flyer, so it has to hold
   against whatever the artwork happens to be doing there. It carries its own
   ground for that reason; in a list row it does not need one. */
.pop .save {
  top: 8px; right: 8px; width: 30px; height: 30px;
  border-radius: 50%;
  background: var(--chrome);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  border: 1px solid var(--line);
}
.save:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.save-i { width: 17px; height: 17px; flex: none; }
/* Both the row and the popup need to be a positioning context for it. */
.event, .place, .pop { position: relative; }
/* Clear of the star, so a long title does not run under it. */
.event .title, .place .title, .pop > strong { padding-right: 30px; }
/* Except inside an expanded row, where the popup's own title is hidden and its
   control would sit on top of the row's. */
.detail .save { display: none; }

.detail:empty { display: none; }
.detail { grid-column: 1 / -1; border-top: 1px solid var(--line); }
/* The popup block reused as-is, minus the chrome it does not need here: no
   rounded card of its own, and the title is already the row above it. */
.detail .pop { padding: 10px 12px 12px; cursor: default; }
.detail .pop > strong { display: none; }
.detail .pop-img { display: none; }
.title { font-weight: 600; letter-spacing: -0.01em; }
.meta { color: var(--muted); font-size: 13px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The dot keyed to a venue's kind, in the row and its popup. */
.swatch { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 6px; vertical-align: middle; }
/* The chosen row — its pin open on the map, or stepped to with the arrow keys.
   The accent ring ties the row to the pin pointing at it; choosing another
   simply moves it, and choosing the same one again clears it. */
.event.selected, .place.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

/* A quiet line under the list: a provider down, or that venues are only counted
   until the map zooms in. Not itself a row, so no card chrome. */
.list-note { list-style: none; color: var(--muted); font-size: 13px;
  text-align: center; padding: 14px 8px; }

/* Map popups, on-theme rather than Leaflet's default white card. A ticket popup
   leads with a thumbnail and offers a buy link; a venue popup is text. */
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
  background: var(--card); color: var(--text); border: 1px solid var(--line);
}
.leaflet-popup-content { margin: 0; }

/* The tip tracks the marker even when the popup has slid sideways to clear a
   screen edge, so it still points at the thing it describes. The shift is set
   on the popup by positionPopup; custom properties inherit, so the tip reads it
   from there. */
.leaflet-popup-tip-container { transform: translateX(var(--tip-shift, 0px)); }

/* Flipped under its marker, when there is no room above. The tip moves to the
   top and turns over — the container clips a rotated square to a triangle, so
   rotating the container is what points it the other way. */
.leaflet-popup.below .leaflet-popup-tip-container {
  top: -19px;
  transform: translateX(var(--tip-shift, 0px)) rotate(180deg);
}
.leaflet-popup-content a { color: var(--accent); }
.pop { padding: 10px 12px; min-width: 160px; max-width: 240px; }
.pop strong { display: block; letter-spacing: -0.01em; }
.pop-img { display: block; width: calc(100% + 24px); margin: -10px -12px 8px;
  height: 120px; object-fit: cover; background: var(--line); }
.pop-sub { color: var(--muted); font-size: 12px; margin-top: 3px; }
.pop-buy { display: inline-block; margin-top: 8px; font-size: 13px; font-weight: 600; }
/* The ticket link is a glyph rather than the word "Tickets", so it needs nothing
   translated. No arrow either: the ticket says what pressing it is for, and an
   arrow beside it was saying that a link is a link. Sized up from the body text
   because it is the one thing in a popup you are meant to press. */
.pop-buy-i { display: inline-flex; align-items: center; font-size: 17px; }
.pop-buy-i .stat-i { width: 1.15em; height: 1.15em; vertical-align: 0; }
/* A venue can offer both a site and a number, so they sit in a row that wraps
   rather than stacking two full-width links. */
.pop-links { display: flex; flex-wrap: wrap; gap: 12px; }

.empty { color: var(--muted); text-align: center; padding: 40px 0; }

