// Map screen — real MapLibre GL map of the resort with live wait-time pins.
// Renders fullscreen (edge-to-edge from the top of the device frame down to the
// bottom-tab bar). Floating chip rows on top of the map handle park selection
// (DLR / DCA / Both) and category filters (Rides / Dining / Shows / Other).
//
// Uses OpenFreeMap (https://openfreemap.org) for vector tiles — completely
// free, no API key, OSM-based.
//
// IMPORTANT: ATTRACTIONS / LANDS are read dynamically from window.PH365 inside
// each function — never destructured at module load — because live-data.jsx
// and queue-times-fetch.jsx replace those arrays in place when fresh data arrives.
(function(){
const { COLORS, waitColor, Stamp, Caption, Body, Icon } = window.PH365_UI;

// Bottom-tabs height (matches BottomTabs in Passholder365.html)
const TAB_BAR_H = 78;

// Park centers + zooms — derived from the real DLR/DCA coordinates
const PARK_VIEWS = {
  DLR: { center: [-117.9189, 33.8121], zoom: 15.7 },
  DCA: { center: [-117.9217, 33.8060], zoom: 15.9 },
  ALL: { center: [-117.9203, 33.8095], zoom: 15.0 },
};

// Map style cascade. With a Stadia Maps API key we use Stadia's
// "alidade_bright" — a vector style that's colorful and friendly while
// staying sharp at the zoom levels admins need for precise pin placement
// (≥18). To override locally without rebuilding, set the global
// window.__PH365_MAP_STYLE_OVERRIDE in the console to any Stadia style
// id (e.g. 'outdoors', 'alidade_smooth', 'stamen_terrain', 'osm_bright').
function chooseMapStyle() {
  const cfg = window.__PH365_CONFIG__ || {};
  if (cfg.stadiaApiKey) {
    const styleId = window.__PH365_MAP_STYLE_OVERRIDE || 'alidade_bright';
    const url = `https://tiles.stadiamaps.com/styles/${styleId}.json?api_key=${encodeURIComponent(cfg.stadiaApiKey)}`;
    console.info(`[map] using Stadia "${styleId}"`);
    return url;
  }
  console.info('[map] using OpenFreeMap bright (no Stadia key configured)');
  return 'https://tiles.openfreemap.org/styles/bright';
}
const MAP_STYLE_URL = chooseMapStyle();

// ── Category groupings ──────────────────────────────────────────────
// data.js exposes a granular `type` per attraction (e.g. "Coaster",
// "Boat Ride", "Show"). Roll those up into the four high-level filter
// pills the user picks from. Anything that doesn't match goes to OTHER.
const CATEGORY_RIDES = new Set([
  'Boat Ride', 'Carousel', 'Coaster', 'Dark Ride', 'Drop',
  'Interactive Dark Ride', 'Simulator', 'Spinner', 'Track Ride',
  'Train', 'Transport', 'Water', 'Wheel',
]);
const CATEGORY_SHOWS  = new Set(['Show']);
const CATEGORY_DINING = new Set(['Dining', 'Food', 'Restaurant', 'Snack']);
function categoryOf(a) {
  const t = a.type || '';
  if (CATEGORY_RIDES.has(t))  return 'rides';
  if (CATEGORY_SHOWS.has(t))  return 'shows';
  if (CATEGORY_DINING.has(t)) return 'dining';
  return 'other';
}

// 'all' is a sentinel — when the active filter is 'all' every category renders.
// Tapping any other chip narrows to just that category (single-select).
const CATEGORIES = [
  { id: 'all',    label: 'All'    },
  { id: 'rides',  label: 'Rides'  },
  { id: 'dining', label: 'Dining' },
  { id: 'shows',  label: 'Shows'  },
  { id: 'other',  label: 'Other'  },
];

function MapScreen({ onSelectAttraction }) {
  const [view, setView]                 = React.useState('DLR');
  const [selectedId, setSelectedId]     = React.useState(null);
  const [mapReady, setMapReady]         = React.useState(false);
  // Single-select filter — 'all' shows every category, otherwise just one.
  const [activeCat, setActiveCat]       = React.useState('all');
  const [editMode, setEditMode]         = React.useState(false);
  // Working coordinates for the pin currently being moved — keyed by attraction id.
  // Only populated while editMode is on; cleared on Save / Cancel / mode-off.
  const [draftCoords, setDraftCoords]   = React.useState(null); // { id, lat, lon } | null
  const [saving, setSaving]             = React.useState(false);
  const [saveErr, setSaveErr]           = React.useState(null);
  const containerRef = React.useRef(null);
  const mapRef       = React.useRef(null);
  const markersRef   = React.useRef([]);
  const geolocateRef = React.useRef(null);
  // Draggable HTML markers for in-progress calibration pins. We keep the
  // GeoJSON line source for the connecting polyline but render the dots
  // themselves as DOM so MapLibre's built-in draggable Marker can handle
  // drag-to-nudge. Map: pin.id → ml.Marker.
  const calibMarkersRef = React.useRef(new Map());

  // Admin status — read from the live USER object. Re-derived on data-ready
  // events via the forceUpdate at the App level (which causes us to re-render).
  const isAdmin = !!(window.PH365 && window.PH365.USER && window.PH365.USER.isAdmin);

  // Keep the latest values available inside long-lived map event handlers
  // (drag, click) without re-binding listeners on every render.
  const editModeRef = React.useRef(editMode);
  const draftRef    = React.useRef(draftCoords);
  const selectedRef = React.useRef(selectedId);
  React.useEffect(() => { editModeRef.current = editMode; }, [editMode]);
  React.useEffect(() => { draftRef.current    = draftCoords; }, [draftCoords]);
  React.useEffect(() => { selectedRef.current = selectedId; }, [selectedId]);

  // ── Lazy-load MapLibre GL JS from CDN ───────────────────────────────
  React.useEffect(() => {
    if (window.maplibregl) { setMapReady(true); return; }

    if (!document.querySelector('link[data-maplibre]')) {
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = 'https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css';
      link.dataset.maplibre = 'true';
      document.head.appendChild(link);
    }
    if (!document.querySelector('script[data-maplibre]')) {
      const s = document.createElement('script');
      s.src = 'https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js';
      s.dataset.maplibre = 'true';
      s.onload  = () => setMapReady(true);
      s.onerror = () => console.error('[map] MapLibre CDN load failed');
      document.head.appendChild(s);
    } else {
      const wait = setInterval(() => {
        if (window.maplibregl) { clearInterval(wait); setMapReady(true); }
      }, 100);
      return () => clearInterval(wait);
    }
  }, []);

  // ── Initialise the map once MapLibre is loaded ──────────────────────
  React.useEffect(() => {
    if (!mapReady || !containerRef.current || mapRef.current) return;
    const ml = window.maplibregl;
    const v = PARK_VIEWS[view];

    const map = new ml.Map({
      container: containerRef.current,
      style: MAP_STYLE_URL,
      center: v.center,
      zoom: v.zoom,
      attributionControl: { compact: true },
      cooperativeGestures: false,
    });
    map.addControl(new ml.NavigationControl({ showCompass: false }), 'top-right');
    // Live "you are here" dot. Pulses when actively tracking, shows heading
    // when the device exposes compass data. We hold a ref so calibration
    // mode can auto-trigger it (admins shouldn't have to find and tap the
    // little button before they can see themselves on the map).
    const geolocate = new ml.GeolocateControl({
      positionOptions: { enableHighAccuracy: true, timeout: 10_000 },
      trackUserLocation: true,
      showUserHeading: true,
      // Don't fly to the user — we already auto-fly to the attraction
      // when calibration starts, and we don't want geolocate yanking
      // the camera off the ride.
      fitBoundsOptions: { maxZoom: 18 },
    });
    map.addControl(geolocate, 'top-right');
    geolocateRef.current = geolocate;
    mapRef.current = map;

    map.on('load', () => {
      addAttractionPins();
      // Active queue-path layers — line + dots through admin-placed pins.
      // Source data swaps when the selected attraction changes.
      if (!map.getSource('queue-path')) {
        map.addSource('queue-path', {
          type: 'geojson',
          data: { type: 'FeatureCollection', features: [] },
        });
        map.addLayer({
          id: 'queue-path-glow',
          type: 'line',
          source: 'queue-path',
          layout: { 'line-cap': 'round', 'line-join': 'round' },
          paint: { 'line-color': '#FBBE4F', 'line-width': 14, 'line-opacity': 0.28, 'line-blur': 5 },
        });
        map.addLayer({
          id: 'queue-path-line',
          type: 'line',
          source: 'queue-path',
          layout: { 'line-cap': 'round', 'line-join': 'round' },
          paint: {
            'line-color': '#FBBE4F', 'line-width': 4, 'line-opacity': 0.95,
            'line-dasharray': [3, 1.5],
          },
        });
      }
      // Active-path pin dots — points the user can tap to "claim" a spot.
      // Stored as a separate source so we can attach a click handler to
      // just this layer (vs the line, which is harder to hit-test cleanly).
      if (!map.getSource('queue-pins')) {
        map.addSource('queue-pins', {
          type: 'geojson',
          data: { type: 'FeatureCollection', features: [] },
        });
        map.addLayer({
          id: 'queue-pins-dots',
          type: 'circle',
          source: 'queue-pins',
          paint: {
            'circle-radius': 6,
            'circle-color': '#FBBE4F',
            'circle-stroke-color': '#fff',
            'circle-stroke-width': 1.5,
            'circle-opacity': 0.95,
          },
        });
      }
      // In-progress calibration overlays (admin pin-drop mode). Distinct
      // bright orange so they read as draft, separate from any saved path.
      if (!map.getSource('calibration-line')) {
        map.addSource('calibration-line', {
          type: 'geojson',
          data: { type: 'FeatureCollection', features: [] },
        });
        map.addLayer({
          id: 'calibration-line',
          type: 'line',
          source: 'calibration-line',
          layout: { 'line-cap': 'round', 'line-join': 'round' },
          paint: { 'line-color': '#FF7A1F', 'line-width': 3, 'line-opacity': 0.85 },
        });
      }
      if (!map.getSource('calibration-pins')) {
        map.addSource('calibration-pins', {
          type: 'geojson',
          data: { type: 'FeatureCollection', features: [] },
        });
        map.addLayer({
          id: 'calibration-pins-dots',
          type: 'circle',
          source: 'calibration-pins',
          paint: {
            'circle-radius': 8,
            'circle-color': '#FF7A1F',
            'circle-stroke-color': '#fff',
            'circle-stroke-width': 2,
          },
        });
        map.addLayer({
          id: 'calibration-pins-labels',
          type: 'symbol',
          source: 'calibration-pins',
          layout: {
            'text-field': ['get', 'label'],
            'text-size': 10,
            'text-font': ['Noto Sans Bold'],
            'text-offset': [0, -1.6],
            'text-anchor': 'top',
          },
          paint: {
            'text-color': '#1A2566',
            'text-halo-color': '#fff',
            'text-halo-width': 1.5,
          },
        });
      }
    });
    return () => { map.remove(); mapRef.current = null; markersRef.current = []; };
  }, [mapReady]);

  // ── Fetch + render the active queue path for the selected attraction ──
  // /active-path returns the admin-placed pins with computed minutes_to_board.
  // We populate both the line source (for the visible polyline) and the pin
  // source (for tap-to-claim hit testing in user mode).
  // [activePathPins] state holds the array so the tap handler can resolve a
  // pin id from a clicked feature.
  const [activePathPins, setActivePathPins] = React.useState([]);
  const activePinsRef = React.useRef([]);
  React.useEffect(() => { activePinsRef.current = activePathPins; }, [activePathPins]);

  React.useEffect(() => {
    if (!mapReady || !mapRef.current) return;
    const map = mapRef.current;
    let cancelled = false;
    const empty = { type: 'FeatureCollection', features: [] };

    const applyEmpty = () => {
      const ls = map.getSource('queue-path');
      const ps = map.getSource('queue-pins');
      if (ls) ls.setData(empty);
      if (ps) ps.setData(empty);
      if (!cancelled) setActivePathPins([]);
    };

    const run = async () => {
      if (!selectedId) { applyEmpty(); return; }
      try {
        const json = await window.PH365_ACTIONS.getActivePath(selectedId);
        if (cancelled) return;
        const pins = json?.pins || [];
        if (pins.length < 2) { applyEmpty(); return; }

        const lineSrc = map.getSource('queue-path');
        const pinSrc  = map.getSource('queue-pins');
        if (!lineSrc || !pinSrc) return;

        lineSrc.setData({
          type: 'FeatureCollection',
          features: [{
            type: 'Feature',
            geometry: { type: 'LineString', coordinates: pins.map(p => [p.lon, p.lat]) },
          }],
        });
        pinSrc.setData({
          type: 'FeatureCollection',
          features: pins.map(p => ({
            type: 'Feature',
            geometry: { type: 'Point', coordinates: [p.lon, p.lat] },
            properties: { id: p.id, ordinal: p.ordinal, minutes_to_board: p.minutes_to_board },
          })),
        });
        setActivePathPins(pins);
      } catch (e) {
        console.warn('[map] active-path fetch failed:', e);
        applyEmpty();
      }
    };

    if (map.isStyleLoaded() && map.getSource('queue-path')) {
      run();
    } else {
      map.once('load', run);
    }
    return () => { cancelled = true; };
  }, [selectedId, mapReady]);

  // ── Calibration state subscription (admin pin-drop) ─────────────────
  const [calib, setCalib] = React.useState(() =>
    window.PH365_ACTIONS?.calibrationSnapshot?.() ?? { active: false });
  React.useEffect(() => {
    if (!window.PH365_ACTIONS?.subscribeCalibration) return;
    return window.PH365_ACTIONS.subscribeCalibration(setCalib);
  }, []);
  const calibRef = React.useRef(calib);
  React.useEffect(() => { calibRef.current = calib; }, [calib]);

  // Render the in-progress calibration line (GeoJSON) and pins (draggable
  // HTML markers). The GeoJSON `calibration-pins` source from map.on('load')
  // stays defined but is no longer populated; markers replace it.
  React.useEffect(() => {
    if (!mapReady || !mapRef.current) return;
    const ml = window.maplibregl;
    const map = mapRef.current;
    const lineSrc = map.getSource('calibration-line');
    if (!lineSrc) return;

    // Line first — connects the pin lat/lons in placement order.
    if (calib.active && calib.pins?.length >= 2) {
      lineSrc.setData({
        type: 'FeatureCollection',
        features: [{
          type: 'Feature',
          geometry: {
            type: 'LineString',
            coordinates: calib.pins.map(p => [Number(p.lon), Number(p.lat)]),
          },
        }],
      });
    } else {
      lineSrc.setData({ type: 'FeatureCollection', features: [] });
    }

    // Markers — match by pin.id so we don't recreate on every snapshot
    // update. Drag-end PATCHes the new coords to the server, which echoes
    // them back into calib state, which re-runs this effect — all idempotent.
    const existing = calibMarkersRef.current;
    const seen = new Set();

    if (calib.active && calib.pins?.length) {
      calib.pins.forEach((pin, idx) => {
        seen.add(pin.id);
        const label = String(idx + 1);
        const lngLat = [Number(pin.lon), Number(pin.lat)];
        let m = existing.get(pin.id);
        if (m) {
          // Reuse — only move/relabel if values changed (avoids fighting
          // an active drag where MapLibre owns the marker position).
          const cur = m.getLngLat();
          if (Math.abs(cur.lng - lngLat[0]) > 1e-7 || Math.abs(cur.lat - lngLat[1]) > 1e-7) {
            m.setLngLat(lngLat);
          }
          if (m._labelEl.textContent !== label) m._labelEl.textContent = label;
        } else {
          const el = document.createElement('div');
          el.className = 'ph365-calib-pin';
          el.textContent = label;
          Object.assign(el.style, {
            width: '32px', height: '32px', borderRadius: '50%',
            background: '#FF7A1F', color: '#fff',
            border: '2px solid #fff',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontFamily: 'Manrope, system-ui, sans-serif',
            fontWeight: '700', fontSize: '13px',
            boxShadow: '0 2px 8px rgba(0,0,0,0.32)',
            cursor: 'grab', userSelect: 'none',
          });
          m = new ml.Marker({ element: el, draggable: true }).setLngLat(lngLat).addTo(map);
          m._pinId = pin.id;
          m._labelEl = el;
          m.on('dragstart', () => { el.style.cursor = 'grabbing'; el.style.transform = (el.style.transform || '') + ' scale(1.1)'; });
          m.on('dragend', async () => {
            el.style.cursor = 'grab';
            el.style.transform = el.style.transform.replace(' scale(1.1)', '');
            const ll = m.getLngLat();
            try {
              await window.PH365_ACTIONS.updateCalibrationPin(pin.id, ll.lat, ll.lng);
            } catch (e) {
              console.warn('[calib] pin update failed; reverting:', e);
              m.setLngLat(lngLat);  // revert visual
            }
          });
          existing.set(pin.id, m);
        }
      });
    }

    // Remove any markers whose pin ids are no longer in the snapshot
    // (after Undo last, Cancel, or Finish).
    for (const [id, m] of existing) {
      if (!seen.has(id)) { m.remove(); existing.delete(id); }
    }
  }, [calib, mapReady]);

  // When entering calibration mode, fly to the attraction's coords so the
  // admin doesn't have to pan around looking for the right ride. Also kick
  // off geolocation so the blue "you are here" dot shows up automatically —
  // admins should immediately see how their physical position compares to
  // the queue route they're tapping out.
  React.useEffect(() => {
    if (!mapReady || !mapRef.current) return;
    if (!calib.active || !calib.attractionId) return;
    const a = (window.PH365.ATTRACTIONS || []).find(x => x.id === calib.attractionId);
    if (a?.lat != null && a?.lon != null) {
      mapRef.current.flyTo({ center: [a.lon, a.lat], zoom: 18, speed: 1.6 });
    }
    // Trigger() is GeolocateControl's public "act as if the user tapped
    // the button" API. Wrap in a try because the first call can throw if
    // permissions haven't been granted yet — the user gets the iOS
    // location prompt, and the dot appears once they accept.
    try { geolocateRef.current?.trigger?.(); }
    catch (e) { console.warn('[map] geolocate trigger failed:', e); }
  }, [calib.active, calib.attractionId, mapReady]);

  // ── Wait estimate when a non-admin user taps the active path ────────
  const [waitEstimate, setWaitEstimate] = React.useState(null);  // { pinId, minutes, attractionId, reportedAt? } | null
  React.useEffect(() => { setWaitEstimate(null); }, [selectedId]);

  // Crowd-sourced wait override. When the selected ride has had a user
  // confirm their position in the last 10 min, we trust that over the
  // queue-times posted wait. Polls every 30s while the card is open.
  const [effectiveWait, setEffectiveWait] = React.useState(null);
  React.useEffect(() => {
    setEffectiveWait(null);
    if (!selectedId) return;
    let cancelled = false;
    const fetchOnce = async () => {
      try {
        const w = await window.PH365_ACTIONS.getEffectiveWait(selectedId);
        if (!cancelled) setEffectiveWait(w);
      } catch (e) { console.warn('[map] effective-wait fetch failed:', e); }
    };
    fetchOnce();
    const id = setInterval(fetchOnce, 30_000);
    return () => { cancelled = true; clearInterval(id); };
  }, [selectedId]);

  // ── Re-fly to park when toggle changes ──────────────────────────────
  React.useEffect(() => {
    if (!mapRef.current) return;
    const v = PARK_VIEWS[view];
    mapRef.current.flyTo({ center: v.center, zoom: v.zoom, speed: 1.4, curve: 1.2 });
    setSelectedId(null);
  }, [view]);

  // ── Re-render markers when active category or edit mode change ─────
  // Note: NOT in this dep list: draftCoords. We don't want to tear down the
  // marker the user is currently dragging — MapLibre owns its position
  // during a drag. Draft changes are reflected by moving just the active
  // marker (effect below) instead of rebuilding the whole layer.
  React.useEffect(() => {
    if (!mapReady || !mapRef.current) return;
    addAttractionPins();
  }, [activeCat, mapReady, editMode, selectedId]);

  // ── When draft coords change (e.g. map-click drop, or Reset), move just
  // the affected marker. Skip near-noop moves so we don't fight an in-flight
  // drag where MapLibre already has the marker at the right spot.
  React.useEffect(() => {
    if (!mapReady || !mapRef.current) return;
    const sel = selectedRef.current;
    if (!sel) return;
    const marker = markersRef.current.find(m => m.attractionId === sel);
    if (!marker) return;
    if (draftCoords && draftCoords.id === sel) {
      const cur = marker.getLngLat();
      if (Math.abs(cur.lat - draftCoords.lat) > 1e-6 || Math.abs(cur.lng - draftCoords.lon) > 1e-6) {
        marker.setLngLat([draftCoords.lon, draftCoords.lat]);
      }
    } else {
      // Reset: snap back to the saved coords on the attraction record
      const a = (window.PH365.ATTRACTIONS || []).find(x => x.id === sel);
      if (a && a.lat != null && a.lon != null) marker.setLngLat([a.lon, a.lat]);
    }
  }, [draftCoords, mapReady]);

  // ── Map clicks: dispatch based on current mode ──────────────────────
  // Three independent click behaviours can be active:
  //   1. Calibration mode (admin) → drop the next pin.
  //   2. Edit-pins mode (admin)   → reposition the selected attraction pin.
  //   3. Click on a queue-pins dot (any user) → show wait-estimate card.
  // (1) and (2) are mutually exclusive; (3) coexists with the "neither" idle case.
  React.useEffect(() => {
    if (!mapReady || !mapRef.current) return;
    const map = mapRef.current;

    async function onMapClick(ev) {
      // 1. Pin-drop calibration takes precedence. If we tapped on an
      //    existing calibration pin layer, undo doesn't trigger here —
      //    use the panel's Undo button.
      if (calibRef.current.active) {
        try {
          await window.PH365_ACTIONS.dropCalibrationPin(ev.lngLat.lat, ev.lngLat.lng);
        } catch (e) { console.warn('[calib] drop pin failed:', e); }
        return;
      }

      // 2. Edit-attraction-pin mode (existing flow).
      if (editModeRef.current) {
        const id = selectedRef.current;
        if (!id) return;
        const { lng, lat } = ev.lngLat;
        setDraftCoords({ id, lat: Number(lat.toFixed(5)), lon: Number(lng.toFixed(5)) });
        return;
      }

      // 3. Queue-pin tap → wait estimate. Tapping a dot is precise; tapping
      //    on the line itself snaps to the nearest pin (line is hard to hit
      //    exactly on a phone). queryRenderedFeatures hit-tests the visible
      //    pixels, so it works for both layers.
      const onPin  = map.queryRenderedFeatures(ev.point, { layers: ['queue-pins-dots'] });
      const onLine = map.queryRenderedFeatures(ev.point, { layers: ['queue-path-line'] });

      if (onPin.length > 0 && selectedRef.current) {
        const props = onPin[0].properties;
        setWaitEstimate({
          pinId:        props.id,
          minutes:      typeof props.minutes_to_board === 'string'
                          ? Number(props.minutes_to_board)
                          : props.minutes_to_board,
          attractionId: selectedRef.current,
          reportedAt:   null,
        });
        return;
      }

      if (onLine.length > 0 && selectedRef.current && activePinsRef.current.length > 0) {
        // Snap to the nearest pin in the active path. Distance in flat
        // lat/lon space is fine at this zoom — the absolute distances are
        // small enough that map projection skew doesn't matter.
        const { lng, lat } = ev.lngLat;
        let closest = null;
        let bestD = Infinity;
        for (const p of activePinsRef.current) {
          const dx = p.lon - lng, dy = p.lat - lat;
          const d = dx * dx + dy * dy;
          if (d < bestD) { bestD = d; closest = p; }
        }
        if (closest) {
          setWaitEstimate({
            pinId:        closest.id,
            minutes:      closest.minutes_to_board,
            attractionId: selectedRef.current,
            reportedAt:   null,
          });
        }
      }
    }
    map.on('click', onMapClick);
    return () => { map.off('click', onMapClick); };
  }, [mapReady]);

  // Cursor feedback on hoverable / tappable layers.
  React.useEffect(() => {
    if (!mapReady || !mapRef.current) return;
    const map = mapRef.current;
    const onEnter = () => { map.getCanvas().style.cursor = 'pointer'; };
    const onLeave = () => { map.getCanvas().style.cursor = ''; };
    for (const layer of ['queue-pins-dots', 'queue-path-line']) {
      map.on('mouseenter', layer, onEnter);
      map.on('mouseleave', layer, onLeave);
    }
    return () => {
      for (const layer of ['queue-pins-dots', 'queue-path-line']) {
        map.off('mouseenter', layer, onEnter);
        map.off('mouseleave', layer, onLeave);
      }
    };
  }, [mapReady]);

  async function reportMyPosition() {
    if (!waitEstimate?.attractionId) return;
    try {
      await window.PH365_ACTIONS.reportQueuePosition({
        attractionId: waitEstimate.attractionId,
        pinId:        waitEstimate.pinId,
      });
      setWaitEstimate(prev => prev ? { ...prev, reportedAt: new Date().toISOString() } : prev);
    } catch (e) {
      console.warn('[map] report failed:', e);
    }
  }

  async function finishCalibration() {
    try {
      await window.PH365_ACTIONS.finishCalibrationSet({ activate: true });
    } catch (e) { alert(e.message || 'Could not finish'); }
  }
  async function undoCalibration() {
    try { await window.PH365_ACTIONS.undoCalibrationPin(); } catch (e) { console.warn(e); }
  }
  async function cancelCalibration() {
    try { await window.PH365_ACTIONS.cancelCalibrationSet(); } catch (e) { console.warn(e); }
  }

  // ── Pin styling ──────────────────────────────────────────────────────
  // Shows get a gold star badge — they have no wait time so a numeric pill
  // would be misleading. Rides get a coloured wait pill keyed off waitColor.
  function styleForPin(a) {
    if (categoryOf(a) === 'shows') {
      return { kind: 'star', bg: '#E8B53C', text: '★', label: 'show' };
    }
    if (a.isOpen === false)   return { kind: 'pill', bg: '#999',           text: '—', label: 'closed' };
    if (!a.lastUpdated)       return { kind: 'pill', bg: COLORS.inkSoft,   text: '·', label: 'loading' };
    return { kind: 'pill', bg: waitColor(a.wait), text: `${a.wait}m`, label: `${a.wait} min` };
  }

  function applyPinStyle(el, s) {
    if (s.kind === 'star') {
      Object.assign(el.style, {
        background: s.bg,
        color: '#fff',
        border: '2px solid #fff',
        borderRadius: '50%',
        padding: '0',
        width: '28px',
        height: '28px',
        font: '700 16px/1 "Fraunces", serif',
        cursor: 'pointer',
        boxShadow: '0 2px 6px rgba(0,0,0,0.35)',
        textAlign: 'center',
        display: 'inline-flex',
        alignItems: 'center',
        justifyContent: 'center',
        whiteSpace: 'nowrap',
      });
    } else {
      Object.assign(el.style, {
        background: s.bg,
        color: '#fff',
        border: '2px solid #fff',
        borderRadius: '999px',
        padding: '3px 8px',
        width: 'auto',
        height: 'auto',
        font: '700 11px/1 "JetBrains Mono", monospace',
        cursor: 'pointer',
        boxShadow: '0 2px 6px rgba(0,0,0,0.35)',
        minWidth: '34px',
        textAlign: 'center',
        display: 'inline-block',
        whiteSpace: 'nowrap',
      });
    }
    el.textContent = s.text;
  }

  // ── Pin construction (reads ATTRACTIONS dynamically from window.PH365) ──
  function addAttractionPins() {
    const ml = window.maplibregl;
    const map = mapRef.current;
    if (!ml || !map) return;

    const ATTRACTIONS = window.PH365.ATTRACTIONS;

    markersRef.current.forEach(m => m.remove());
    markersRef.current = [];

    const draft = draftRef.current;
    const sel   = selectedRef.current;
    const edit  = editModeRef.current;

    for (const a of ATTRACTIONS) {
      // Use draft coords for the pin currently being edited
      const usingDraft = draft && draft.id === a.id;
      const lat = usingDraft ? draft.lat : a.lat;
      const lon = usingDraft ? draft.lon : a.lon;
      if (lat == null || lon == null) continue;
      if (activeCat !== 'all' && categoryOf(a) !== activeCat) continue;
      const s = styleForPin(a);
      const isSel = a.id === sel;
      const isEditingThis = edit && isSel;

      const el = document.createElement('button');
      el.className = 'ph365-pin';
      el.dataset.attractionId = a.id;
      el.setAttribute('aria-label', `${a.name} — ${s.label}`);
      applyPinStyle(el, s);
      // Admin edit highlight overlays whatever base style applyPinStyle set.
      if (isEditingThis) {
        el.style.border = '2px dashed #fff';
        el.style.outline = '2px solid #FFC400';
        el.style.outlineOffset = '2px';
        el.style.cursor = 'grab';
        el.style.boxShadow = '0 0 0 4px rgba(255,196,0,0.35), 0 4px 10px rgba(0,0,0,0.4)';
      }

      el.addEventListener('click', (ev) => {
        ev.stopPropagation();
        // In edit mode, clicking a different pin switches the edit target.
        // Don't fly the map — admin is positioning, not exploring.
        setSelectedId(a.id);
        if (editModeRef.current) {
          setDraftCoords(null); // discard any in-progress draft for the previous pin
        } else {
          map.flyTo({ center: [lon, lat], zoom: 17, speed: 1.6 });
        }
      });

      const marker = new ml.Marker({
        element: el,
        draggable: isEditingThis,
      }).setLngLat([lon, lat]).addTo(map);

      if (isEditingThis) {
        marker.on('dragstart', () => { el.style.cursor = 'grabbing'; });
        marker.on('drag', () => {
          const ll = marker.getLngLat();
          // Live readout — push into draft so the floating card updates
          setDraftCoords({
            id: a.id,
            lat: Number(ll.lat.toFixed(5)),
            lon: Number(ll.lng.toFixed(5)),
          });
        });
        marker.on('dragend', () => { el.style.cursor = 'grab'; });
      }

      marker.attractionId = a.id;
      markersRef.current.push(marker);
    }
  }

  function updatePinsInPlace() {
    if (!markersRef.current.length) return;
    const byId = new Map(window.PH365.ATTRACTIONS.map(a => [a.id, a]));
    let updated = 0;
    for (const m of markersRef.current) {
      const a = byId.get(m.attractionId);
      if (!a) continue;
      const s = styleForPin(a);
      const el = m.getElement();
      applyPinStyle(el, s);
      el.setAttribute('aria-label', `${a.name} — ${s.label}`);
      updated++;
    }
    return updated;
  }

  // ── React to data changes ────────────────────────────────────────────
  React.useEffect(() => {
    if (!mapReady || !mapRef.current) return;
    const onWaits = () => updatePinsInPlace();
    const onDataReady = () => { if (mapRef.current) addAttractionPins(); };
    window.addEventListener('ph365-waits-refreshed', onWaits);
    window.addEventListener('ph365-data-ready', onDataReady);
    updatePinsInPlace();
    return () => {
      window.removeEventListener('ph365-waits-refreshed', onWaits);
      window.removeEventListener('ph365-data-ready', onDataReady);
    };
  }, [mapReady]);

  // ── Admin: save the dragged pin to Supabase ─────────────────────────
  async function saveDraftCoords() {
    const draft = draftCoords;
    if (!draft || !draft.id) return;
    const supabase = window.PH365_SUPABASE;
    if (!supabase) {
      setSaveErr('Supabase not configured');
      return;
    }
    setSaving(true); setSaveErr(null);
    try {
      // Chain .select() so we get back the updated rows. Without this, a
      // Supabase UPDATE returns { error: null } even when RLS silently
      // blocked the write (0 rows affected) — which made saves *look*
      // successful but nothing actually persisted, so pins reverted on
      // page refresh. Reading data.length back is the only reliable way
      // to confirm the write landed.
      const { data, error } = await supabase
        .from('attractions')
        .update({ lat: draft.lat, lon: draft.lon })
        .eq('id', draft.id)
        .select('id, lat, lon');
      if (error) throw error;
      if (!data || data.length === 0) {
        throw new Error(
          'Update affected 0 rows — your account may not be flagged as admin in user_profiles, or the admin RLS migration (20260502010000_admin_bootstrap.sql) has not been applied to this Supabase project.'
        );
      }

      // Use the values the server actually persisted (in case it rounded
      // them to fit the lat/lon NUMERIC(8,5) precision) so the in-memory
      // copy matches what the next reload will fetch.
      const saved = data[0];
      const savedLat = saved.lat != null ? Number(saved.lat) : draft.lat;
      const savedLon = saved.lon != null ? Number(saved.lon) : draft.lon;

      // Mutate window.PH365 in place so the map immediately reflects the
      // saved coords (and other screens that read from PH365).
      const next = (window.PH365.ATTRACTIONS || []).map(a =>
        a.id === draft.id ? { ...a, lat: savedLat, lon: savedLon } : a
      );
      window.PH365 = { ...window.PH365, ATTRACTIONS: next };
      setDraftCoords(null);
      window.dispatchEvent(new CustomEvent('ph365-data-ready'));
      console.info(`[map] saved coords for ${draft.id}: ${savedLat}, ${savedLon}`);
    } catch (err) {
      console.error('[map] save coords failed:', err);
      setSaveErr(err.message || 'Save failed');
    } finally {
      setSaving(false);
    }
  }

  function exitEditMode() {
    setEditMode(false);
    setDraftCoords(null);
    setSaveErr(null);
  }

  const selected = selectedId ? window.PH365.ATTRACTIONS.find(a => a.id === selectedId) : null;

  // ── Layout ───────────────────────────────────────────────────────────
  // Outer container fills the screen-wrap (which is flex:1 below an absent
  // top bar when on the map tab). The map paints edge-to-edge; the floating
  // chip rows and selected-ride card sit on top via z-index.
  return (
    <div style={{
      position: 'absolute',
      inset: 0,
      overflow: 'hidden',
      background: COLORS.paperDeep,
    }}>
      {/* Map container — MapLibre mounts here, fills entire screen */}
      <div ref={containerRef} style={{
        position: 'absolute',
        top: 0, left: 0, right: 0,
        bottom: TAB_BAR_H,
      }} />

      {!mapReady && (
        <div style={{
          position: 'absolute', top: 0, left: 0, right: 0, bottom: TAB_BAR_H,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          background: COLORS.paperDeep, zIndex: 2,
        }}>
          <Caption color={COLORS.inkSoft}>Loading map…</Caption>
        </div>
      )}

      {/* Top floating bar — park toggle + category filters */}
      <div style={{
        position: 'absolute',
        top: 12, left: 12, right: 12,
        zIndex: 5,
        display: 'flex',
        flexDirection: 'column',
        gap: 8,
        pointerEvents: 'none',
      }}>
        {/* Park toggle */}
        <div style={{
          display: 'flex',
          gap: 4,
          padding: 4,
          borderRadius: 999,
          background: 'rgba(255,255,255,0.92)',
          backdropFilter: 'blur(8px)',
          WebkitBackdropFilter: 'blur(8px)',
          border: `1px solid ${COLORS.paperEdge}`,
          boxShadow: '0 4px 14px rgba(0,0,0,0.18)',
          pointerEvents: 'auto',
          alignSelf: 'center',
        }}>
          {[['DLR', 'DLR'], ['DCA', 'DCA'], ['ALL', 'Both']].map(([k, label]) => (
            <button key={k} onClick={() => setView(k)} style={{
              padding: '6px 14px',
              borderRadius: 999,
              background: view === k ? COLORS.ink : 'transparent',
              color: view === k ? COLORS.paper : COLORS.ink,
              border: 'none',
              fontFamily: 'Manrope', fontSize: 11, fontWeight: 700, cursor: 'pointer',
              letterSpacing: '0.04em',
            }}>{label}</button>
          ))}
        </div>

        {/* Admin: edit-pins toggle (only shown for admin users) */}
        {isAdmin && (
          <div style={{
            display: 'flex',
            justifyContent: 'center',
            pointerEvents: 'none',
          }}>
            <button
              onClick={() => editMode ? exitEditMode() : setEditMode(true)}
              aria-pressed={editMode}
              style={{
                pointerEvents: 'auto',
                padding: '6px 12px',
                borderRadius: 999,
                background: editMode ? '#FFC400' : 'rgba(255,255,255,0.92)',
                color: editMode ? '#1A2566' : COLORS.ink,
                border: `1px solid ${editMode ? '#FFC400' : COLORS.paperEdge}`,
                fontFamily: 'Manrope', fontSize: 11, fontWeight: 700, cursor: 'pointer',
                boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
                backdropFilter: 'blur(8px)',
                WebkitBackdropFilter: 'blur(8px)',
                letterSpacing: '0.06em',
                display: 'flex', alignItems: 'center', gap: 6,
              }}
            >
              <span style={{
                display: 'inline-block', width: 6, height: 6, borderRadius: '50%',
                background: editMode ? '#1A2566' : '#FFC400',
              }} />
              {editMode ? 'EDITING PINS — TAP TO EXIT' : 'ADMIN · EDIT PINS'}
            </button>
          </div>
        )}

        {/* Category filter pills — single-select. Tap a chip to focus on
            just that category; tap "All" to clear the filter. */}
        <div style={{
          display: 'flex',
          gap: 6,
          flexWrap: 'wrap',
          justifyContent: 'center',
          pointerEvents: 'none',
        }}>
          {CATEGORIES.map(c => {
            const active = activeCat === c.id;
            return (
              <button
                key={c.id}
                onClick={() => setActiveCat(c.id)}
                aria-pressed={active}
                style={{
                  pointerEvents: 'auto',
                  padding: '6px 12px',
                  borderRadius: 999,
                  background: active ? COLORS.teal : 'rgba(255,255,255,0.92)',
                  color: active ? '#fff' : COLORS.ink,
                  border: `1px solid ${active ? COLORS.teal : COLORS.paperEdge}`,
                  fontFamily: 'Manrope', fontSize: 11, fontWeight: 700, cursor: 'pointer',
                  boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
                  backdropFilter: 'blur(8px)',
                  WebkitBackdropFilter: 'blur(8px)',
                  letterSpacing: '0.02em',
                }}
              >{c.label}</button>
            );
          })}
        </div>
      </div>

      {/* ─── Calibration panel — admin pin-drop mode ─── */}
      {calib.active && (() => {
        const a = (window.PH365.ATTRACTIONS || []).find(x => x.id === calib.attractionId);
        const minutes = calib.lastPinAt
          ? Math.max(0, Math.round((Date.now() - new Date(calib.lastPinAt).getTime()) / 60000))
          : null;
        return (
          <div style={{
            position: 'absolute',
            left: 12, right: 12,
            bottom: TAB_BAR_H + 12,
            zIndex: 6,
          }}>
            <div style={{
              background: 'rgba(255,255,255,0.98)',
              border: `2px solid #FF7A1F`,
              borderRadius: 14,
              padding: '12px 14px',
              boxShadow: '0 6px 20px rgba(0,0,0,0.28)',
            }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                <span style={{
                  fontFamily: '"JetBrains Mono"', fontSize: 9, fontWeight: 700,
                  color: '#fff', background: '#FF7A1F', padding: '2px 6px',
                  borderRadius: 4, letterSpacing: '0.12em',
                }}>CALIBRATING</span>
                <Body size={14} weight={700} style={{
                  flex: 1, minWidth: 0,
                  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                }}>{a?.name ?? calib.attractionId}</Body>
              </div>
              <div style={{ display: 'flex', gap: 14, marginBottom: 10 }}>
                <div>
                  <Caption style={{ fontSize: 9 }}>Pins dropped</Caption>
                  <div style={{ fontFamily: '"JetBrains Mono"', fontSize: 22, fontWeight: 700 }}>
                    {calib.pinCount}
                  </div>
                </div>
                <div>
                  <Caption style={{ fontSize: 9 }}>Since last drop</Caption>
                  <div style={{ fontFamily: '"JetBrains Mono"', fontSize: 22, fontWeight: 700,
                    color: minutes != null && minutes >= 1 ? COLORS.moss : COLORS.inkSoft }}>
                    {minutes != null ? `${minutes}m` : '—'}
                  </div>
                </div>
              </div>
              <Caption style={{ marginBottom: 10, color: COLORS.inkSoft, lineHeight: 1.45 }}>
                Tap the map at your current spot in line, ideally once per minute.
                The last pin should be the boarding point.
              </Caption>
              <div style={{ display: 'flex', gap: 8 }}>
                <button
                  onClick={cancelCalibration}
                  style={{
                    flex: 1, padding: '10px 12px', borderRadius: 10,
                    background: 'transparent', color: COLORS.inkSoft,
                    border: `1px solid ${COLORS.paperEdge}`,
                    fontFamily: 'Manrope', fontSize: 12, fontWeight: 600, cursor: 'pointer',
                  }}
                >Cancel</button>
                <button
                  onClick={undoCalibration}
                  disabled={calib.pinCount === 0}
                  style={{
                    flex: 1, padding: '10px 12px', borderRadius: 10,
                    background: 'transparent', color: COLORS.ink,
                    border: `1px solid ${COLORS.paperEdge}`,
                    fontFamily: 'Manrope', fontSize: 12, fontWeight: 600,
                    cursor: calib.pinCount === 0 ? 'not-allowed' : 'pointer',
                    opacity: calib.pinCount === 0 ? 0.5 : 1,
                  }}
                >Undo last</button>
                <button
                  onClick={finishCalibration}
                  disabled={calib.pinCount < 2}
                  style={{
                    flex: 1.5, padding: '10px 12px', borderRadius: 10,
                    background: calib.pinCount < 2 ? COLORS.paperEdge : '#FF7A1F',
                    color: '#fff', border: 'none',
                    fontFamily: 'Manrope', fontSize: 12, fontWeight: 700,
                    cursor: calib.pinCount < 2 ? 'not-allowed' : 'pointer',
                    boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.25)',
                  }}
                >Finish & save</button>
              </div>
            </div>
          </div>
        );
      })()}

      {/* ─── Wait estimate card — when user taps a pin on the active path ─── */}
      {!calib.active && waitEstimate && (() => {
        const a = (window.PH365.ATTRACTIONS || []).find(x => x.id === waitEstimate.attractionId);
        const m = waitEstimate.minutes;
        const accent = m == null ? COLORS.inkSoft
          : m <= 10 ? COLORS.moss
          : m <= 25 ? COLORS.mustard
          : COLORS.clay;
        return (
          <div style={{
            position: 'absolute',
            left: 12, right: 12,
            bottom: TAB_BAR_H + 12,
            zIndex: 6,
          }}>
            <div style={{
              background: 'rgba(255,255,255,0.98)',
              border: `1px solid ${COLORS.paperEdge}`,
              borderRadius: 14,
              padding: '14px 16px',
              boxShadow: '0 6px 20px rgba(0,0,0,0.22)',
            }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
                <Caption color={COLORS.teal} style={{ letterSpacing: '0.12em' }}>YOU TAPPED · IF YOU'RE HERE</Caption>
                <button
                  onClick={() => setWaitEstimate(null)}
                  aria-label="Close"
                  style={{
                    marginLeft: 'auto', background: 'transparent', border: 'none',
                    cursor: 'pointer', padding: 4, display: 'flex',
                  }}
                ><Icon name="close" size={14} color={COLORS.inkSoft} /></button>
              </div>
              <Body size={13} weight={600} style={{ marginBottom: 8 }}>
                {a?.name ?? 'This ride'}
              </Body>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 10 }}>
                <span style={{
                  fontFamily: '"JetBrains Mono"', fontWeight: 700, fontSize: 36, color: accent,
                  letterSpacing: '-0.02em',
                }}>
                  {m != null ? `~${m}` : '—'}
                </span>
                <span style={{ fontFamily: 'Manrope', fontSize: 14, fontWeight: 600, color: accent }}>
                  min from boarding
                </span>
              </div>
              <Body size={11.5} color={COLORS.inkSoft} style={{ lineHeight: 1.5, marginBottom: 10 }}>
                Based on the most recent admin calibration walk. Tap "I'm here" to confirm — that helps tune wait estimates with today's actual queue speed.
              </Body>
              <button
                onClick={reportMyPosition}
                disabled={waitEstimate.reportedAt != null}
                style={{
                  width: '100%', padding: '10px 12px', borderRadius: 10,
                  background: waitEstimate.reportedAt ? COLORS.paperDeep : COLORS.teal,
                  color: waitEstimate.reportedAt ? COLORS.inkSoft : COLORS.paper,
                  border: waitEstimate.reportedAt ? `1px solid ${COLORS.paperEdge}` : 'none',
                  fontFamily: 'Manrope', fontSize: 13, fontWeight: 700,
                  cursor: waitEstimate.reportedAt ? 'default' : 'pointer',
                  boxShadow: waitEstimate.reportedAt ? 'none' : 'inset 0 1px 0 rgba(255,255,255,0.18)',
                }}
              >{waitEstimate.reportedAt ? '✓ Position reported — thanks!' : "I'm here — confirm"}</button>
            </div>
          </div>
        );
      })()}

      {/* Edit-mode hint when no pin selected yet */}
      {editMode && !selected && (
        <div style={{
          position: 'absolute',
          left: 12, right: 12,
          bottom: TAB_BAR_H + 12,
          zIndex: 5,
        }}>
          <div style={{
            background: 'rgba(26,37,102,0.96)',
            color: '#FFC400',
            border: `1px solid #FFC400`,
            borderRadius: 14,
            padding: '12px 14px',
            boxShadow: '0 6px 20px rgba(0,0,0,0.32)',
            fontFamily: 'Manrope', fontSize: 13, fontWeight: 600,
            textAlign: 'center', letterSpacing: '0.02em',
          }}>
            Tap any pin to start editing its location.
          </div>
        </div>
      )}

      {/* Selected ride card — floats above the bottom tab bar */}
      {selected && (() => {
        const LANDS = window.PH365.LANDS;
        const isClosed = selected.isOpen === false;
        const hasLive  = selected.lastUpdated != null;
        const draft = draftCoords && draftCoords.id === selected.id ? draftCoords : null;
        const curLat = draft ? draft.lat : selected.lat;
        const curLon = draft ? draft.lon : selected.lon;
        const dirty  = !!draft;

        // Admin edit panel: replaces the standard card while edit mode is on
        if (editMode) {
          return (
            <div style={{
              position: 'absolute',
              left: 12, right: 12,
              bottom: TAB_BAR_H + 12,
              zIndex: 5,
            }}>
              <div style={{
                background: 'rgba(255,255,255,0.98)',
                backdropFilter: 'blur(8px)',
                WebkitBackdropFilter: 'blur(8px)',
                border: `2px solid #FFC400`,
                borderRadius: 14,
                padding: '12px 14px',
                boxShadow: '0 6px 20px rgba(0,0,0,0.28)',
              }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
                  <span style={{
                    fontFamily: '"JetBrains Mono"', fontSize: 9, fontWeight: 700,
                    color: '#1A2566', background: '#FFC400', padding: '2px 6px',
                    borderRadius: 4, letterSpacing: '0.12em',
                  }}>EDITING PIN</span>
                  <Body size={14} weight={700} style={{
                    flex: 1, minWidth: 0,
                    whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                  }}>{selected.name}</Body>
                </div>
                <Caption style={{ marginBottom: 8 }}>
                  {LANDS[selected.land]?.name ?? selected.land} · {selected.type}
                </Caption>
                <div style={{
                  display: 'flex', gap: 8, alignItems: 'center', marginBottom: 10,
                  fontFamily: '"JetBrains Mono"', fontSize: 12, fontWeight: 600,
                  color: dirty ? COLORS.clay : COLORS.inkSoft,
                }}>
                  <span>lat {curLat != null ? Number(curLat).toFixed(5) : '—'}</span>
                  <span>lon {curLon != null ? Number(curLon).toFixed(5) : '—'}</span>
                  {dirty && <span style={{ color: COLORS.clay }}>· unsaved</span>}
                </div>
                <Caption style={{ marginBottom: 10, color: COLORS.inkSoft }}>
                  Drag the highlighted pin, or tap anywhere on the map to drop it there.
                </Caption>
                {saveErr && (
                  <Body size={12} color={COLORS.clay} style={{ marginBottom: 8 }}>{saveErr}</Body>
                )}
                <div style={{ display: 'flex', gap: 8 }}>
                  <button
                    onClick={() => setDraftCoords(null)}
                    disabled={!dirty || saving}
                    style={{
                      flex: 1, padding: '10px 12px', borderRadius: 10,
                      background: 'transparent',
                      color: dirty ? COLORS.ink : COLORS.inkSoft,
                      border: `1px solid ${COLORS.paperEdge}`,
                      fontFamily: 'Manrope', fontSize: 13, fontWeight: 700,
                      cursor: dirty && !saving ? 'pointer' : 'not-allowed',
                    }}
                  >Reset</button>
                  <button
                    onClick={saveDraftCoords}
                    disabled={!dirty || saving}
                    style={{
                      flex: 2, padding: '10px 12px', borderRadius: 10,
                      background: dirty ? COLORS.teal : COLORS.paperEdge,
                      color: '#fff',
                      border: 'none',
                      fontFamily: 'Manrope', fontSize: 13, fontWeight: 700,
                      cursor: dirty && !saving ? 'pointer' : 'not-allowed',
                      letterSpacing: '0.04em',
                    }}
                  >{saving ? 'Saving…' : (dirty ? 'Lock in location' : 'Drag pin to edit')}</button>
                </div>
              </div>
            </div>
          );
        }

        return (
          <div style={{
            position: 'absolute',
            left: 12, right: 12,
            bottom: TAB_BAR_H + 12,
            zIndex: 5,
          }}>
            <div onClick={() => onSelectAttraction(selected.id)} style={{
              background: 'rgba(255,255,255,0.97)',
              backdropFilter: 'blur(8px)',
              WebkitBackdropFilter: 'blur(8px)',
              border: `1px solid ${COLORS.paperEdge}`,
              borderRadius: 14,
              padding: '12px 14px',
              cursor: 'pointer',
              display: 'flex',
              alignItems: 'center',
              gap: 12,
              boxShadow: '0 6px 20px rgba(0,0,0,0.22)',
            }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <Body size={14} weight={700} style={{
                  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                }}>{selected.name}</Body>
                <Caption style={{ marginTop: 2 }}>
                  {LANDS[selected.land]?.name ?? selected.land} · {selected.type}
                  {isAdmin && curLat != null && curLon != null && (
                    <span style={{ marginLeft: 6, fontFamily: '"JetBrains Mono"', color: COLORS.inkSoft }}>
                      · {Number(curLat).toFixed(4)}, {Number(curLon).toFixed(4)}
                    </span>
                  )}
                </Caption>
              </div>
              {(() => {
                // Crowd-sourced wait wins when present; falls back to
                // queue-times' selected.wait. Source dot communicates which.
                const userMin = effectiveWait?.source === 'user' ? effectiveWait.minutes : null;
                const showMin = userMin != null ? userMin : selected.wait;
                const showColor = userMin != null
                  ? (userMin <= 15 ? COLORS.moss : userMin <= 35 ? COLORS.mustard : COLORS.clay)
                  : waitColor(selected.wait);
                if (selected.type === 'Show') return (
                  <span style={{ fontFamily: 'Fraunces, serif', fontSize: 22, fontWeight: 600, color: '#E8B53C' }}>★ Show</span>
                );
                if (isClosed) return (
                  <span style={{ fontFamily: '"JetBrains Mono"', fontSize: 12, fontWeight: 700, color: COLORS.inkSoft }}>Closed</span>
                );
                if (showMin == null && !hasLive) return (
                  <span style={{ fontFamily: '"JetBrains Mono"', fontSize: 11, fontWeight: 600, color: COLORS.inkSoft }}>Loading…</span>
                );
                return (
                  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
                    <Stamp value={showMin} unit="min" size={22} color={showColor} />
                    {userMin != null && (
                      <span style={{
                        fontFamily: '"JetBrains Mono"', fontSize: 8, fontWeight: 700,
                        color: COLORS.moss, letterSpacing: '0.1em', marginTop: 2,
                      }}>● PASSHOLDER · {Math.max(1, Math.round((effectiveWait.age_seconds ?? 0) / 60))}M AGO</span>
                    )}
                  </div>
                );
              })()}
              <Icon name="chevron" size={14} color={COLORS.inkSoft} />
            </div>
          </div>
        );
      })()}
    </div>
  );
}

window.MapScreen = MapScreen;
})();
