/* ═══════════════════════════════════════════════════════════
   CSS VARIABLES & RESET
═══════════════════════════════════════════════════════════ */
:root {
  --bg:            #060a06;
  --bg2:           #0b120b;
  --bg3:           #0f1a0f;
  --bg-overlay:    rgba(6,10,6,0.75);
  --accent:        #39ff14;
  --accent-dim:    #1a7a08;
  --accent-rgb:    57,255,20;
  --accent-glow:   rgba(57,255,20,0.18);
  --accent-glow2:  rgba(57,255,20,0.06);
  --amber:         #ffb700;
  --amber-rgb:     255,183,0;
  --red:           #ff3c3c;
  /* Peer identity — marker core, ring, hover glow, card border and flash.
     Semantic, not chromatic: it is whatever colour reads as "another user"
     against the mode's own background, exactly as --accent is per-mode for
     "self". Paired dim/full exactly like --accent-dim/--accent: the dim tone
     carries the lesser state (card hover), the full tone the open state.
     Dark = the standard bright amber (11.4:1 on --bg). */
  --peer-rgb:      255,183,0;
  --peer-dim-rgb:  122,87,0;
  --text:          #b8e8b0;
  --text-dim:      #527a4a;
  --border:        #1e3a1a;
  --border-bright: #2d5c27;
  --radius:        4px;
  /* The radar heartbeat. One period for every ambient radar rhythm, so they beat
     together instead of against each other: the marker pulse (ring-expand) below,
     and the SpreadZoom cascade, which reads this value via getComputedStyle exactly
     as the map layers read --accent. CSS owns it because it is a static design
     value, not a runtime one — unlike --map-pitch and --toast-bottom, which JS
     writes. Deliberately NOT derived from CONFIG.pollMs: that is a shared transport
     rate, and a viewing-comfort value must not follow a network constant.
     Seconds or milliseconds are both parsed on the JS side; keep it in seconds to
     match the rest of this file. Not themed — timing is not a colour. */
  --radar-pulse:   2s;
}

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

html, body {
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  overflow-x: hidden;
}

/* Noise grain overlay */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E");
  pointer-events: none;
  z-index: 9998;
  opacity: 0.35;
}

/* ═══════════════════════════════════════════════════════════
   LAYOUT
═══════════════════════════════════════════════════════════ */
.app {
  display: grid;
  grid-template-rows: 1fr auto;
  min-height: 100vh;
  max-width: 1400px;
  margin: 0 auto;
  padding: 0 16px;
}

/* ═══════════════════════════════════════════════════════════
   LOGO — sits in .form-topbar above the Signal panel, paired
   with the mode toggle (see MODE TOGGLE SWITCH further down)
═══════════════════════════════════════════════════════════ */
.form-topbar {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.logo {
  display: flex;
  align-items: center;
  gap: 12px;
}

.logo-img {
  height: 48px;
  width: auto;
  filter: brightness(0) saturate(100%) invert(79%) sepia(53%) saturate(1471%) hue-rotate(50deg) brightness(102%) contrast(101%);
}

.logo-text-wrap {
  display: flex;
  flex-direction: column;
  gap: 3px;
  justify-content: center;
  height: 42px;
}

.logo-text {
  font-family: 'Orbitron', sans-serif;
  font-weight: 900;
  font-size: 20px;
  letter-spacing: 0.15em;
  color: var(--accent);
  text-shadow:
    0 0 8px var(--accent),
    0 0 24px rgba(var(--accent-rgb),0.4);
  line-height: 1.1;
}

.logo-sub {
  font-size: 11px;
  color: var(--text-dim);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  line-height: 1.1;
}

.status-pill {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  color: var(--text-dim);
  letter-spacing: 0.12em;
}

.status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent-dim);
  flex-shrink: 0;
  position: relative;  /* contains the absolutely-positioned ring pseudo-elements */
}
.status-dot.live {
  background: var(--accent);
  box-shadow: 0 0 6px var(--accent);
}
/* Single radar pulse ring — transparent circle with box-shadow ring outline.
   Bursts from dot centre (scale 0), expands to scale(3), fades by 76%,
   holds invisible until loop restarts cleanly. Dot stays constant at all times. */
.status-dot.live::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: transparent;
  box-shadow: 0 0 0 2px rgba(var(--accent-rgb), 0.7);
  animation: status-ring 2.5s ease-out infinite;
  pointer-events: none;
}
.status-dot.warn { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
.status-dot.off  { background: var(--text-dim); }

@keyframes status-ring {
  0%   { transform: scale(0); opacity: 1; }
  76%  { transform: scale(3); opacity: 0; }
  100% { transform: scale(3); opacity: 0; }
}

#clock {
  /* Monospace so every digit is the same width — HH:MM:SS is therefore a
     constant width and the per-second update can't reflow the row / nudge the
     LIVE·GPS pills. Uses the app's base mono face (matches the adjacent pills). */
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  color: var(--text-dim);
  letter-spacing: 0.1em;
}

/* ═══════════════════════════════════════════════════════════
   FOOTER
═══════════════════════════════════════════════════════════ */
.footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px 20px;
  padding: 16px 0 20px;
  border-top: 1px solid var(--border);
  font-size: 11px;
  color: var(--text-dim);
  letter-spacing: 0.05em;
}

.footer a {
  color: var(--text-dim);
  text-decoration: none;
  letter-spacing: 0.05em;
}

.footer a:hover {
  color: var(--accent);
}

.footer-copy {
  width: 100%;
  text-align: center;
  opacity: 0.5;
  margin-top: 4px;
  font-size: 10px;
}

/* ═══════════════════════════════════════════════════════════
   MAIN GRID — form (260px) | map (flex) | cards (260px)
═══════════════════════════════════════════════════════════ */
.main {
  display: grid;
  grid-template-columns: 260px 1fr 260px;
  grid-template-areas: "form map cards";
  gap: 20px;
  padding: 20px 0;
  align-items: start;
  min-height: 0;
}

/* ── COL-MAP (centre column) ── */
.col-map {
  grid-area: map;
  display: flex;
  flex-direction: column;
  gap: 14px;
  position: sticky;
  top: 20px;
}

/* System status row — right-aligned above map */
.map-status-bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 20px;
}

/* Map display container — rectangular, fills available space */
.map-wrap {
  position: relative;
  width: 100%;
  height: calc(100vh - 180px);
  min-height: 400px;
  border: 1px solid var(--border-bright);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow:
    0 0 0 6px var(--bg2),
    0 0 0 7px var(--border),
    0 0 60px rgba(var(--accent-rgb),0.08),
    0 0 120px rgba(var(--accent-rgb),0.04);
}

/* Fullscreen state — #fullscreenBtn (see app.js) targets .col-map rather
   than .map-wrap alone, so the status bar, map, position strip, and OSM
   attribution all stay visible together instead of just the raw canvas.
   .col-map is already a flex column (see rule above); here the map simply
   grows to fill the space left over by its siblings. */
.col-map:fullscreen,
.col-map:-webkit-full-screen {
  position: static;
  width: 100vw;
  height: 100vh;
  max-width: none;
  padding: 16px;
  box-sizing: border-box;
  background: var(--bg);
}

.col-map:fullscreen .map-wrap,
.col-map:-webkit-full-screen .map-wrap {
  flex: 1;
  height: auto;
  min-height: 0;
}

/* Go Offline button, reparented into #colMap while fullscreen (see the
   fullscreenchange handler in app.js). Pinned to the top so it stays visible
   even if the fullscreen content ever exceeds the viewport height. Mirrors the
   position:sticky idiom .col-map itself uses in the normal desktop layout. */
.col-map:fullscreen .btn-leave,
.col-map:-webkit-full-screen .btn-leave {
  position: sticky;
  top: 0;
  z-index: 20;
}

/* MapLibre fills the wrap */
#mapContainer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.maplibregl-map {
  width: 100% !important;
  height: 100% !important;
}

/* Hide all MapLibre default controls — attribution handled via static HTML */
.maplibregl-ctrl-logo,
.maplibregl-compact,
.maplibregl-ctrl-attrib,
.maplibregl-ctrl-bottom-right {
  display: none !important;
}

/* Custom map controls overlay */
.map-controls {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 10;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.map-ctrl-btn {
  width: 32px;
  height: 32px;
  background: var(--bg2);
  border: 1px solid var(--border-bright);
  border-radius: var(--radius);
  color: var(--accent);
  font-size: 16px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s;
  font-family: 'Share Tech Mono', monospace;
  /* Render symbol glyphs (⛶ fullscreen, ♫ music toggle, etc.) as monochrome
     text so they inherit `color` rather than falling back to a color-emoji
     font. Paired with a U+FE0E variation selector on the emoji-capable glyphs
     for browsers that don't yet support font-variant-emoji (e.g. Safari). */
  font-variant-emoji: text;
}

/* Hover only on true-hover devices; excluded when button is already active */
@media (hover: hover) {
  .map-ctrl-btn:hover:not(.active) {
    background: var(--bg3);
    border-color: var(--accent);
    color: var(--accent);
  }
}

/* Remove focus ring after mouse/touch click; preserve for keyboard navigation */
.map-ctrl-btn:focus         { outline: none; }
.map-ctrl-btn:focus-visible { outline: 1px solid var(--border-bright); }

/* Active (toggle-on) state — applies to all map control toggle buttons */
.map-ctrl-btn.active {
  background: var(--accent);
  color: var(--bg);
  box-shadow: 0 0 10px var(--accent-glow);
}

#centerBtn     { font-size: 13px; }
#tiltBtn       { font-size: 13px; }
#spreadBtn     { font-size: 14px; }
#fullscreenBtn { font-size: 13px; }

/* Map corner branding overlay */
.map-overlay-label {
  position: absolute;
  bottom: 8px;
  left: 10px;
  z-index: 10;
  font-family: 'Orbitron', sans-serif;
  font-size: 8px;
  letter-spacing: 0.25em;
  color: rgba(var(--accent-rgb),0.35);
  pointer-events: none;
  text-transform: uppercase;
}

/* Logo overlay — top-left corner of the map, shown at all times (in and out
   of fullscreen, which is why the logo need not be reparented into #colMap).
   Mirrors the .map-overlay-label / .map-controls overlay pattern: absolutely
   positioned, decorative (ignores pointer events). Reuses .logo-img so it
   inherits the same dark/light theme tint; only the size is overridden here. */
.map-logo-overlay {
  position: absolute;
  top: 10px;
  left: 10px;
  /* z-index:auto — the overlay is moved into MapLibre's canvas container at map
     load (see app.js). There it layers by DOM order: above the tile canvas but
     below markers and popups (which MapLibre appends afterwards / above), so the
     logo never covers avatars, user markers, or popups. */
  z-index: auto;
  pointer-events: none;
  overflow: hidden;
}
.map-logo-overlay .logo-img {
  height: 26px;
  display: block;
}
/* OFFLINE state: the logo is reparented back into #mapWrap (see positionMapLogo
   in app.js). Lift it above the waiting overlay (z-index:5) but keep it below
   the map controls (z-index:10) so it stays visible while not transmitting. */
.map-logo-overlay.logo-offline {
  z-index: 6;
}

/* ── Map-logo sheen ──────────────────────────────────────────
   A highlight swept across the logo shape. The ::after is masked by the logo
   PNG so the shine only touches the logo's pixels (not its bounding box), and
   the logo <img> itself is never masked. It runs ONLY while transmitting
   (body.live): a slow ambient pass ~every 9s, and a quicker pass on hover for
   pointer devices. Honors reduced-motion. Sweeps LEFT → RIGHT (the highlight
   enters from the left and exits to the right). */
.map-logo-overlay::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(105deg, transparent 38%, rgba(255,255,255,0.75) 50%, transparent 62%);
  background-repeat: no-repeat;
  background-size: 250% 100%;
  background-position: 160% 0;   /* resting: highlight parked off the left */
  -webkit-mask-image: url("/img/opin2-tag.png");
          mask-image: url("/img/opin2-tag.png");
  -webkit-mask-size: contain;     mask-size: contain;
  -webkit-mask-position: center;  mask-position: center;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
body.live .map-logo-overlay::after {
  opacity: 1;
  animation: logo-sheen 9s linear infinite;
}
@keyframes logo-sheen {
  0%   { background-position: 160% 0; }   /* highlight off the left, ready to enter */
  22%  { background-position: -60% 0; }   /* ~2s left→right sweep across the logo */
  100% { background-position: -60% 0; }   /* ~7s idle, highlight parked off the right */
}
@media (hover: hover) {
  body.live .map-logo-overlay { pointer-events: auto; } /* enable rollover on pointer devices */
  body.live .map-logo-overlay:hover::after { animation: logo-sheen-hover 0.8s linear; } /* single sweep per hover (re-entering :hover re-triggers) */
}
@keyframes logo-sheen-hover {
  0%   { background-position: 160% 0; }
  100% { background-position: -60% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .map-logo-overlay::after { animation: none !important; opacity: 0; }
}

/* Waiting for GPS overlay */
.map-waiting-overlay {
  position: absolute;
  inset: 0;
  z-index: 5;
  background: var(--bg-overlay);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  pointer-events: none;
}

.map-waiting-overlay.hidden { display: none; }

.map-waiting-icon {
  font-size: 48px;
  opacity: 0.25;
  animation: locked-pulse 3s ease-in-out infinite;
}

.map-waiting-text {
  font-family: 'Orbitron', sans-serif;
  font-size: 11px;
  letter-spacing: 0.3em;
  color: var(--text-dim);
  text-transform: uppercase;
}

.map-waiting-sub {
  font-size: 10px;
  color: var(--text-dim);
  opacity: 0.6;
  letter-spacing: 0.1em;
}

/* ── CUSTOM MAP MARKERS ── */

/* Self marker */
.marker-self {
  cursor: pointer;
}

.marker-self:hover,
.marker-self.marker-popup-open,
.marker-self.marker-hover {
  filter: drop-shadow(0 2px 12px rgba(var(--accent-rgb),0.8));
}

/* Emoji variant — zero-size anchor at the geographic coordinate;
   emoji and ring are positioned absolutely, centred on this point */
.marker-self--emoji {
  width: 0;
  height: 0;
  position: relative;
}

/* Inner wrapper — provides sizing and positioning context for ring and core */
.marker-self-inner {
  width: 18px;
  height: 18px;
  position: relative;
}

/* Steady dot with a single crisp glow. The pulse is carried solely by the
   expanding ring below (canonical location-dot pattern), so the dot itself does
   not animate. */
.marker-self-core {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 6px var(--accent);
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

/* Ring hugs the 12px dot exactly (18px inner − 3px inset each side = 12px) and
   expands from the dot's edge outward (edge-start), leaving transform free for the
   scale animation. */
/* Pulse ring — v2 technique: a transparent, dot-sized element with the ring drawn
   as a box-shadow OUTSIDE it (never a filled circle). inset:3px sizes it to the
   12px dot; the shadow sits just outside, so it emanates from the dot's edge.
   Kept thin (2.4px) and light so dot and emoji read as the same delicate ripple. */
.marker-self-ring {
  border-radius: 50%;
  background: transparent;
  box-shadow: 0 0 0 2.4px rgba(var(--accent-rgb),0.7);
  position: absolute;
  inset: 3px;
  animation: ring-expand var(--radar-pulse) ease-out infinite;
}

/* rotateX(--map-pitch) lays the ring onto the ground plane so it tilts with the map,
   matching the range circle and the SpreadZoom rings — all three are concentric
   ground geometry around the same point, and one of them ignoring the ground plane
   reads as a bug once the map is pitched. --map-pitch is written by the map's 'pitch'
   handler in app.js and is 0deg on a flat map, so the untilted rendering is byte-for-
   byte what it was before.
   The icon deliberately does NOT tilt: MapLibre's own pitchAlignment:'map' would
   rotate the whole marker element, laying the emoji/dot face-down. Markers are
   billboard labels and face the camera in every 3D map; only the ground ring turns.
   Hence the rotation lives here, on the ring child, rather than on the Marker.
   Bearing is deliberately absent: pitch rotates about the screen's horizontal axis
   while bearing rotates within the ground plane, and a circle is rotationally
   symmetric — so a ground circle projects to an ellipse with a horizontal major axis
   at any bearing. rotateX alone is complete; no rotate/bearing listener is needed.
   Orthographic (no perspective), which is exactly what MapLibre's own marker
   pitchAlignment does, so the ring agrees with the library rather than diverging. */
@keyframes ring-expand {
  0%   { transform: rotateX(var(--map-pitch, 0deg)) scale(1); opacity: 1; }
  70%  { transform: rotateX(var(--map-pitch, 0deg)) scale(3); opacity: 0; }
  100% { transform: rotateX(var(--map-pitch, 0deg)) scale(3); opacity: 0; }
}

/* Radar FX off (Interactions → Radar FX). The attribute is absent by default, so
   the animated state above is the default and this rule is purely subtractive —
   the same shape as data-mode, where absent means the :root theme applies.
   Specificity 0,2,0 beats the 0,1,0 rules above, so no !important is needed.
   Only the animation stops: the ring keeps its box-shadow and stays on screen at
   rest, because the ring is a rendering of presence, not the presence itself.
   Deliberately not a @media (prefers-reduced-motion) block — that preference sets
   the checkbox's initial value in app.js instead, so there is one source of truth
   and the checkbox can never disagree with what is on screen. Safe because without
   JS there are no markers, so there are no rings to animate.
   .status-dot.live's status-ring is intentionally not gated: it reports the
   connection, not the radar, and lives in the sidebar rather than on the map. */
[data-radar-pulse="off"] .marker-self-ring,
[data-radar-pulse="off"] .marker-user-ring {
  animation: none;
  /* animation:none drops the keyframes, and with them the rotateX above — so the
     resting ring has to carry the tilt itself or it would sit flat on a pitched map.
     Only the motion is off; the ring is still ground geometry. */
  transform: rotateX(var(--map-pitch, 0deg));
}

/* Other user marker — pulsing dot style, mirrors self marker */
.marker-user {
  cursor: pointer;
}

.marker-user:hover,
.marker-user.marker-popup-open,
.marker-user.marker-hover {
  filter: drop-shadow(0 2px 12px rgba(var(--peer-rgb),0.8));
}

/* Emoji variant — zero-size anchor at the geographic coordinate;
   emoji and ring are positioned absolutely, centred on this point */
.marker-user--emoji {
  width: 0;
  height: 0;
  position: relative;
}

/* Inner wrapper — provides sizing and positioning context for ring and core */
.marker-user-inner {
  width: 18px;
  height: 18px;
  position: relative;
}

.marker-user-core {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: rgb(var(--peer-rgb));
  box-shadow: 0 0 6px rgb(var(--peer-rgb));
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.marker-user-ring {
  border-radius: 50%;
  background: transparent;
  box-shadow: 0 0 0 2.4px rgba(var(--peer-rgb),0.7);
  position: absolute;
  inset: 3px;
  animation: ring-expand var(--radar-pulse) ease-out infinite;
}

/* Emoji container overrides — parent is 0×0 so inset produces no area; explicit
   size and negative offsets centre the ring on the anchor point. Sized to the 28px
   glyph so the box-shadow ring emanates from the emoji's edge, matching the dot.
   The ring's colour and 2.4px thickness are inherited from the base rules above, so
   both icons keep the same light ripple weight. */
.marker-self--emoji .marker-self-ring,
.marker-user--emoji .marker-user-ring {
  width: 28px;
  height: 28px;
  inset: auto;
  left: -14px;
  top: -14px;
}

/* Emoji avatar marker — absolutely positioned and centred on the 0×0 anchor point */
.marker-emoji {
  font-size: 28px;
  line-height: 1;
  display: block;
  user-select: none;
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-50%, -50%);
  transform-origin: center bottom;
}

/* Squash-and-stretch — fires on open (click handlers) and close (popup.on close) */
.emoji-squash {
  animation: emoji-squash 0.6s ease-out forwards;
}

@keyframes emoji-squash {
  0%   { transform: translate(-50%, -50%) scale(1,    1);    }
  20%  { transform: translate(-50%, -50%) scale(1.3,  0.6);  }
  40%  { transform: translate(-50%, -50%) scale(0.95, 1.1);  }
  60%  { transform: translate(-50%, -50%) scale(1.1,  0.9);  }
  80%  { transform: translate(-50%, -50%) scale(0.98, 1.02); }
  100% { transform: translate(-50%, -50%) scale(1,    1);    }
}

/* ── MAP POPUP — ONE-LINER COMPACT ── */
.maplibregl-popup-content {
  background: var(--bg2) !important;
  border: 1px solid var(--border-bright) !important;
  border-radius: var(--radius) !important;
  box-shadow: 0 4px 20px rgba(0,0,0,0.7) !important;
  padding: 0 !important;
  overflow: hidden !important;
  font-family: 'Share Tech Mono', monospace !important;
}

.maplibregl-popup-tip {
  border-top-color: var(--border-bright) !important;
  border-bottom-color: var(--border-bright) !important;
}

.liner {
  width: 100%;
}

.liner-r1 {
  display: flex;
  align-items: center;
  position: relative;
  padding: 6px 44px 4px 6px;
  border-bottom: 1px solid var(--border);
}

.liner-head-titles {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1;
}

.liner-eyebrow {
  font-size: 7px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 1px;
}

.liner-topic {
  font-family: 'Orbitron', sans-serif;
  font-size: 11px;
  font-weight: 700;
  color: var(--accent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.liner-close {
  position: absolute;
  top: 6px;   /* mirrors r1's padding-top — same spacing as OPEN TO from top edge */
  right: 6px; /* mirrors r1's padding-left — same spacing as OPEN TO from side edge */
  min-width: 44px;
  min-height: 0;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  color: var(--text-dim);
  font-family: sans-serif;
  font-size: 14px;
  line-height: 1;
  transition: color 0.15s;
}

.liner-close:hover { color: var(--accent); }

.liner-r2 {
  font-size: 8px;
  color: var(--text-dim);
  padding: 2px 6px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-style: italic;
  line-height: 1;
}

.liner-r3 {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 3px 6px 2px 6px;
  font-size: 9px;
  flex-wrap: nowrap;
  border-bottom: 1px solid var(--border);
  line-height: 1;
}

.liner-sep  { color: var(--border-bright); }
.liner-ag   { color: var(--text); white-space: nowrap; }
.liner-dist { color: var(--amber); white-space: nowrap; }

.liner-dir {
  display: flex;
  align-items: center;
  gap: 2px;
  color: var(--text);
  white-space: nowrap;
}

.liner-arr { font-size: 10px; color: var(--accent); display: inline-block; }
.liner-dur { color: var(--text-dim); white-space: nowrap; font-size: 8px; }
.liner-r4  { padding: 2px 6px 3px 6px; font-size: 11px; line-height: 1.4; }

/* Ad slot inside popup liner */
.liner-r4 ins        { display: block; margin: 0; line-height: 1.4; }
.liner-r4 ins a      { color: var(--accent); }
.liner-r4 ins iframe { display: block; }

/* Card panel ad slot — preserved */
.card-static ins a      { color: var(--accent); }
.card-static ins iframe { display: block; }

/* ── SHARED INTERACTION CONTROLS (Buzz + Gift) ──
   Every interaction renders the same skeleton — an emoji "chip" (a small bordered
   box) that IS the control: it shows the emoji at rest and the status glyph during
   a send. For Gift the chip holds a <select> (emoji picker) plus a status span,
   shown mutually exclusively; for Buzz it's a clickable box holding 🐝. Off /
   disabled / strikethrough live on generic classes (.lr-off / .lr-self / .lr-cooling
   on the .lr-ctrl container; strike targets .lr-emoji-chip), so any future
   interaction inherits the exact same states for free. */

/* Action rows — 50/50 in both popup and card, so the two chips line up. */
/* Chips sit adjacent, left-packed. The old 1fr 1fr grid existed to give the former
   [chip][label] pairs equal halves; with the labels gone (Phase 2) the chips are
   small fixed-width boxes, so a flex row with a tight gap places them next to each
   other with no dead space stretching each into a half-column. */
.liner-actions,
.user-card .card-actions {
  display: flex;
  gap: 6px;
  align-items: center;
}
.liner-actions {
  padding: 2px 6px;
  border-bottom: 1px solid var(--border);
}
.user-card .card-actions { margin-bottom: 4px; }

/* The control: [chip][label] on one row. Not itself clickable — the label button
   is the send trigger; the chip is emoji display (Buzz) or the picker (Gift).
   align-items:flex-start so that when a label wraps to 2–3 lines the chip stays
   aligned to the FIRST line (both Buzz and Gift), rather than centering. */
.lr-ctrl {
  display: flex;
  align-items: center;
  min-width: 0;
}

/* Emoji chip — the shared bordered box. A fixed width (--chip-w) is shared by the
   Buzz chip and the Gift chip so both read identically in the 50/50 actions row.
   The chip now IS the control (the separate label is gone): it shows the emoji at
   rest and the status glyph (➤/✓/✕/countdown) during a send. */
.lr-emoji-chip {
  --chip-w: 2.9em;
  flex: 0 0 auto;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--chip-w);
  height: 1.5em;
  padding: 0 3px;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-variant-emoji: emoji;
  font-size: 12px;
  line-height: 1;
  color: var(--text);
}

/* ── Gift chip — [ <select> | status span ], mutually exclusive ──────────────
   The chip is a positioning context holding both children; exactly one shows at a
   time. Default: the select is visible, the status span is hidden. During a status
   frame .lr-gift-showing-status flips them, so a status glyph NEVER renders over an
   emoji — the select is display:none and absent from the box. */
.lr-chip-gift { position: relative; padding: 0; }

.lr-gift-select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  border: none;
  background: transparent;
  /* Center the emoji both axes. text-align-last handles horizontal; a line-height
     equal to the chip height (1.5em) centers it vertically, replacing the native
     select's baseline metrics that rendered it 1-2px bottom-heavy. Now the spacing
     above and below the emoji is symmetric, matching the Buzz span. */
  text-align: center;
  text-align-last: center;
  line-height: 1.5em;
  padding: 0 14px 0 6px;
  color: var(--text);
  font: inherit;
  cursor: pointer;
  /* Custom caret (native arrow width/position varies and clips the emoji on some
     mobile engines). */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='6' viewBox='0 0 8 6'%3E%3Cpath d='M1 1l3 3 3-3' stroke='%23527a4a' stroke-width='1.4' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 4px center;
}
[data-mode="light"] .lr-gift-select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='6' viewBox='0 0 8 6'%3E%3Cpath d='M1 1l3 3 3-3' stroke='%236d6a85' stroke-width='1.4' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.lr-gift-select:focus         { outline: none; }
.lr-gift-select:focus-visible { outline: 1px solid var(--border-bright); }
.lr-ctrl:not(.lr-off) .lr-gift-select:disabled { cursor: not-allowed; opacity: 0.6; }

/* Status face — hidden by default; shown (and the select hidden) during a frame. */
.lr-gift-status { display: none; align-items: center; justify-content: center; width: 100%; }
.lr-ctrl.lr-gift-showing-status .lr-gift-select { display: none; }
.lr-ctrl.lr-gift-showing-status .lr-gift-status { display: inline-flex; }

/* Status parity with the party FAB (.partyfetti-fab.cooling): while a chip shows
   status (➤/✓/✕/countdown) it renders exactly as the FAB's status does — the text
   font (not the color-emoji font of the resting 🐝/🎁), tabular figures so the
   countdown width stays steady (20→9→8), the accent colour (the app's active-state
   colour, which the chips' own hover state also uses), and 11px to match the FAB's
   cooling size. Gift's status lives in the dedicated .lr-gift-status span; Buzz's
   chip carries .lr-showing-status while its swapped content is a status glyph. */
.lr-gift-status,
.lr-ctrl.lr-showing-status .lr-chip-btn {
  font-variant-emoji: normal;
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--accent);
}

/* ── Buzz chip — the clickable action itself ─────────────────────────────────
   Accent-tinted border on hover, a slight press on active — matching the site's
   other interactive controls (.map-ctrl-btn). */
.lr-chip-btn { cursor: pointer; transition: border-color 0.15s, color 0.15s; }
.lr-ctrl:not(.lr-off):not(.lr-cooling) .lr-chip-btn:hover  { border-color: var(--accent); color: var(--accent); }
.lr-ctrl:not(.lr-off):not(.lr-cooling) .lr-chip-btn:active { transform: translateY(0.5px); }
.lr-chip-btn:focus         { outline: none; }
.lr-chip-btn:focus-visible { outline: 1px solid var(--border-bright); }

/* ── Off state — self, viewer-off, or target-off ─────────────────────────────
   Dim + strike the chip (there is no separate label now), and block clicks on the
   whole control. */
.lr-ctrl.lr-off { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
.lr-ctrl.lr-off .lr-emoji-chip { text-decoration: line-through; }
/* Self override — can't action yourself; not an "off" toggle, so no strike. */
.lr-ctrl.lr-self .lr-emoji-chip { text-decoration: none; }

/* ── Mid-cooldown ────────────────────────────────────────────────────────────
   Dim the chip and make it fully inert — pointer-events:none so the Buzz chip does
   not react to clicks/press at all (matching the party FAB's non-response), not
   merely no-op in the handler. The select is disabled in JS alongside. */
.lr-ctrl.lr-cooling .lr-emoji-chip { opacity: 0.4; }
.lr-ctrl.lr-cooling .lr-chip-btn   { cursor: not-allowed; pointer-events: none; }

/* ── PARTY 'FETTI INDICATOR ──
   A NON-interactive 🎉 shown after age/gender in each card + popup, signalling
   whether that user is open to Party 'Fetti. It deliberately reuses the exact
   off/self visual vocabulary of the Buzz/Gift controls so the states read the
   same everywhere: off (viewer's or that user's Party 'Fetti off) = dimmed +
   strikethrough; self = dimmed, no strikethrough; on = full. */
.party-ind {
  font-variant-emoji: emoji;
  line-height: 1;
  cursor: default;
  -webkit-user-select: none;
  user-select: none;
}
.party-ind.is-off  { opacity: 0.4; text-decoration: line-through; }
.party-ind.is-self { opacity: 0.4; }
/* Popup wraps the indicator so it gets a little inline breathing room in the
   liner-r2 text flow; the card uses .age-gender-info's flex gap instead. */
.party-ind-wrap { margin: 0 3px; }


.panel {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
}

/* ── JOIN FORM ── */
.form-group {
  margin-bottom: 10px;
}

/* Field heading labels only — excludes interactive wrapper labels (.radio-label, .checkbox-label)
   which have their own independent styling and must not inherit these form-header properties. */
.form-group label:not(.radio-label):not(.checkbox-label) {
  display: block;
  font-size: 10px;
  letter-spacing: 0.2em;
  color: var(--text-dim);
  text-transform: uppercase;
  margin-bottom: 6px;
}

.form-group label:not(.radio-label):not(.checkbox-label) .required {
  color: var(--red);
  margin-left: 2px;
}

/* ⓘ info affordance next to a field label — opens the Information dropdown
   and jumps to that field's Field Guide entry (openInfoTo). */
/* Labels that carry an ⓘ become a flex row so the ⓘ right-aligns to the
   field's right edge while the label text stays left-aligned. */
.form-group label:has(.field-info-btn) {
  display: flex;
  align-items: center;
}

.field-info-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: baseline;
  margin-left: auto;
  padding: 0;
  border: none;
  background: none;
  color: var(--accent-dim);
  font-size: 11px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity 0.15s, color 0.15s;
}

.field-info-btn:hover,
.field-info-btn:focus-visible {
  opacity: 1;
  color: var(--accent);
  outline: none;
}

.radio-group {
  display: flex;
  gap: 16px;
  margin-top: 6px;
}

.radio-label {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  font-size: 12px;
  color: var(--text);
  text-transform: none;
  letter-spacing: 0.05em;
}

.radio-label input[type="radio"] {
  width: 16px;
  height: 16px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--accent);
}

.checkbox-input {
  width: 16px;
  height: 16px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--accent);
}

/* ── CONSENT ROW (terms & conditions checkbox) ── */
.consent-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-top: 12px;
  margin-bottom: 10px;
}

.consent-row.error .checkbox-input {
  outline: 2px solid var(--red);
  outline-offset: 2px;
}

.consent-row .checkbox-input {
  flex-shrink: 0;
  margin-top: 2px; /* optical alignment with first text line */
}

/* ── FIELD ROW (two 50/50 columns; stays side-by-side at all viewport widths) ── */
.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  align-items: start;
}

.field-col {
  min-width: 0; /* let inputs/selects shrink within their column instead of overflowing */
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  font-size: 12px;
  color: var(--text);
  text-transform: none;
  letter-spacing: 0.05em;
  white-space: nowrap;
}

.checkbox-label:hover span { color: var(--accent); }

.consent-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.consent-label {
  font-size: 10px;
  color: var(--text-dim);
  line-height: 1.5;
  letter-spacing: 0.03em;
  cursor: pointer;
}

.consent-label .required {
  color: var(--red);
  margin-left: 2px;
}

.consent-label a {
  color: var(--accent-dim);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.consent-label a:hover {
  color: var(--accent);
}

.consent-note {
  font-size: 9px;
  color: var(--text-dim);
  opacity: 0.65;
  line-height: 1.4;
  letter-spacing: 0.02em;
}

.radio-label:hover span {
  color: var(--accent);
}

.form-group select {
  width: 100%;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--accent);
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  padding: 9px 12px;
  outline: none;
  transition: border-color 0.2s, box-shadow 0.2s;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23527a4a'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  padding-right: 32px;
}

.form-group select:focus {
  border-color: var(--accent-dim);
  box-shadow: 0 0 0 2px var(--accent-glow2), 0 0 12px var(--accent-glow2);
}

.form-group select.error {
  border-color: var(--red);
  box-shadow: 0 0 0 2px rgba(255,60,60,0.12);
}

.form-group select option {
  background: var(--bg3);
  color: var(--accent);
}

.form-group select option:disabled {
  color: var(--text-dim);
}

/* Locked (display-only) select — communicates a fixed default the user can't change */
.form-group select:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

/* ── Locked number field with a unit suffix (Heartbeat, Timeout, Position rate/
   threshold) ── mirrors the locked-select look; the unit sits inside the field on
   the right and the number is right-padded to clear it. Read-only display values;
   they dim (via .locked-num) on join like the selects. */
.field-unit { position: relative; }
.field-unit .field-suffix {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  color: var(--text-dim);
  pointer-events: none;
}
.form-group input.locked-num {
  width: 100%;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--accent);
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  padding: 9px 34px 9px 12px; /* right pad clears the unit suffix */
  outline: none;
  appearance: textfield;
  -moz-appearance: textfield;
}
.form-group input.locked-num::-webkit-outer-spin-button,
.form-group input.locked-num::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.form-group input.locked-num:disabled { cursor: not-allowed; opacity: 0.6; }

/* On-Move checkbox (Position row) — a fixed "on" status indicator, non-interactive.
   Stays solid (not dimmed) since it always reflects the same enabled behaviour. */
.field-col-check { min-width: 0; }
.form-group input.locked-check {
  width: 18px;
  height: 18px;
  margin: 4px 0 0;
  accent-color: var(--accent);
  cursor: not-allowed;
  opacity: 1;
}

/* Section divider — a thin rule between field groups (used before the Away/Idle
   thresholds), matching the Field Guide divider's border-top treatment. */
.form-divider {
  border-top: 1px solid var(--border);
  margin: 14px 0;
}

/* Avatar select fills its 50% column (matches the other two-column rows) */
#inp-avatar {
  width: 100%;
}

.form-group input[type="text"],
.form-group input[type="number"] {
  width: 100%;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--accent);
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  padding: 9px 12px;
  outline: none;
  transition: border-color 0.2s, box-shadow 0.2s;
  caret-color: var(--accent);
}

.form-group input::placeholder { color: var(--text-dim); opacity: 0.6; }

.form-group input:focus {
  border-color: var(--accent-dim);
  box-shadow: 0 0 0 2px var(--accent-glow2), 0 0 12px var(--accent-glow2);
}

.form-group input.error {
  border-color: var(--red);
  box-shadow: 0 0 0 2px rgba(255,60,60,0.12);
}

.char-counter {
  font-size: 9px;
  color: var(--text-dim);
  margin-top: 4px;
  text-align: right;
  letter-spacing: 0.05em;
}

.btn {
  width: 100%;
  background: transparent;
  border: 1px solid var(--accent-dim);
  border-radius: var(--radius);
  color: var(--accent);
  font-family: 'Orbitron', sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  padding: 11px;
  cursor: pointer;
  transition: all 0.2s;
  position: relative;
  overflow: hidden;
}

.btn::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform: translateX(-100%);
  transition: transform 0.25s ease;
  z-index: 0;
}
.btn:hover::before { transform: translateX(0); }
.btn:hover { color: var(--bg); border-color: var(--accent); box-shadow: 0 0 20px var(--accent-glow); }
.btn span { position: relative; z-index: 1; }

.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.btn:disabled::before { display: none; }

#joinBtn, #leaveBtn {
  margin-top: 10px;
}

.btn-leave {
  border-color: var(--red);
  color: var(--red);
  font-size: 10px;
}
.btn-leave::before { background: var(--red); }
.btn-leave:hover { color: #fff; border-color: var(--red); }

/* ── GEO STATUS ── */
.geo-status {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  color: var(--text-dim);
  padding: 8px 10px;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* ── GPS HELP PANEL ── */
.gps-help-panel {
  display: none;
  margin: 15px 0;
  padding: 12px;
  background: rgba(255,183,0,0.1);
  border: 1px solid var(--amber);
  border-radius: var(--radius);
}

.gps-help-title {
  font-size: 13px;
  font-weight: bold;
  color: var(--amber);
  margin-bottom: 8px;
}

.gps-help-body {
  font-size: 11px;
  line-height: 1.5;
  margin-bottom: 10px;
}

.gps-help-instructions {
  margin-bottom: 10px;
  font-size: 10px;
  color: var(--text-dim);
}

.gps-help-instructions ul {
  margin: 5px 0 0 15px;
  padding: 0;
  line-height: 1.6;
}

.gps-help-after {
  margin-top: 10px;
  font-size: 10px;
  color: var(--text-dim);
}

.gps-help-after ul {
  margin: 5px 0 0 15px;
  padding: 0;
}

#lockedPanel .locked-icon {
  font-size: 36px;
}

#retryGpsBtn {
  padding: 8px;
  font-size: 12px;
}

/* ── SECTION TOGGLE (Compact Expandable Sections) — shared by the
   Connection and Information collapsible sections ── */
.section-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-top: 10px;
  font-size: 10px;
  color: var(--text-dim);
  cursor: pointer;
  transition: all 0.2s ease;
  user-select: none;
}

.section-toggle:hover {
  border-color: var(--border-bright);
  background: var(--bg2);
}

.section-toggle-icon {
  font-size: 11px;
  color: var(--accent-dim);
  transition: transform 0.2s ease;
  flex-shrink: 0;
}

.section-toggle.expanded .section-toggle-icon {
  transform: rotate(90deg);
}

.section-toggle-label {
  flex: 1;
  font-weight: 600;
  letter-spacing: 0.05em;
}

.section-content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
  font-size: 10px;
  line-height: 1.5;
  color: var(--text-dim);
}

.section-content.expanded {
  max-height: 9999px; /* fallback only — toggleSection() sets exact scrollHeight via inline style */
}

.section-inner {
  padding: 12px 10px 0;
}

/* Last field group carries no bottom margin: combined with the removed
   bottom padding above, the gap below the final row equals the consent row's
   10px margin-top — the same spacing shown when the section is collapsed. */
.section-inner .form-group:last-child {
  margin-bottom: 0;
}

.section-content ul {
  margin: 6px 0 0 16px;
  padding: 0;
}

.section-content li {
  margin-bottom: 4px;
}

.section-content strong {
  color: var(--text);
}

/* Field Guide entries — targets of the field-label ⓘ buttons (openInfoTo). */
.fg-entry {
  margin-top: 3px;
  padding: 1px 4px;
  border-radius: var(--radius);
  scroll-margin: 14px;
}

.fg-entry strong {
  color: var(--accent);
}

/* Brief highlight when an entry is jumped to from a field's ⓘ. */
.fg-entry.fg-flash {
  animation: fgFlash 1.6s ease;
}

@keyframes fgFlash {
  0%, 100% { background: transparent; }
  12%      { background: var(--accent-glow2); }
}

/* ── POSITION STRIP (below map) ── */
.pos-strip {
  display: flex;
  gap: 6px;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 14px;
  flex-wrap: wrap;
}

.pos-cell {
  flex: 1;
  /* 58px lets all four data cells (LAT/LNG/ACC/FIX) fit on the first row down to
     ~320px-wide screens; only SYNC then wraps to its own row. flex:1 still grows
     the cells to fill the row, so values stay readable at wider widths. */
  min-width: 58px;
}

.pos-label {
  font-size: 8px;
  color: var(--text-dim);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  margin-bottom: 3px;
}

.pos-val {
  font-size: 11px;
  color: var(--accent);
  text-shadow: 0 0 4px rgba(var(--accent-rgb),0.25);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pos-val.dim { color: var(--text-dim); text-shadow: none; }

.pos-cell-sync { padding-top: 4px; }
.pos-cell-sync .poll-bar { margin: 0; }

/* ── OSM ATTRIBUTION ── */
.osm-attribution {
  font-size: 10px;
  color: var(--text-dim);
  text-align: right;
  /* Sit 10px below the position strip — matching the .form-group spacing used
     above form-field labels. The .col-map flex gap is 14px, so -4px margin-top
     (with no top padding) nets the intended 10px. */
  margin-top: -4px;
  padding: 0 2px;
  letter-spacing: 0.04em;
}

.osm-attribution a {
  color: var(--accent-dim);
  text-decoration: none;
}

.osm-attribution a:hover {
  color: var(--accent);
  text-decoration: underline;
}

/* ═══════════════════════════════════════════════════════════
   COL-FORM (left column) — form inputs, buttons, info toggle
   COL-CARDS (right column) — locked/live panels, user cards
═══════════════════════════════════════════════════════════ */
.col-form {
  grid-area: form;
  display: flex;
  flex-direction: column;
  gap: 12px;
  position: sticky;
  top: 20px;
  max-height: calc(100vh - 100px);
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border-bright) var(--bg2);
}

.col-form::-webkit-scrollbar { width: 4px; }
.col-form::-webkit-scrollbar-track { background: var(--bg2); }
.col-form::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; }

.col-cards {
  grid-area: cards;
  display: flex;
  flex-direction: column;
  gap: 12px;
  position: sticky;
  top: 20px;
  max-height: calc(100vh - 100px);
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border-bright) var(--bg2);
}

.col-cards::-webkit-scrollbar { width: 4px; }
.col-cards::-webkit-scrollbar-track { background: var(--bg2); }
.col-cards::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; }

.users-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  /* The header is now a direct flex child of .col-cards (gap:12px), so that gap
     applies below it on top of any margin. Its previous 8px spacing lived inside
     #livePanel where the col-cards gap didn't apply. -4px cancels the surplus
     (12 gap − 4 = 8) to restore exactly the previous 8px below the header. */
  margin-bottom: -4px;
}

.users-title {
  font-family: 'Orbitron', sans-serif;
  font-size: 10px;
  letter-spacing: 0.25em;
  color: var(--accent-dim);
  text-transform: uppercase;
}

.users-count {
  font-family: 'Orbitron', sans-serif;
  font-size: 16px;
  color: var(--accent);
  text-shadow: 0 0 8px rgba(var(--accent-rgb),0.5);
  line-height: 1;
}

.users-count span { font-size: 11px; color: var(--text-dim); margin-left: 4px; }

/* ── USER CARDS ── */
.users-grid {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.user-card {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  position: relative;
  overflow: hidden;
  transition: border-color 0.3s, box-shadow 0.3s;
  animation: card-in 0.35s ease both;
  cursor: pointer;
}

/* Peer cards mirror the self pair below (.user-card.is-me / .is-me.popup-open):
   dim tone for the lesser state, full tone once the popup is open. The full tone
   is the same --peer-rgb the marker and the flash use, so a peer reads as one
   colour across card, marker and flash. */
.user-card:hover:not(.is-me) {
  border-color: rgb(var(--peer-dim-rgb));
  box-shadow: 0 0 14px rgba(var(--peer-rgb),0.1);
}

.user-card.popup-open:not(.is-me) {
  border-color: rgb(var(--peer-rgb));
  box-shadow: 0 0 14px rgba(var(--peer-rgb),0.2);
}

@keyframes card-in {
  from { opacity: 0; transform: translateY(10px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

.user-card::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: var(--accent-dim);
  transition: background 0.3s;
}

.user-card.is-me {
  border-color: var(--accent-dim);
  box-shadow: 0 0 20px var(--accent-glow2), inset 0 0 20px rgba(var(--accent-rgb),0.03);
}
.user-card.is-me::before { background: var(--accent); box-shadow: 0 0 8px var(--accent); }

.user-card.is-me.popup-open {
  border-color: var(--accent);
  box-shadow: 0 0 20px var(--accent-glow), inset 0 0 20px rgba(var(--accent-rgb),0.06);
}

.user-card.stale        { opacity: 0.5; border-style: dashed; }
.user-card.off-viewport { opacity: 0.55; }

/* ── BUZZ PULSE (additive — does not affect any existing animation) ────────── */
/* Triple border flash on buzz send/receive, in the flashing entity's own colour.
   Uses border-color not outline — outline is clipped on left/right by .col-cards
   overflow-y:auto (which implicitly sets overflow-x:auto). border-color is within
   the card's own box, never clipped.
   CSS animations override transitions, so step-end border-color flashes correctly
   despite .user-card having transition: border-color 0.3s. Handles both active
   (.popup-open) and non-active cards with one rule: off state uses var(--border)
   (the base card border), on state uses rgb(var(--lr-flash-rgb)). When the
   animation ends on an active card, the border-color transition fades cleanly
   back to that same colour — the card's open-state border and the flash now
   resolve to one token per entity (--peer-rgb / --accent-rgb), so there is no
   hue shift at the hand-off. */
.lr-flash { animation: lr-flash 3s step-end forwards; }

/* Flash colour follows the flashing entity's own identity: self = accent, every
   other user = peer. --lr-flash-rgb defaults to peer here and is overridden to
   accent on self surfaces (.user-card.is-me, .marker-self, .liner.is-me). One
   variable drives all three flash keyframes, so the colour stays consistent across
   card border, popup liner and marker glow, and adapts to the theme. */
.lr-flash, .lr-flash-popup, .lr-flash-marker { --lr-flash-rgb: var(--peer-rgb); }
.user-card.is-me.lr-flash,
.liner.is-me .lr-flash-popup,
.liner.is-me.lr-flash-popup,
.marker-self.lr-flash-marker { --lr-flash-rgb: var(--accent-rgb); }

@keyframes lr-flash {
  0%        { border-color: var(--border); }
  8%        { border-color: rgb(var(--lr-flash-rgb)); }
  17%       { border-color: var(--border); }
  25%       { border-color: rgb(var(--lr-flash-rgb)); }
  34%       { border-color: var(--border); }
  42%       { border-color: rgb(var(--lr-flash-rgb)); }
  51%, 100% { border-color: var(--border); }
}

/* Popup liner flash — inset box-shadow instead of outline because
   .maplibregl-popup-content has overflow:hidden !important which clips
   outline on children. Inset renders inside .liner's own border, never clipped. */
.lr-flash-popup { animation: lr-flash-popup 3s step-end forwards; }

@keyframes lr-flash-popup {
  0%        { box-shadow: inset 0 0 0 2px transparent; }
  8%        { box-shadow: inset 0 0 0 2px rgb(var(--lr-flash-rgb)); }
  17%       { box-shadow: inset 0 0 0 2px transparent; }
  25%       { box-shadow: inset 0 0 0 2px rgb(var(--lr-flash-rgb)); }
  34%       { box-shadow: inset 0 0 0 2px transparent; }
  42%       { box-shadow: inset 0 0 0 2px rgb(var(--lr-flash-rgb)); }
  51%, 100% { box-shadow: inset 0 0 0 2px transparent; }
}

/* Map marker glow flash — filter:drop-shadow on the 0×0 emoji container applies
   to all absolutely-positioned children (emoji span + ring). Colour follows the
   entity (self accent / other peer) via --lr-flash-rgb; on removal, the
   hover/popup-open glow resumes if active. */
.lr-flash-marker { animation: lr-flash-marker 3s step-end forwards; }

@keyframes lr-flash-marker {
  0%        { filter: none; }
  8%        { filter: drop-shadow(0 2px 12px rgba(var(--lr-flash-rgb),0.8)); }
  17%       { filter: none; }
  25%       { filter: drop-shadow(0 2px 12px rgba(var(--lr-flash-rgb),0.8)); }
  34%       { filter: none; }
  42%       { filter: drop-shadow(0 2px 12px rgba(var(--lr-flash-rgb),0.8)); }
  51%, 100% { filter: none; }
}

.user-card .card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 5px;
  gap: 8px;
}

.user-card .age-gender {
  font-family: 'Orbitron', sans-serif;
  font-size: 11px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.3;
  margin-bottom: 6px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 5px;
  align-items: center;
}

/* Inner wrapper: avatar + age/gender text as a flex row within grid column 1 */
.age-gender-info {
  display: flex;
  align-items: center;
  gap: 6px;
}

.user-card .topic-wrap {
  margin-bottom: 3px;
}

.user-card .topic-eyebrow {
  font-size: 7px;
  color: var(--text-dim);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  margin-bottom: 2px;
  margin-top: 1px;
}

.user-card .topic {
  font-family: 'Orbitron', sans-serif;
  font-size: 11px;
  font-weight: 700;
  color: var(--accent);
  text-shadow: 0 0 8px rgba(var(--accent-rgb),0.3);
  word-break: break-word;
  line-height: 1.3;
}

.user-card .card-instructions {
  font-size: 10px;
  color: var(--text);
  line-height: 1.4;
  margin-bottom: 4px;
  padding: 4px 7px;
  background: var(--bg3);
  border-left: 2px solid var(--border);
  border-radius: var(--radius);
}

.card-static ins {
  display: block;
  margin-bottom: 4px;
  font-size: 12px;
  line-height: 1.4;
}

.user-card .me-badge {
  font-size: 9px;
  background: var(--accent);
  color: var(--bg);
  border-radius: 2px;
  padding: 2px 6px;
  letter-spacing: 0.15em;
  font-weight: 700;
  white-space: nowrap;
  flex-shrink: 0;
}

.user-card .card-meta {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 5px;
  margin-bottom: 8px;
}

.meta-item .label {
  font-size: 8px;
  color: var(--text-dim);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  margin-bottom: 2px;
}

.meta-item .value {
  font-size: 11px;
  color: var(--text);
}

.coord-pair .label {
  font-size: 8px;
  color: var(--text-dim);
  letter-spacing: 0.1em;
}
.coord-pair .value {
  font-size: 10px;
  color: var(--accent);
}

/* ── DISTANCE / BEARING ROW ── */
.card-distance {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 8px;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.dist-value {
  font-family: 'Orbitron', sans-serif;
  font-size: 13px;
  color: var(--amber);
  text-shadow: 0 0 8px rgba(255,183,0,0.35);
}

.bearing-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
}

.bearing-arrow {
  font-size: 17px;
  line-height: 1;
  transition: transform 0.6s cubic-bezier(0.34,1.56,0.64,1);
  display: inline-block;
  color: var(--accent);
  text-shadow: 0 0 8px var(--accent);
}

.bearing-text {
  font-family: 'Orbitron', sans-serif;
  font-size: 11px;
  color: var(--text);
  letter-spacing: 0.1em;
  min-width: 36px;
  text-align: right;
}

/* ── EMPTY STATE ── */
.empty-state {
  grid-column: 1/-1;
  text-align: center;
  padding: 60px 20px;
  color: var(--text-dim);
}

.empty-state .big {
  font-family: 'Orbitron', sans-serif;
  font-size: 40px;
  opacity: 0.15;
  margin-bottom: 12px;
}

.empty-state p {
  font-size: 12px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

.empty-state .sub {
  margin-top: 6px;
  font-size: 10px;
  opacity: 0.6;
  text-transform: none;
  letter-spacing: 0.05em;
}

.card-distance-me {
  justify-content: center;
  color: var(--text-dim);
  font-size: 11px;
  letter-spacing: 0.1em;
}

.card-distance-gps {
  color: var(--text-dim);
  font-size: 11px;
}

/* ── CARD IDENTITY ROW (redesign) ──
   avatar + age/gender on the left; distance + direction moved to the right (as
   in the popup), at a reduced size so both fit on one row. The "— THIS IS YOU —"
   / "GPS acquiring…" states occupy the same right-hand slot. */
.user-card .card-identity {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
  font-family: 'Orbitron', sans-serif;
  font-size: 11px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.3;
}
/* Distance/direction sits in column 2, LEFT-aligned on the 50% gridline so it
   lines up with "Gift" in the actions row and "Active" in the stats box below. */
.card-identity-dist {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  justify-self: start;
  white-space: nowrap;
}
.card-identity-dist .cid-dist {
  font-family: 'Orbitron', sans-serif;
  font-size: 10px;
  color: var(--amber);
  text-shadow: 0 0 8px rgba(var(--amber-rgb),0.35);
}
/* Secondary (parenthetical) unit — smaller + dimmed so the exact primary reads
   first and the whole string stays compact enough to never clip the card edge. */
.card-identity-dist .cid-dist-sec {
  font-family: 'Share Tech Mono', monospace;
  font-size: 8px;
  color: var(--text-dim);
}
.card-identity-dist .cid-arr {
  font-size: 12px;
  line-height: 1;
  display: inline-block;
  color: var(--accent);
  text-shadow: 0 0 8px var(--accent);
  transition: transform 0.6s cubic-bezier(0.34,1.56,0.64,1);
}
.card-identity-dist .cid-bearing {
  font-size: 9px;
  letter-spacing: 0.08em;
  color: var(--text);
}
/* GPS-acquiring placeholder text in the distance slot. */
.card-identity-dist.is-gps {
  font-family: 'Share Tech Mono', monospace;
  font-size: 9px;
  font-weight: 400;
  letter-spacing: 0.1em;
  color: var(--text-dim);
  text-shadow: none;
}

/* ── COORDINATES ROW ──
   Lower card row: LAT + LNG in two equal columns. Sits below the presence row
   so the card mirrors the popup flow (Session · Last Seen · Status, then coords).
   Reuses .coord-pair. */
.user-card .card-coords {
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 5px 8px;
  display: grid;
  /* 4 columns matching .card-presence above, so the two boxes' columns align. LAT,
     LNG, ALTITUDE fill the first three; the fourth is intentionally empty. */
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
  margin-top: 2px;
}
.card-coords .coord-pair { min-width: 0; }
.card-coords .coord-pair .label,
.card-presence .coord-pair .label {
  font-size: 8px;
  color: var(--text-dim);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin-bottom: 2px;
}
.card-coords .coord-pair .value,
.card-presence .coord-pair .value {
  font-size: 10px;
  color: var(--accent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── PRESENCE / SESSION ROW ── */
/* Upper card row: Session, Last Seen, Status in the first three of four equal
   columns (the fourth stays empty), preserving the original stats-box 4-column
   format so these fields keep their column width. */
.user-card .card-presence {
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 5px 8px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
  margin-bottom: 2px;
}

/* ── IDLE WARNING DIALOG ── */
/* Visually hidden but announced. NOT display:none, which would remove it from
   the accessibility tree and silence the live region. */
.idle-sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.idle-modal {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0,0,0,0.6);
  padding: 16px;
}
.idle-modal.is-hidden { display: none; }

.idle-modal-box {
  background: var(--bg2);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: 0 0 32px var(--accent-glow);
  padding: 20px;
  max-width: 320px;
  width: 100%;
  text-align: center;
}

.idle-modal-title {
  font-family: 'Orbitron', sans-serif;
  font-size: 14px;
  color: var(--accent);
  margin: 0 0 8px 0;
  letter-spacing: 0.05em;
}

.idle-modal-desc {
  font-size: 11px;
  color: var(--text);
  margin: 0 0 16px 0;
  line-height: 1.5;
}

.idle-countdown {
  font-family: 'Share Tech Mono', monospace;
  font-size: 16px;
  color: var(--amber);
  font-variant-numeric: tabular-nums;
}

.idle-stay-btn {
  width: 100%;
  padding: 10px;
  background: transparent;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  color: var(--accent);
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 0.1em;
  cursor: pointer;
  transition: background 0.2s, color 0.2s;
}
.idle-stay-btn:hover { background: var(--accent); color: var(--bg); }
.idle-stay-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Popup presence row — matches .liner-r3's padding and bottom rule exactly. */
.liner-r5 {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 3px 6px 2px 6px;
  font-size: 9px;
  line-height: 1;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--border);
}
.liner-state { color: var(--text-dim); white-space: nowrap; font-size: 8px; }

/* Marker presence treatment.
   Idle and Away de-emphasise by DESATURATING THE ICON ONLY — the dot
   (.marker-*-core) or the emoji glyph (.marker-emoji) — graded idle -> away. The
   dot's core carries its own box-shadow glow, so the fill and that glow desaturate
   together; the emoji glyph has no ambient glow, so only the glyph desaturates. The
   expanding pulse ring (.marker-*-ring) is a separate element and keeps its
   accent/peer colour, so idle/away users still pulse and stay findable and
   legible. Live / Idle / Away is also stated in words on the card and popup
   presence rows. */
.marker-self-core,
.marker-user-core,
.marker-emoji {
  transition: filter 0.3s ease;
}

.marker-self.presence-idle .marker-self-core,
.marker-self.presence-idle .marker-emoji,
.marker-user.presence-idle .marker-user-core,
.marker-user.presence-idle .marker-emoji { filter: grayscale(0.7); }

.marker-self.presence-away .marker-self-core,
.marker-self.presence-away .marker-emoji,
.marker-user.presence-away .marker-user-core,
.marker-user.presence-away .marker-emoji { filter: grayscale(1); }

@media (prefers-reduced-motion: reduce) {
  .marker-self-core,
  .marker-user-core,
  .marker-emoji { transition: none; }
}

/* ── POLL STATUS BAR ── */
.poll-bar {
  height: 2px;
  background: var(--border);
  border-radius: 1px;
  overflow: hidden;
  margin-top: 4px;
}

.poll-bar-inner {
  height: 100%;
  background: var(--accent-dim);
  width: 0%;
  transition: width 0.1s linear;
}

.poll-bar-inner.active { background: var(--accent); }

/* ── TOAST STACK ──
   Toasts are independent cards inside a fixed, bottom-anchored flex column.
   The container is pinned at its bottom edge and grows UPWARD as toasts are
   added; because it is a normal `column`, the most recently appended toast sits
   at the bottom (nearest the anchor) and older toasts float up above it. The
   container ignores pointer events so its gaps never intercept clicks; each
   toast opts back in. See mountToast() in app.js for the lifecycle/cap logic. */
#toast-stack {
  position: fixed;
  bottom: var(--toast-bottom, 24px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 10000;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  max-width: calc(100vw - 32px);
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  background: var(--bg2);
  border: 1px solid var(--border-bright);
  border-radius: var(--radius);
  padding: 10px 20px;
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--text);
  box-shadow: 0 8px 32px rgba(0,0,0,0.6);
  max-width: 100%;
  white-space: normal;
  word-break: break-word;
  display: flex;
  align-items: flex-start;
  gap: 8px;
  opacity: 0;
  transform: translateY(40px);
  transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), opacity 0.3s ease;
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast.hide {
  opacity: 0;
  transform: translateY(20px);
  transition: transform 0.25s ease, opacity 0.25s ease;
}
/* Variant = who acted, not what happened.
     .ok   — this user acted, or the system succeeded  -> --accent (the self colour)
     .peer — ANOTHER user acted (buzz / gift / emote)  -> --peer-rgb (the peer colour)
     .warn — the system has a warning                  -> --amber
     .err  — the system failed                         -> --red
   .peer and .warn were both --amber until now, which made a buzz from a peer and a
   system warning literally indistinguishable — the same collision the markers had,
   one layer up. Routing peer actions through --peer-rgb resolves it and leaves
   --amber meaning exactly one thing here: warning. */
.toast.ok   { border-color: var(--accent);          color: var(--accent); }
.toast.err  { border-color: var(--red);             color: var(--red); }
.toast.peer { border-color: rgb(var(--peer-rgb));   color: rgb(var(--peer-rgb)); }
.toast.warn { border-color: var(--amber);           color: var(--amber); }

/* × dismiss button — present on all toasts, always right-aligned */
.toast-close {
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  margin-left: auto;
  opacity: 0.6;
  padding: 0;
  flex-shrink: 0;
}
.toast-close:hover { opacity: 1; }

/* Clickable action link inside the buzz toast ("Go to User →") */
.toast-action {
  cursor: pointer;
  text-decoration: underline;
  flex-shrink: 0;
}
.toast-action:hover { opacity: 0.8; }

/* Inline countdown span inside toasts — "(20s)", "(3s)" etc. The gap before it
   is a non-breaking margin (not a leading space) so the no-wrap countdown never
   forces a premature line break when the whole line still fits. The center-map
   toast stacks its parts on their own lines, so it takes no left gap. */
.toast-countdown { white-space: nowrap; margin-left: 0.35em; }
.center-toast .toast-countdown { margin-left: 0; }

/* Toast emoji column — emoji stays pinned left; text wraps under itself not under emoji */
.toast-text-wrap {
  display: flex;
  align-items: flex-start;
  gap: 4px;
  flex: 1;
  min-width: 0;
}

.toast-emoji { flex-shrink: 0; }

/* ── CENTER-MAP TOAST (Type 2) ──
   Private SENT + RECEIVED interactions (Buzz / Gift) raise a prominent toast
   centered over the map (and the self marker). Reuses the mountToast lifecycle
   (cap + evict-oldest + show/hide), as a vertically-centered stack with a
   HORIZONTAL slide that enters from the RIGHT and exits to the LEFT (right→left).
   Frosted-glass background keeps the map faintly visible behind while the emoji /
   text stay fully opaque. On appear the outline flashes 3× in unison with the
   card/marker flash. Lives inside #mapWrap so fullscreen covers it. */
#lr-center-stack {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 15;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  width: max-content;
  max-width: min(320px, calc(100% - 32px));
  pointer-events: none;
}
.center-toast {
  position: relative;
  pointer-events: auto;
  background: color-mix(in srgb, var(--bg2) 70%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--border-bright);
  border-radius: var(--radius);
  padding: 16px 26px;
  color: var(--text);
  box-shadow: 0 8px 32px rgba(0,0,0,0.6);
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  opacity: 0;
  transform: translateX(40px);
  transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), opacity 0.3s ease;
}
/* Variant colour — matches the bottom toast: sent = accent, received = peer.
   --ct-flash feeds the 3× appear flash below. */
.center-toast.ok   { border-color: var(--accent);        --ct-flash: var(--accent); }
.center-toast.peer { border-color: rgb(var(--peer-rgb)); --ct-flash: rgb(var(--peer-rgb)); }
.center-toast.show {
  opacity: 1;
  transform: translateX(0);
  animation: center-toast-flash 3s step-end;
}
.center-toast.hide {
  opacity: 0;
  transform: translateX(-40px);
  transition: transform 0.25s ease, opacity 0.25s ease;
}
/* 3× outline flash on appear — pulses the border between a dim beat and the
   variant colour, on the same 8/25/42% cadence as lr-flash, then settles on the
   resting variant border (no `forwards`, so CSS border-color resumes cleanly). */
@keyframes center-toast-flash {
  0%, 17%, 34%          { border-color: var(--border-bright); }
  8%, 25%, 42%, 51%, 100% { border-color: var(--ct-flash); }
}
/* Big dynamic emoji on its own line (~2.5×). */
.center-toast-emoji {
  font-variant-emoji: emoji;
  font-size: 2.5em;
  line-height: 1.1;
}
.center-toast-msg {
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--text);
}
.center-toast.ok   .center-toast-msg { color: var(--accent); }
.center-toast.peer .center-toast-msg { color: rgb(var(--peer-rgb)); }
.center-toast .toast-action { text-decoration: underline; cursor: pointer; }
.center-toast .toast-action:hover { opacity: 0.8; }
.center-toast .toast-countdown { white-space: nowrap; }
.center-toast .toast-close {
  position: absolute;
  top: 6px;
  right: 8px;
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  opacity: 0.6;
  padding: 0;
}
.center-toast .toast-close:hover { opacity: 1; }

/* ── LOCKED PANEL ── */
.locked-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 32px 16px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--bg2);
  min-height: 180px;
}

.locked-icon {
  font-size: 56px;
  opacity: 0.12;
  margin-bottom: 20px;
  animation: locked-pulse 3s ease-in-out infinite;
}

@keyframes locked-pulse {
  0%,100% { opacity: 0.10; transform: scale(1); }
  50%      { opacity: 0.20; transform: scale(1.06); }
}

.locked-title {
  font-family: 'Orbitron', sans-serif;
  font-size: 13px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 12px;
}

.locked-sub {
  font-size: 11px;
  color: var(--text-dim);
  letter-spacing: 0.1em;
  opacity: 0.6;
  max-width: 280px;
  line-height: 1.7;
  text-align: center;
}

/* ── SCROLLBAR ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg2); }
::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 3px; }

/* Mobile: single column — form, map, cards stacked in natural order */
@media (max-width: 860px) {
  .main {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto;
    grid-template-areas: "form" "map" "cards";
  }
  .col-form,
  .col-cards {
    position: static;
    max-height: none;
    overflow-y: visible;
  }
  .col-map {
    position: static;
  }
  .map-wrap {
    height: 65vw;
    min-height: 300px;
  }
  /* Force consistent toast width on mobile — without this, short messages
     produce a narrow toast while long ones expand to max-width. Both should
     span the full content area (16px gutter each side, matching .app padding). */
  .toast {
    width: calc(100vw - 32px);
  }
}

/* ═══════════════════════════════════════════════════════════
   LIGHT THEME
═══════════════════════════════════════════════════════════ */
[data-mode="light"] {
  --bg:            #f0f2f5;
  --bg2:           #e4e8ef;
  --bg3:           #d8dde8;
  --bg-overlay:    rgba(240,242,245,0.85);
  --accent:        #7c3aed;
  --accent-dim:    #4c1d95;
  --accent-rgb:    124,58,237;
  --accent-glow:   rgba(124,58,237,0.18);
  --accent-glow2:  rgba(124,58,237,0.06);
  --amber:         #d97706;
  --amber-rgb:     217,119,6;
  --red:           #dc2626;
  /* Amber cannot clear 3:1 on a light basemap without becoming brown, so peer
     identity leaves the amber hue here — deep pink #e6007e. WCAG 2.2 SC 1.4.11
     wants 3:1 for UI components; markers sit on the OSM raster at
     raster-brightness-max 1.0, not on flat --bg, so the real backdrop varies:
     4.0:1 on --bg, 3.7:1 on the --bg2 card border, 3.3:1 on park green #c8e6a0,
     2.8:1 on water #aad3df. Water is the one backdrop still under the bar — the
     best any un-browned hue does here, and far past amber's 1.6:1. Sits 65° off
     the light --accent purple, so self and peer stay distinguishable at 12px.
     --peer-dim-rgb is pink-800 — same family, and it sits to --peer-rgb as
     --accent-dim sits to --accent on the --bg2 card (6.4:1 dim / 3.7:1 full). */
  --peer-rgb:      230,0,126;
  --peer-dim-rgb:  157,23,77;
  --text:          #1e1b4b;
  --text-dim:      #6d6a85;
  --border:        #c4bfe0;
  --border-bright: #9d96c8;
}

/* Light theme: reduce noise opacity */
[data-mode="light"] body::after {
  opacity: 0.15;
}

/* Light theme: logo filter for purple tint */
[data-mode="light"] .logo-img {
  filter: brightness(0) saturate(100%) invert(25%) sepia(80%) saturate(1500%) hue-rotate(250deg) brightness(90%) contrast(110%);
}

/* Light theme: select dropdown arrow uses light theme text-dim color (#6d6a85) */
[data-mode="light"] .form-group select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%236d6a85'/%3E%3C/svg%3E");
}

/* ── MODE TOGGLE SWITCH ── */
.mode-toggle {
  display: flex;
  align-items: center;
  gap: 7px;
  cursor: pointer;
  user-select: none;
}

.mode-toggle-label {
  font-size: 13px;
  color: var(--text-dim);
  letter-spacing: 0.12em;
}

.mode-toggle input[type="checkbox"] {
  display: none;
}

.mode-toggle-track {
  width: 32px;
  height: 18px;
  border-radius: 9px;
  /* Always the theme accent (purple in light, green in dark) — the thumb
     position, not the track color, indicates the on/off state. */
  background: var(--accent);
  border: 1px solid var(--accent);
  position: relative;
  transition: background 0.2s, border-color 0.2s;
  flex-shrink: 0;
}

.mode-toggle-thumb {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--bg);
  position: absolute;
  top: 2px;
  left: 2px;
  transition: transform 0.2s, background 0.2s;
}

.mode-toggle input:checked + .mode-toggle-track .mode-toggle-thumb {
  transform: translateX(14px);
}

/* ── COOKIE CONSENT BANNER ── */
.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 9000;
  background: var(--bg2);
  border-top: 1px solid var(--border-bright);
  padding: 14px 20px;
  display: none;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 20px;
  box-shadow: 0 -4px 24px rgba(0,0,0,0.4);
}

.cookie-banner.visible {
  display: flex;
}

.cookie-banner-text {
  flex: 1;
  /* Cap the 220px minimum at the banner's own width, so when the banner is
     narrower than 220px the text wraps to fit instead of overflowing (and forcing
     the banner content wider than the banner). At wider sizes min() keeps the
     220px floor, so the normal layout is unchanged. */
  min-width: min(220px, 100%);
  font-size: 11px;
  color: var(--text-dim);
  line-height: 1.5;
  letter-spacing: 0.03em;
}

/* Expandable cookie-category detail — full-width row at the bottom of the banner */
.cookie-details {
  order: 1;
  flex: 1 0 100%;
  font-size: 11px;
  color: var(--text-dim);
  line-height: 1.5;
  letter-spacing: 0.03em;
}

.cookie-details summary {
  cursor: pointer;
  color: var(--accent-dim);
  font-weight: 600;
  user-select: none;
}

.cookie-details summary:hover {
  color: var(--accent);
}

.cookie-details p {
  margin: 8px 0 4px;
}

.cookie-details ul {
  margin: 0 0 0 16px;
  padding: 0;
}

.cookie-details li {
  margin-bottom: 4px;
}

.cookie-details strong {
  color: var(--text);
}

.cookie-banner-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  /* Allow this container to shrink and release its automatic min-content floor,
     so its own flex-wrap stacks the second button on a narrow banner instead of
     overflowing/clipping it. */
  min-width: 0;
}

.cookie-btn {
  font-family: 'Orbitron', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  padding: 8px 14px;
  border-radius: var(--radius);
  cursor: pointer;
  transition: all 0.2s;
  white-space: nowrap;
}

.cookie-btn-essential {
  background: transparent;
  border: 1px solid var(--border-bright);
  color: var(--text-dim);
}

.cookie-btn-essential:hover {
  border-color: var(--text-dim);
  color: var(--text);
}

.cookie-btn-accept {
  background: transparent;
  border: 1px solid var(--accent-dim);
  color: var(--accent);
}

.cookie-btn-accept:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
  box-shadow: 0 0 12px var(--accent-glow);
}

.cookie-banner-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 10px;
  /* Allow the links to wrap and shrink so they can't overflow a narrow banner. */
  min-width: 0;
}

.cookie-banner-links a {
  color: var(--text-dim);
  text-decoration: none;
  letter-spacing: 0.05em;
}

.cookie-banner-links a:hover {
  color: var(--accent);
}

.cookie-sep {
  color: var(--border-bright);
  font-size: 12px;
}

/* ── AD PLACEHOLDER (shown when cookies not granted) ── */
body:not(.cookies-granted) ins[data-revive-zoneid] {
  display: none;
}

.ad-placeholder {
  padding: 1px 6px;
  background: var(--bg3);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  font-size: 9px;
  color: var(--text-dim);
  letter-spacing: 0.04em;
  line-height: 1.3;
  text-align: center;
}

.ad-placeholder-link {
  color: var(--accent-dim);
  text-decoration: none;
  white-space: nowrap;
}

.ad-placeholder-link:hover {
  color: var(--accent);
}

.card-static .ad-placeholder {
  margin-bottom: 6px;
}

body.cookies-granted .ad-placeholder {
  display: none;
}
