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

/* The look Dean was actually pointing at: freeparking's own trick, not a
   different CARTO style. Voyager's tiles are inverted and hue-rotated back
   into place, which is why a richly-coloured basemap was switched to above
   rather than kept on dark_all — an already-dark style has far less colour to
   invert into something worth looking at. Checked against a real tile with the
   filter applied before this went anywhere near the deployed page, rather than
   guessed at from the numbers alone. */
.leaflet-tile-pane {
  filter: invert(1) hue-rotate(180deg) brightness(0.95) contrast(0.9) saturate(0.7);
}

/* iOS 26 lays out an installed web app as if the screen were a status bar's
   height shorter — WebKit bug 301108, "viewport does not work correctly for
   viewport-fit=cover", open with no fix. `inset: 0` on a fixed element then
   stops short of the real bottom edge on an iPhone 17, though the same rule is
   correct on older iOS and in an ordinary browser tab. freeparking hit this
   first: every viewport measure reads short except 100lvh, so standalone mode
   takes its height from that explicitly rather than from the inset.
   Browser tabs are untouched — this is scoped to the installed app only. */
@media (display-mode: standalone) {
  #map { bottom: auto; height: 100lvh; }
}
@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); }
/* The header is spacing, not a caption.
   
   It used to carry the count and the reach — "7 · 672 · 1.7 mi · 34 min" — and
   on a laptop that was already hidden, since the same figures sit over the map's
   filter bar a few inches away and repeating them left a bare number adrift.
   The phone kept it on the grounds that the panel covers the map and takes the
   count with it, so there it was the only copy.
   
   It goes on the phone too. What the line answers is "how far does this view
   reach", which is a question about the *map* — and the panel is what you open
   when you have stopped looking at the map and started reading the list. The
   walk time to the edge of a view you cannot see is not worth a line above the
   thing you came to read.
   
   The element stays: its top padding is what holds the first card clear of the
   list button floating over it. */
.panel-head > * { display: none; }
@media (min-width: 641px) { .panel-head { min-height: 0; } }

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

/* What the promoter wrote, on the card rather than only in the popup it opens.
   
   Clamped to two lines. The panel is a narrow column and the list is scanned
   rather than read, so a full blurb on every card would cost more rows than it
   is worth — two lines is enough to tell one club night from another, which is
   the job here. The whole of it is still in the popup a tap away.

   Only about half the rows have one, since Ticketmaster sends no prose in a
   search response, so this has to sit comfortably absent as well as present. */
.event .card-summary {
  margin-top: 3px;
  color: var(--muted); font-size: 12px; line-height: 1.45;
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}

/* The flyer is a fixed square, so a card that grows past it leaves the image
   short and a band of ground beside it. Stretching it to whatever the row
   becomes keeps the two columns the same height however much text there is. */
.event img, .event .no-image { height: 100%; min-height: 96px; }
/* 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, .detail .thumb { 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; }

/* What the promoter wrote. Sits below the genre line and above the buy control,
   which is the order the question is asked in: what is it, what sort, is it
   worth reading on, then how to get in.
   In a map popup it is clamped to a few lines, because the popup floats over the
   map and a long blurb would cover what it is anchored to. The expanded row
   lifts that clamp below — there the panel scrolls, so length costs nothing. */
.pop-summary {
  color: var(--text); font-size: 12.5px; line-height: 1.45; margin-top: 7px;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4;
  overflow: hidden;
  /* Promoter copy arrives with long unbroken strings — hashtags, URLs, run-on
     names — which would otherwise push the popup wider than its max-width. */
  overflow-wrap: anywhere;
}
.detail .pop-summary { -webkit-line-clamp: unset; display: block; font-size: 13px; }
/* Colour set explicitly rather than left to the anchor default, because a
   button-based control — the share glyph — is not an <a> and gets no colour
   from anywhere otherwise, and read visibly duller than its neighbours for it.
   One rule now covers every pop-buy alike, anchor or button. */
.pop-buy { display: inline-block; margin-top: 8px; font-size: 13px; font-weight: 600; color: var(--accent); }
/* 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; }
/* The play button sits beside the ticket and is drawn the same size, so the two
   read as a pair. It carries `hidden` until Deezer has answered, and `.pop-buy-i`
   sets a display, which would otherwise win over the attribute. */
.pop-buy[hidden] { display: none; }
.pop-play { margin-left: 12px; background: none; border: 0; padding: 0;
            color: inherit; cursor: pointer; font: inherit; }
/* The artwork holds the play button, so it needs to be something a button can be
   positioned against. It stands in for the image in the card's grid, which is
   why it carries no size of its own — the image rules above still apply. */
.thumb { position: relative; display: block; }

/* Centred, and drawn on nothing: a filled triangle with a shadow reads on both a
   dark flyer and a pale one without a plate behind it competing with the art. */
.play-over { position: absolute; inset: 0; display: grid; place-items: center;
             background: none; border: 0; padding: 0; cursor: pointer;
             color: #fff; filter: drop-shadow(0 1px 3px rgba(0,0,0,.65)); }
.play-over[hidden] { display: none; }
.play-over .stat-i { width: 32px; height: 32px; }
.play-over:hover { transform: scale(1.06); }

/* One row, everywhere the listing appears. It spans both grid columns, the way
   .detail does below it, rather than being auto-placed under the thumbnail —
   that was the actual cause of it sitting cramped in the corner on a phone,
   not merely a missing margin. */
.action-row {
  grid-column: 1 / -1; display: flex; gap: 12px; align-items: center;
  padding: 0 12px 12px;
}
.action-row .pop-buy { margin-top: 0; }

/* Tickets is the one entry that is not a search — it is the thing the listing
   is for — so it is a filled pill in the accent rather than a bare glyph like
   its neighbours, and reads as the obvious next tap rather than one icon among
   several. */
.action-row .act-buy {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--accent); color: var(--on-accent);
  padding: 6px 12px; border-radius: 999px; font-size: 15px;
}
.action-row .act-buy .stat-i { width: 1em; height: 1em; }

/* The searches sit apart from the ticket pill, dimmed, as the second thing to
   reach for rather than competing with it. */
.action-row .pop-buy:not(.act-buy) { opacity: .6; }
.action-row .pop-buy:not(.act-buy):hover { opacity: 1; }
/* 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; }

