/**
 * <folk-map-viewer> -- real-time location sharing map.
 *
 * Creates/joins map rooms, shows participant locations on a map,
 * and provides location sharing controls.
 *
 * Demo mode: real MapLibre GL map showing a simulated festival meetup
 * with animated participants, waypoints, and a route line — using the
 * same UI as the real app.
 */

import { RoomSync, type RoomState, type ParticipantState, type LocationState, type ParticipantStatus, type PrecisionLevel, type PrivacySettings, type WaypointType } from "./map-sync";
import { loadRoomHistory, saveRoomVisit, updateRoomThumbnail } from "./map-room-history";
import { MapPushManager } from "./map-push";
import { fuzzLocation, haversineDistance, formatDistance, formatTime } from "./map-privacy";
import { getModuleApiBase } from "../../../shared/url-helpers";
import "./map-privacy-panel";
import { TourEngine } from "../../../shared/tour-engine";
import { ViewHistory } from "../../../shared/view-history.js";
import { requireAuth } from "../../../shared/auth-fetch";
import { getUsername } from "../../../shared/components/rstack-identity";
import { MapsLocalFirstClient } from "../local-first-client";
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import type { DocumentId } from "../../../shared/local-first/document";
import { mapsSchema, mapsDocId } from "../schemas";

// MapLibre loaded via CDN — use window access with type assertion

const MAPLIBRE_CSS = "https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.css";
const MAPLIBRE_JS = "https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.js";

const OSM_ATTRIBUTION = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';

const DARK_STYLE = {
	version: 8,
	sources: {
		osm: {
			type: "raster",
			tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
			tileSize: 256,
			attribution: OSM_ATTRIBUTION,
			maxzoom: 19,
		},
	},
	layers: [{ id: "osm", type: "raster", source: "osm" }],
};

const LIGHT_STYLE = {
	version: 8,
	sources: {
		osm: {
			type: "raster",
			tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
			tileSize: 256,
			attribution: OSM_ATTRIBUTION,
			maxzoom: 19,
		},
	},
	layers: [{ id: "osm", type: "raster", source: "osm" }],
};

const PARTICIPANT_COLORS = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#ec4899", "#14b8a6", "#f97316"];
const EMOJIS = ["\u{1F600}", "\u{1F60E}", "\u{1F913}", "\u{1F973}", "\u{1F98A}", "\u{1F431}", "\u{1F436}", "\u{1F984}", "\u{1F31F}", "\u{1F525}", "\u{1F49C}", "\u{1F3AE}"];
const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes

// ─── Demo constants: simulated festival meetup ──────────────────
// Ziegeleipark Mildenberg — CCC festival site near Berlin
const DEMO_CENTER: [number, number] = [13.0406, 52.9753];

const DEMO_PARTICIPANTS: { id: string; name: string; emoji: string; color: string; status: "online" | "away" | "ghost"; speed: number; route: [number, number][] }[] = [
	{
		id: "demo-alice", name: "Alice", emoji: "\u{1F98A}", color: "#ef4444", status: "online", speed: 1,
		route: [[13.0390, 52.9758], [13.0398, 52.9762], [13.0408, 52.9760], [13.0415, 52.9755], [13.0410, 52.9748], [13.0400, 52.9750]],
	},
	{
		id: "demo-boris", name: "Boris", emoji: "\u{1F43A}", color: "#3b82f6", status: "online", speed: 1,
		route: [[13.0420, 52.9745], [13.0425, 52.9750], [13.0430, 52.9755], [13.0425, 52.9760], [13.0418, 52.9758], [13.0415, 52.9750]],
	},
	{
		id: "demo-chen", name: "Chen", emoji: "\u{1F31F}", color: "#f59e0b", status: "away", speed: 0.5,
		route: [[13.0380, 52.9745], [13.0385, 52.9748], [13.0390, 52.9745], [13.0385, 52.9742]],
	},
	{
		id: "demo-dana", name: "Dana", emoji: "\u{1F3AE}", color: "#22c55e", status: "online", speed: 1,
		route: [[13.0405, 52.9770], [13.0412, 52.9768], [13.0418, 52.9765], [13.0412, 52.9762], [13.0405, 52.9765]],
	},
	{
		id: "demo-eve", name: "Eve", emoji: "\u{1F52E}", color: "#8b5cf6", status: "ghost", speed: 0,
		route: [[13.0435, 52.9740]],
	},
];

const DEMO_WAYPOINTS: { id: string; name: string; emoji: string; lat: number; lng: number }[] = [
	{ id: "wp-stage", name: "Main Stage", emoji: "\u{1F3B5}", lat: 52.9760, lng: 13.0410 },
	{ id: "wp-hacker", name: "Hacker Center", emoji: "\u{1F4BB}", lat: 52.9750, lng: 13.0425 },
	{ id: "wp-cafe", name: "Chaos Cafe", emoji: "\u2615", lat: 52.9745, lng: 13.0388 },
	{ id: "wp-parking", name: "Parking", emoji: "\u{1F17F}", lat: 52.9738, lng: 13.0440 },
];

// Pre-computed route from Alice's start to Main Stage (~320m outdoor path)
const DEMO_ROUTE: [number, number][] = [
	[13.0390, 52.9758], [13.0393, 52.9759], [13.0396, 52.9760],
	[13.0400, 52.9761], [13.0403, 52.9761], [13.0406, 52.9761],
	[13.0408, 52.9760], [13.0410, 52.9760],
];

class FolkMapViewer extends HTMLElement {
	private shadow: ShadowRoot;
	private space = "";
	private room = "";
	private view: "lobby" | "map" = "lobby";
	private rooms: string[] = [];
	private loading = false;
	private error = "";
	private syncStatus: "disconnected" | "connected" = "disconnected";
	// Demo mode state
	private _demoState: RoomState | null = null;
	private _demoAnimParticipants: { id: string; route: [number, number][]; progress: number; speed: number }[] = [];
	private _demoInterval: ReturnType<typeof setInterval> | null = null;

	// MapLibre + sync state (room mode)
	private map: any = null;
	private participantMarkers: Map<string, any> = new Map();
	private waypointMarkers: Map<string, any> = new Map();
	private sync: RoomSync | null = null;
	private syncUrl = "";
	private participantId = "";
	private userName = "";
	private userEmoji = "";
	private userColor = "";
	private sharingLocation = false;
	private watchId: number | null = null;
	private pushManager: MapPushManager | null = null;
	private privacySettings: PrivacySettings = { precision: "exact", ghostMode: false };
	private showPrivacyPanel = false;
	private geoPermissionState: PermissionState | "" = "";
	private geoTimeoutCount = 0;
	// Modals/panels state
	private showShareModal = false;
	private showMeetingModal = false;
	private showImportModal = false;
	private showEmojiPicker = false;
	private stalenessTimer: ReturnType<typeof setInterval> | null = null;
	private selectedParticipant: string | null = null;
	private selectedWaypoint: string | null = null;
	private activeRoute: { segments: any[]; totalDistance: number; estimatedTime: number; destination: string } | null = null;
	private thumbnailTimer: ReturnType<typeof setTimeout> | null = null;
	private _themeObserver: MutationObserver | null = null;
	private _history = new ViewHistory<"lobby" | "map">("lobby", "rmaps");
	private _stopPresence: (() => void) | null = null;
	private _offlineUnsub: (() => void) | null = null;
	private _subscribedDocIds: string[] = [];

	// Chat + Local-first state
	private lfClient: MapsLocalFirstClient | null = null;
	private sidebarTab: "participants" | "chat" = "participants";
	private unreadCount = 0;

	// Indoor/outdoor mode
	private mapMode: "outdoor" | "indoor" = "outdoor";
	private indoorEvent: string | null = null;
	private indoorView: any = null;

	// Guided tour
	private _tour!: TourEngine;
	private static readonly TOUR_STEPS = [
		{ target: '#create-room', title: "Create a Room", message: "Start a new map room to share locations with others in real time.", advanceOnClick: true },
		{ target: '.room-card, .history-card', title: "Join a Room", message: "Click any room card to enter and see participants on the map.", advanceOnClick: false },
		{ target: '#share-location', title: "Share Location", message: "Toggle location sharing so others in the room can see where you are.", advanceOnClick: false },
		{ target: '#drop-waypoint', title: "Drop a Pin", message: "Drop a waypoint pin on the map to mark a point of interest for everyone.", advanceOnClick: false },
	];

	private isDarkTheme(): boolean {
		const theme = document.documentElement.getAttribute("data-theme");
		if (theme) return theme === "dark";
		return window.matchMedia("(prefers-color-scheme: dark)").matches;
	}

	constructor() {
		super();
		this.shadow = this.attachShadow({ mode: "open" });
		this._tour = new TourEngine(
			this.shadow,
			FolkMapViewer.TOUR_STEPS,
			"rmaps_tour_done",
			() => this.shadow.host as HTMLElement,
		);
	}

	connectedCallback() {
		this.space = this.getAttribute("space") || "demo";
		this.room = this.getAttribute("room") || "";
		if (this.space === "demo") {
			this.loadDemoData();
		} else {
			this.loadUserProfile();
			this.pushManager = new MapPushManager(this.getApiBase());
			if (this.room) {
				this.joinRoom(this.room);
			} else {
				this.checkSyncHealth();
				this.render();
			}
			this.subscribeOffline();
		}
		if (!localStorage.getItem("rmaps_tour_done")) {
			setTimeout(() => this._tour.start(), 1200);
		}

		// When inside a folk-shape, block map interactions until editing mode
		const parentShape = this.closest("folk-shape");
		if (parentShape) {
			this._parentShape = parentShape;
			this._onEditEnter = () => this.setMapInteractive(true);
			this._onEditExit = () => this.setMapInteractive(false);
			parentShape.addEventListener("edit-enter", this._onEditEnter);
			parentShape.addEventListener("edit-exit", this._onEditExit);
			// Start with interactions disabled
			this.setMapInteractive(false);
		}
		this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rmaps', context: this.room || 'Maps' }));
		window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
	}

	disconnectedCallback() {
		this._history.destroy();
		window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
		this._stopPresence?.();
		this._offlineUnsub?.(); this._offlineUnsub = null;
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (runtime) {
			for (const id of this._subscribedDocIds) runtime.unsubscribe(id);
		}
		this._subscribedDocIds = [];
		if (this._demoInterval) { clearInterval(this._demoInterval); this._demoInterval = null; }
		this.leaveRoom();
		if (this._themeObserver) {
			this._themeObserver.disconnect();
			this._themeObserver = null;
		}
		if (this.stalenessTimer) { clearInterval(this.stalenessTimer); this.stalenessTimer = null; }
		if (this._parentShape) {
			this._parentShape.removeEventListener("edit-enter", this._onEditEnter!);
			this._parentShape.removeEventListener("edit-exit", this._onEditExit!);
			this._parentShape = null;
		}
	}

	private _parentShape: Element | null = null;
	private _onEditEnter: (() => void) | null = null;
	private _onEditExit: (() => void) | null = null;
	private _mapInteractive = true;

	private async subscribeOffline() {
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (!runtime?.isInitialized) return;
		try {
			const docId = mapsDocId(this.space) as DocumentId;
			await runtime.subscribe(docId, mapsSchema);
			this._subscribedDocIds.push(docId);
		} catch { /* runtime unavailable */ }
	}

	private setMapInteractive(interactive: boolean) {
		this._mapInteractive = interactive;
		if (this.map) {
			if (interactive) {
				this.map.scrollZoom?.enable();
				this.map.boxZoom?.enable();
				this.map.dragRotate?.enable();
				this.map.dragPan?.enable();
				this.map.keyboard?.enable();
				this.map.doubleClickZoom?.enable();
				this.map.touchZoomRotate?.enable();
			} else {
				this.map.scrollZoom?.disable();
				this.map.boxZoom?.disable();
				this.map.dragRotate?.disable();
				this.map.dragPan?.disable();
				this.map.keyboard?.disable();
				this.map.doubleClickZoom?.disable();
				this.map.touchZoomRotate?.disable();
			}
		}
		// Also toggle pointer-events on the map container
		const container = this.shadow.getElementById("map-container");
		if (container) {
			container.style.pointerEvents = interactive ? "" : "none";
		}
	}

	// ─── User profile ────────────────────────────────────────────

	private loadUserProfile() {
		try {
			const saved = JSON.parse(localStorage.getItem("rmaps_user") || "null");
			if (saved) {
				this.participantId = saved.id;
				this.userName = saved.name;
				this.userEmoji = saved.emoji;
				this.userColor = saved.color;
				return;
			}
		} catch {}
		this.participantId = crypto.randomUUID();
		this.userEmoji = EMOJIS[Math.floor(Math.random() * EMOJIS.length)];
		this.userColor = PARTICIPANT_COLORS[Math.floor(Math.random() * PARTICIPANT_COLORS.length)];
	}

	private changeEmoji(emoji: string) {
		this.userEmoji = emoji;
		localStorage.setItem("rmaps_user", JSON.stringify({
			id: this.participantId,
			name: this.userName,
			emoji: this.userEmoji,
			color: this.userColor,
		}));
		// Update sync state so other participants see the change
		if (this.sync) {
			const state = this.sync.getState();
			const me = state.participants[this.participantId];
			if (me) {
				me.emoji = emoji;
				// Re-join with updated emoji
				this.sync.join({ ...me, emoji });
			}
		}
		// Update own marker if present
		const myMarker = this.participantMarkers.get(this.participantId);
		if (myMarker) {
			const el = myMarker.getElement?.();
			if (el) {
				const emojiSpan = el.querySelector(".marker-emoji");
				if (emojiSpan) emojiSpan.textContent = emoji;
			}
		}
		this.showEmojiPicker = false;
		this.updateEmojiButton();
	}

	private ensureUserProfile(): boolean {
		if (this.userName) return true;
		// Use EncryptID username if authenticated
		const identityName = getUsername();
		if (identityName) {
			this.userName = identityName;
			localStorage.setItem("rmaps_user", JSON.stringify({
				id: this.participantId,
				name: this.userName,
				emoji: this.userEmoji,
				color: this.userColor,
			}));
			return true;
		}
		return false;
	}

	private pendingRoomSlug = "";

	private showJoinForm(slug: string) {
		this.pendingRoomSlug = slug;
		const saved = JSON.parse(localStorage.getItem("rmaps_user") || "null");
		const savedName = saved?.name || "";
		const savedEmoji = saved?.emoji || this.userEmoji;

		const overlay = document.createElement("div");
		overlay.id = "join-form-overlay";
		overlay.style.cssText = `
			position:fixed;inset:0;z-index:50;background:rgba(0,0,0,0.6);
			display:flex;align-items:center;justify-content:center;
			backdrop-filter:blur(4px);
		`;

		const card = document.createElement("div");
		card.style.cssText = `
			background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);
			border-radius:16px;padding:28px;width:340px;max-width:90vw;
			box-shadow:0 16px 48px rgba(0,0,0,0.4);text-align:center;
			font-family:system-ui,-apple-system,sans-serif;
		`;

		card.innerHTML = `
			<div style="font-size:48px;margin-bottom:4px;" id="join-avatar-preview">${savedEmoji}</div>
			<div style="font-size:18px;font-weight:700;color:var(--rs-text-primary);margin-bottom:4px;">Join ${this.esc(slug)}</div>
			<div style="font-size:13px;color:var(--rs-text-muted);margin-bottom:16px;">Choose your avatar and display name</div>
			<div style="display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-bottom:16px;">
				${EMOJIS.map(e => `<button class="join-emoji-opt" data-e="${e}" style="width:36px;height:36px;border-radius:8px;border:2px solid ${e === savedEmoji ? "#4f46e5" : "transparent"};background:${e === savedEmoji ? "#4f46e520" : "var(--rs-bg-surface-sunken)"};font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:border-color 0.15s;">${e}</button>`).join("")}
			</div>
			<input id="join-name-input" type="text" placeholder="Your display name" value="${this.esc(savedName)}" style="
				width:100%;padding:10px 14px;border-radius:8px;border:1px solid var(--rs-border);
				background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:15px;
				outline:none;margin-bottom:12px;box-sizing:border-box;
			">
			${savedName ? `<button id="join-quick-btn" style="
				width:100%;padding:10px;border-radius:8px;border:none;
				background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:14px;
				margin-bottom:8px;
			">Join as ${this.esc(savedEmoji)} ${this.esc(savedName)}</button>` : ""}
			<button id="join-submit-btn" style="
				width:100%;padding:10px;border-radius:8px;border:1px solid var(--rs-border);
				background:var(--rs-bg-surface);color:var(--rs-text-primary);font-weight:600;cursor:pointer;font-size:14px;
			">${savedName ? "Join with new name" : "Join Room"}</button>
			<button id="join-cancel-btn" style="
				margin-top:8px;background:none;border:none;color:var(--rs-text-muted);
				cursor:pointer;font-size:12px;padding:4px;
			">Cancel</button>
		`;

		overlay.appendChild(card);
		this.shadow.appendChild(overlay);

		let selectedEmoji = savedEmoji;
		const preview = card.querySelector("#join-avatar-preview") as HTMLElement;
		const nameInput = card.querySelector("#join-name-input") as HTMLInputElement;

		card.querySelectorAll(".join-emoji-opt").forEach(btn => {
			btn.addEventListener("click", () => {
				selectedEmoji = (btn as HTMLElement).dataset.e!;
				preview.textContent = selectedEmoji;
				card.querySelectorAll(".join-emoji-opt").forEach(b => {
					const isSelected = (b as HTMLElement).dataset.e === selectedEmoji;
					(b as HTMLElement).style.borderColor = isSelected ? "#4f46e5" : "transparent";
					(b as HTMLElement).style.background = isSelected ? "#4f46e520" : "var(--rs-bg-surface-sunken)";
				});
			});
		});

		const doJoin = (name: string, emoji: string) => {
			if (!name.trim()) { nameInput.style.borderColor = "#ef4444"; return; }
			this.userName = name.trim();
			this.userEmoji = emoji;
			localStorage.setItem("rmaps_user", JSON.stringify({
				id: this.participantId, name: this.userName,
				emoji: this.userEmoji, color: this.userColor,
			}));
			overlay.remove();
			this.joinRoom(this.pendingRoomSlug);
		};

		card.querySelector("#join-quick-btn")?.addEventListener("click", () => doJoin(savedName, savedEmoji));
		card.querySelector("#join-submit-btn")?.addEventListener("click", () => doJoin(nameInput.value, selectedEmoji));
		nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") doJoin(nameInput.value, selectedEmoji); });
		card.querySelector("#join-cancel-btn")?.addEventListener("click", () => overlay.remove());
		overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });

		nameInput.focus();
	}

	// ─── Demo mode ───────────────────────────────────────────────

	private loadDemoData() {
		this.view = "map";
		this.room = "festival-demo";
		this.syncStatus = "connected";
		this.participantId = "demo-alice";
		this.userName = "Alice";
		this.userEmoji = "\u{1F98A}";
		this.userColor = "#ef4444";

		// Initialize animated participant states
		this._demoAnimParticipants = DEMO_PARTICIPANTS.map(p => ({
			id: p.id,
			route: p.route,
			progress: Math.random() * p.route.length, // stagger start positions
			speed: p.speed,
		}));

		// Build initial state
		this._demoState = this.buildDemoState();

		// Render using the real map UI
		this.render();

		// Initialize real MapLibre GL map
		this.initMapView().then(() => {
			if (!this.map) return;

			// Override center/zoom for festival site
			this.map.setCenter(DEMO_CENTER);
			this.map.setZoom(16);

			const onReady = () => {
				// Place markers via the real onRoomStateChange pipeline
				if (this._demoState) this.onRoomStateChange(this._demoState);
				// Draw demo route line
				this.showDemoRoute();
				// Start animation interval
				this._demoInterval = setInterval(() => this.tickDemoAnimation(), 4000);
			};
			if (this.map.loaded()) onReady();
			else this.map.once("load", onReady);
		});
	}

	private buildDemoState(): RoomState {
		const now = new Date().toISOString();
		const participants: Record<string, ParticipantState> = {};

		for (const dp of DEMO_PARTICIPANTS) {
			const anim = this._demoAnimParticipants.find(a => a.id === dp.id);
			const routeLen = dp.route.length;
			const idx = anim ? Math.floor(anim.progress) % routeLen : 0;
			const nextIdx = (idx + 1) % routeLen;
			const frac = anim ? anim.progress - Math.floor(anim.progress) : 0;

			// Interpolate position
			const lng = dp.route[idx][0] + (dp.route[nextIdx][0] - dp.route[idx][0]) * frac;
			const lat = dp.route[idx][1] + (dp.route[nextIdx][1] - dp.route[idx][1]) * frac;

			// Compute heading from movement direction
			const dlng = dp.route[nextIdx][0] - dp.route[idx][0];
			const dlat = dp.route[nextIdx][1] - dp.route[idx][1];
			const heading = Math.atan2(dlng, dlat) * (180 / Math.PI);

			participants[dp.id] = {
				id: dp.id,
				name: dp.name,
				emoji: dp.emoji,
				color: dp.color,
				joinedAt: now,
				lastSeen: now,
				status: dp.status,
				location: {
					latitude: lat,
					longitude: lng,
					accuracy: 5,
					heading: dp.speed > 0 ? heading : undefined,
					timestamp: now,
					source: "gps",
				},
			};
		}

		return {
			id: "festival-demo",
			slug: "festival-demo",
			name: "Festival Demo",
			createdAt: now,
			participants,
			waypoints: DEMO_WAYPOINTS.map(w => ({
				id: w.id,
				name: w.name,
				emoji: w.emoji,
				latitude: w.lat,
				longitude: w.lng,
				createdBy: "demo-system",
				createdAt: now,
				type: "meeting" as WaypointType,
			})),
		};
	}

	private tickDemoAnimation() {
		for (const anim of this._demoAnimParticipants) {
			const dp = DEMO_PARTICIPANTS.find(p => p.id === anim.id);
			if (!dp || dp.speed === 0) continue;
			// Advance 25% toward next waypoint (full segment in 4 ticks = 16s)
			anim.progress += 0.25 * anim.speed;
			if (anim.progress >= dp.route.length) anim.progress -= dp.route.length;
		}
		this._demoState = this.buildDemoState();
		if (this._demoState) this.onRoomStateChange(this._demoState);
	}

	private showDemoRoute() {
		if (!this.map) return;
		// Draw a route line from Alice's start to Main Stage
		this.map.addSource("demo-route", {
			type: "geojson",
			data: {
				type: "Feature",
				properties: {},
				geometry: {
					type: "LineString",
					coordinates: DEMO_ROUTE,
				},
			},
		});
		this.map.addLayer({
			id: "demo-route-line",
			type: "line",
			source: "demo-route",
			layout: { "line-join": "round", "line-cap": "round" },
			paint: {
				"line-color": "#4f46e5",
				"line-width": 4,
				"line-opacity": 0.7,
				"line-dasharray": [2, 1],
			},
		});
	}

	private showDemoToast(msg: string) {
		// Remove existing toast
		this.shadow.getElementById("demo-toast")?.remove();
		const toast = document.createElement("div");
		toast.id = "demo-toast";
		toast.style.cssText = `
			position:fixed;bottom:100px;left:50%;transform:translateX(-50%);z-index:100;
			background:rgba(15,23,42,0.95);color:#e2e8f0;padding:10px 20px;border-radius:10px;
			font-size:13px;font-family:system-ui,sans-serif;border:1px solid rgba(255,255,255,0.1);
			backdrop-filter:blur(8px);box-shadow:0 8px 24px rgba(0,0,0,0.4);
			opacity:0;transition:opacity 0.2s ease;white-space:nowrap;
		`;
		toast.textContent = msg;
		this.shadow.appendChild(toast);
		requestAnimationFrame(() => { toast.style.opacity = "1"; });
		setTimeout(() => { toast.style.opacity = "0"; setTimeout(() => toast.remove(), 200); }, 2800);
	}

	private attachDemoMapListeners() {
		// Back button
		this.shadow.querySelectorAll("[data-back]").forEach((el) => {
			el.addEventListener("click", () => this.goBack());
		});

		// Functional: locate me (fly to Alice)
		this.shadow.getElementById("locate-me-fab")?.addEventListener("click", () => {
			if (!this.map || !this._demoState) return;
			const alice = this._demoState.participants["demo-alice"];
			if (alice?.location) {
				this.map.flyTo({ center: [alice.location.longitude, alice.location.latitude], zoom: 17 });
			}
			const btn = this.shadow.getElementById("locate-me-fab");
			if (btn) { btn.style.background = "#4285f4"; btn.style.color = "#fff"; setTimeout(() => { btn.style.background = "var(--rs-bg-surface)"; btn.style.color = "var(--rs-text-secondary)"; }, 1500); }
		});

		// Functional: fit all participants
		this.shadow.getElementById("fit-all-fab")?.addEventListener("click", () => {
			if (!this.map || !this._demoState || !(window as any).maplibregl) return;
			const bounds = new (window as any).maplibregl.LngLatBounds();
			for (const p of Object.values(this._demoState.participants)) {
				if (p.location) bounds.extend([p.location.longitude, p.location.latitude]);
			}
			this.map.fitBounds(bounds, { padding: 60, maxZoom: 17 });
		});

		// Functional: sidebar toggle
		this.shadow.getElementById("header-participants-toggle")?.addEventListener("click", () => {
			const sidebar = this.shadow.querySelector(".map-sidebar") as HTMLElement;
			if (sidebar && window.innerWidth > 768) {
				sidebar.style.display = sidebar.style.display === "none" ? "" : "none";
			}
			const sheet = this.shadow.getElementById("mobile-bottom-sheet");
			if (sheet) sheet.classList.toggle("expanded");
		});

		// Sidebar tab switching (functional)
		this.shadow.querySelectorAll("[data-sidebar-tab]").forEach((btn) => {
			btn.addEventListener("click", () => {
				this.sidebarTab = (btn as HTMLElement).dataset.sidebarTab as "participants" | "chat";
				const pList = this.shadow.getElementById("participant-list");
				const cPanel = this.shadow.getElementById("chat-panel-container");
				if (pList) pList.style.display = this.sidebarTab === "participants" ? "block" : "none";
				if (cPanel) cPanel.style.display = this.sidebarTab === "chat" ? "block" : "none";
				this.shadow.querySelectorAll("[data-sidebar-tab]").forEach(b => {
					const isActive = (b as HTMLElement).dataset.sidebarTab === this.sidebarTab;
					(b as HTMLElement).style.borderColor = isActive ? "#4f46e5" : "var(--rs-border)";
					(b as HTMLElement).style.background = isActive ? "#4f46e520" : "transparent";
					(b as HTMLElement).style.color = isActive ? "#818cf8" : "var(--rs-text-muted)";
				});
				if (this.sidebarTab === "chat") this.showDemoToast("Chat requires a live room. Sign up to try it!");
			});
		});

		// Mobile floating buttons
		this.shadow.getElementById("mobile-friends-btn")?.addEventListener("click", () => {
			const sheet = this.shadow.getElementById("mobile-bottom-sheet");
			if (sheet) sheet.classList.toggle("expanded");
		});
		this.shadow.getElementById("sheet-close-btn")?.addEventListener("click", () => {
			const s = this.shadow.getElementById("mobile-bottom-sheet");
			if (s) s.classList.remove("expanded");
		});
		const sheet = this.shadow.getElementById("mobile-bottom-sheet");
		const sheetHandle = this.shadow.getElementById("sheet-handle");
		if (sheet && sheetHandle) {
			sheetHandle.addEventListener("click", () => sheet.classList.toggle("expanded"));
			let startY = 0;
			let sheetWasExpanded = false;
			sheetHandle.addEventListener("touchstart", (e: Event) => { const te = e as TouchEvent; startY = te.touches[0].clientY; sheetWasExpanded = sheet.classList.contains("expanded"); }, { passive: true });
			sheetHandle.addEventListener("touchend", (e: Event) => { const te = e as TouchEvent; const dy = te.changedTouches[0].clientY - startY; if (sheetWasExpanded && dy > 40) sheet.classList.remove("expanded"); else if (!sheetWasExpanded && dy < -40) sheet.classList.add("expanded"); }, { passive: true });
		}

		// Restricted controls — show toast
		const restricted = (msg: string) => () => this.showDemoToast(msg);
		this.shadow.getElementById("share-location")?.addEventListener("click", restricted("Sign up to share your location"));
		this.shadow.getElementById("header-share-toggle")?.addEventListener("click", restricted("Sign up to share your location"));
		this.shadow.getElementById("map-share-float")?.addEventListener("click", restricted("Sign up to share your location"));
		this.shadow.getElementById("drop-waypoint")?.addEventListener("click", restricted("Sign up to drop pins"));
		this.shadow.getElementById("share-room-btn")?.addEventListener("click", restricted("Sign up to share rooms"));
		this.shadow.getElementById("bell-toggle")?.addEventListener("click", restricted("Sign up to enable notifications"));
		this.shadow.getElementById("privacy-toggle")?.addEventListener("click", restricted("Sign up to configure privacy"));
		this.shadow.getElementById("indoor-toggle")?.addEventListener("click", restricted("Sign up to use indoor navigation"));
		this.shadow.getElementById("emoji-picker-btn")?.addEventListener("click", restricted("Sign up to customize your avatar"));
		this.shadow.getElementById("mobile-qr-btn")?.addEventListener("click", restricted("Sign up to share via QR"));
	}

	// ─── Room mode: API / health ─────────────────────────────────

	private getApiBase(): string {
		const path = window.location.pathname;
		const match = path.match(/^(\/[^/]+)?\/rmaps/);
		return match ? match[0] : "";
	}

	private async checkSyncHealth() {
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(3000) });
			if (res.ok) {
				const data = await res.json();
				this.syncStatus = data.sync !== false ? "connected" : "disconnected";
			}
		} catch {
			this.syncStatus = "disconnected";
		}
		this.render();
	}

	private async loadStats() {
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/stats`, { signal: AbortSignal.timeout(3000) });
			if (res.ok) {
				const data = await res.json();
				this.rooms = Object.keys(data.rooms || {});
			}
		} catch {
			this.rooms = [];
		}
		this.render();
	}

	// ─── Room mode: join / leave / create ────────────────────────

	private joinRoom(slug: string) {
		if (!this.ensureUserProfile()) {
			this.showJoinForm(slug);
			return;
		}
		this.room = slug;
		this.view = "map";
		this.render();
		this.initMapView().then(() => this.restoreLastLocation());
		this.initRoomSync();
		this.initLocalFirstClient();
		// Periodically refresh staleness indicators
		this.stalenessTimer = setInterval(() => this.refreshStaleness(), 15000);
		// Auto-start sharing if user had it enabled previously
		if (localStorage.getItem(`rmaps_sharing_${slug}`) === "true") {
			setTimeout(() => this.toggleLocationSharing(), 500);
		}
	}

	private restoreLastLocation() {
		try {
			const saved = JSON.parse(localStorage.getItem(`rmaps_loc_${this.room}`) || "null");
			if (saved && this.map && Date.now() - saved.ts < 30 * 60 * 1000) {
				this.map.flyTo({ center: [saved.lng, saved.lat], zoom: 13 });
			}
		} catch {}
	}

	private async initLocalFirstClient() {
		this.lfClient = new MapsLocalFirstClient(this.space);
		await this.lfClient.init();
		await this.lfClient.subscribe();
		// Track unread chat messages
		this.lfClient.onChange((doc) => {
			const msgs = Object.values(doc.messages || {});
			const lastSeen = parseInt(localStorage.getItem(`rmaps_chat_seen_${this.room}`) || "0", 10);
			this.unreadCount = msgs.filter(m => m.createdAt > lastSeen).length;
			this.updateChatBadge();
		});
	}

	private createRoom() {
		if (!requireAuth("create map room")) return;
		this.showCreateRoomForm();
	}

	private showCreateRoomForm() {
		const overlay = document.createElement("div");
		overlay.id = "create-room-overlay";
		overlay.style.cssText = `
			position:fixed;inset:0;z-index:50;background:rgba(0,0,0,0.6);
			display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);
		`;
		const card = document.createElement("div");
		card.style.cssText = `
			background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);
			border-radius:16px;padding:28px;width:340px;max-width:90vw;
			box-shadow:0 16px 48px rgba(0,0,0,0.4);text-align:center;
			font-family:system-ui,-apple-system,sans-serif;
		`;
		card.innerHTML = `
			<div style="font-size:32px;margin-bottom:8px;">&#127760;</div>
			<div style="font-size:18px;font-weight:700;color:var(--rs-text-primary);margin-bottom:4px;">Create Room</div>
			<div style="font-size:13px;color:var(--rs-text-muted);margin-bottom:16px;">Enter a name for your new map room</div>
			<input id="room-name-input" type="text" placeholder="Room name (e.g. berlin-meetup)" style="
				width:100%;padding:10px 14px;border-radius:8px;border:1px solid var(--rs-border);
				background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:15px;
				outline:none;margin-bottom:12px;box-sizing:border-box;
			">
			<button id="room-create-btn" style="
				width:100%;padding:10px;border-radius:8px;border:none;
				background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:14px;
			">Create & Join</button>
			<button id="room-cancel-btn" style="
				margin-top:8px;background:none;border:none;color:var(--rs-text-muted);
				cursor:pointer;font-size:12px;padding:4px;
			">Cancel</button>
		`;
		overlay.appendChild(card);
		this.shadow.appendChild(overlay);

		const input = card.querySelector("#room-name-input") as HTMLInputElement;
		const doCreate = () => {
			const name = input.value.trim();
			if (!name) { input.style.borderColor = "#ef4444"; return; }
			const slug = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
			overlay.remove();
			this.joinRoom(slug);
		};
		card.querySelector("#room-create-btn")?.addEventListener("click", doCreate);
		input.addEventListener("keydown", (e) => { if (e.key === "Enter") doCreate(); });
		card.querySelector("#room-cancel-btn")?.addEventListener("click", () => overlay.remove());
		overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
		input.focus();
	}

	private leaveRoom() {
		this.captureThumbnail();
		if (this.watchId !== null) {
			navigator.geolocation.clearWatch(this.watchId);
			this.watchId = null;
		}
		this.sharingLocation = false;
		if (this.sync) {
			this.sync.leave();
			this.sync = null;
		}
		this.participantMarkers.forEach((m) => m.remove());
		this.participantMarkers.clear();
		this.waypointMarkers.forEach((m) => m.remove());
		this.waypointMarkers.clear();
		if (this.map) {
			this.map.remove();
			this.map = null;
		}
		if (this.indoorView) {
			this.indoorView.remove();
			this.indoorView = null;
		}
		this.mapMode = "outdoor";
		this.indoorEvent = null;
		if (this.lfClient) {
			this.lfClient.disconnect();
			this.lfClient = null;
		}
		this.sidebarTab = "participants";
		this.unreadCount = 0;
		if (this._themeObserver) {
			this._themeObserver.disconnect();
			this._themeObserver = null;
		}
		if (this.thumbnailTimer) clearTimeout(this.thumbnailTimer);
		if (this.stalenessTimer) { clearInterval(this.stalenessTimer); this.stalenessTimer = null; }
	}

	// ─── MapLibre GL ─────────────────────────────────────────────

	private async loadMapLibre(): Promise<void> {
		if ((window as any).maplibregl) return;
		const link = document.createElement("link");
		link.rel = "stylesheet";
		link.href = MAPLIBRE_CSS;
		document.head.appendChild(link);
		return new Promise<void>((resolve, reject) => {
			const script = document.createElement("script");
			script.src = MAPLIBRE_JS;
			script.onload = () => resolve();
			script.onerror = () => reject(new Error("Failed to load MapLibre GL"));
			document.head.appendChild(script);
		});
	}

	private async initMapView() {
		try {
			await this.loadMapLibre();
		} catch {
			const container = this.shadow.getElementById("map-container");
			if (container) {
				container.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--rs-text-secondary,#94a3b8);font-size:0.9rem;text-align:center;padding:2rem">Map unavailable — check your connection and try reloading.</div>`;
			}
			return;
		}
		const container = this.shadow.getElementById("map-container");
		if (!container || !(window as any).maplibregl) return;

		this.map = new (window as any).maplibregl.Map({
			container,
			style: this.isDarkTheme() ? DARK_STYLE : LIGHT_STYLE,
			center: [0, 20],
			zoom: 2,
			preserveDrawingBuffer: true,
		});

		this.map.addControl(new (window as any).maplibregl.NavigationControl(), "top-right");
		this.map.addControl(new (window as any).maplibregl.ScaleControl(), "bottom-left");

		// If inside a folk-shape and not in editing mode, disable map interactions
		if (this._parentShape && !this._mapInteractive) {
			this.setMapInteractive(false);
		}

		// Apply dark mode inversion filter to OSM tiles
		this.applyDarkFilter();

		// Theme observer — swap map tiles on toggle
		this._themeObserver = new MutationObserver(() => {
			this.map?.setStyle(this.isDarkTheme() ? DARK_STYLE : LIGHT_STYLE);
			this.applyDarkFilter();
			this.updateMarkerTheme();
		});
		this._themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });

		// Debounced thumbnail capture on moveend
		this.map.on("moveend", () => {
			if (this.thumbnailTimer) clearTimeout(this.thumbnailTimer);
			this.thumbnailTimer = setTimeout(() => this.captureThumbnail(), 3000);
		});

		// Initial thumbnail capture after tiles load
		this.map.on("load", () => {
			setTimeout(() => this.captureThumbnail(), 2000);
		});
	}

	// ─── Room sync ───────────────────────────────────────────────

	private async initRoomSync() {
		// Fetch sync URL from server
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/sync-url`, { signal: AbortSignal.timeout(3000) });
			if (res.ok) {
				const data = await res.json();
				this.syncUrl = data.syncUrl || "";
			}
		} catch {}

		this.sync = new RoomSync(
			this.room,
			this.participantId,
			(state) => this.onRoomStateChange(state),
			(connected) => {
				this.syncStatus = connected ? "connected" : "disconnected";
				const dot = this.shadow.querySelector(".status-dot");
				if (dot) {
					dot.className = `status-dot ${connected ? "status-connected" : "status-disconnected"}`;
				}
			},
			undefined, // onLocationRequest (default)
			(fromId, fromName, fromEmoji) => this.onRouteRequest(fromId, fromName, fromEmoji),
		);

		this.sync.connect(this.syncUrl || undefined);

		const now = new Date().toISOString();
		this.sync.join({
			id: this.participantId,
			name: this.userName,
			emoji: this.userEmoji,
			color: this.userColor,
			joinedAt: now,
			lastSeen: now,
			status: "online",
		});

		saveRoomVisit(this.room, this.room);
		try { localStorage.setItem("rmaps_last_room", this.room); } catch {}

		// Listen for SW-forwarded location request pushes
		if ("serviceWorker" in navigator) {
			navigator.serviceWorker.addEventListener("message", (event) => {
				if (event.data?.type === "LOCATION_REQUEST") {
					const reqRoom = event.data.data?.roomSlug;
					if (reqRoom === this.room) {
						this.showPingToast(event.data.data?.fromName || "Someone");
						if (this.sharingLocation) {
							// Already sharing — sync will propagate automatically
						} else if (this.privacySettings.precision !== "hidden") {
							// Not sharing yet — start sharing in response to ping
							this.toggleLocationSharing();
						}
					}
				}
			});
		}
	}

	// ─── Persist room state to SW IndexedDB for offline access ──

	private persistRoomState(state: RoomState) {
		if (!this.room || !("serviceWorker" in navigator)) return;
		navigator.serviceWorker.controller?.postMessage({
			type: "SAVE_ROOM_STATE",
			roomSlug: this.room,
			state,
		});
	}

	// ─── State change → update markers ───────────────────────────

	private onRoomStateChange(state: RoomState) {
		// Persist to IndexedDB for offline pinging
		this.persistRoomState(state);

		if (!this.map || !(window as any).maplibregl) {
			console.warn("[rMaps] onRoomStateChange: map not ready", { map: !!this.map, maplibregl: !!(window as any).maplibregl });
			return;
		}

		const currentIds = new Set<string>();
		if (this.space === "demo") console.log("[rMaps demo] onRoomStateChange:", Object.keys(state.participants).length, "participants,", state.waypoints.length, "waypoints");

		// Update participant markers
		for (const [id, p] of Object.entries(state.participants)) {
			currentIds.add(id);
			if (p.location) {
				const lngLat: [number, number] = [p.location.longitude, p.location.latitude];
				const ageMs = Date.now() - new Date(p.location.timestamp).getTime();
				const isStale = ageMs > STALE_THRESHOLD_MS;

				if (this.participantMarkers.has(id)) {
					const marker = this.participantMarkers.get(id);
					marker.setLngLat(lngLat);
					// Update staleness + heading on existing marker
					const el = marker.getElement?.();
					if (el) {
						el.style.opacity = isStale ? "0.5" : "1";
						if (isStale) {
							el.style.borderColor = "#6b7280";
						} else {
							el.style.borderColor = p.color;
						}
						// Update heading arrow
						const arrow = el.querySelector(".heading-arrow") as HTMLElement | null;
						if (p.location.heading !== undefined && p.location.heading !== null) {
							if (arrow) {
								arrow.style.transform = `translateX(-50%) rotate(${p.location.heading}deg)`;
								arrow.style.display = "block";
								arrow.style.borderBottomColor = isStale ? "#6b7280" : p.color;
							}
						} else if (arrow) {
							arrow.style.display = "none";
						}
						// Update tooltip with age
						const ageSec = Math.floor(ageMs / 1000);
						const ageLabel = ageSec < 60 ? `${ageSec}s ago` : ageSec < 3600 ? `${Math.floor(ageSec / 60)}m ago` : "stale";
						el.title = `${p.name} - ${p.status} (${ageLabel})`;
					}
				} else {
					const isSelf = id === this.participantId;
					const dark = this.isDarkTheme();
					const markerBg = dark ? '#1a1a2e' : '#fafaf7';
					const textShadow = dark ? 'rgba(0,0,0,0.8)' : 'rgba(0,0,0,0.3)';
					const el = document.createElement("div");
					el.className = "participant-marker";

					if (isSelf) {
						// Self-marker: pulsing blue dot
						el.dataset.selfMarker = "true";
						el.style.cssText = `
							width: 20px; height: 20px; border-radius: 50%;
							background: #4285f4; border: 3px solid #fff;
							cursor: pointer; position: relative;
							box-shadow: 0 0 8px rgba(66,133,244,0.6);
						`;
						// Animated pulse ring
						const ring = document.createElement("div");
						ring.style.cssText = `
							position: absolute; top: 50%; left: 50%;
							width: 36px; height: 36px; border-radius: 50%;
							border: 2px solid #4285f4; opacity: 0;
							transform: translate(-50%, -50%);
							animation: selfPulse 2s ease-out infinite;
							pointer-events: none;
						`;
						el.appendChild(ring);
					} else {
						el.style.cssText = `
							width: 36px; height: 36px; border-radius: 50%;
							border: 3px solid ${isStale ? "#6b7280" : p.color}; background: ${markerBg};
							display: flex; align-items: center; justify-content: center;
							font-size: 18px; cursor: pointer; position: relative;
							box-shadow: 0 0 8px ${p.color}60;
							opacity: ${isStale ? "0.5" : "1"};
							transition: opacity 0.3s;
						`;

						// Emoji span with class for later updates
						const emojiSpan = document.createElement("span");
						emojiSpan.className = "marker-emoji";
						emojiSpan.textContent = p.emoji;
						el.appendChild(emojiSpan);

						// Status dot overlay (bottom-right corner)
						const statusDot = document.createElement("div");
						statusDot.className = "marker-status-dot";
						const dotColor = isStale ? "#6b7280" : (
							p.status === "online" ? "#22c55e" : p.status === "away" ? "#f59e0b" :
							p.status === "ghost" ? "#64748b" : "#ef4444"
						);
						statusDot.style.cssText = `
							position:absolute;bottom:-1px;right:-1px;
							width:10px;height:10px;border-radius:50%;
							background:${dotColor};border:2px solid ${markerBg};
							box-shadow:0 0 6px ${dotColor};
						`;
						el.appendChild(statusDot);
					}

					// Staleness tooltip
					const ageSec = Math.floor(ageMs / 1000);
					const ageLabel = ageSec < 60 ? `${ageSec}s ago` : ageSec < 3600 ? `${Math.floor(ageSec / 60)}m ago` : "stale";
					el.title = isSelf ? "You" : `${p.name} - ${p.status} (${ageLabel})`;

					// Heading arrow (CSS triangle)
					const arrow = document.createElement("div");
					arrow.className = "heading-arrow";
					const arrowColor = isSelf ? "#4285f4" : p.color;
					arrow.style.cssText = `
						position: absolute; top: ${isSelf ? "-8px" : "-6px"}; left: 50%;
						width: 0; height: 0;
						border-left: 4px solid transparent;
						border-right: 4px solid transparent;
						border-bottom: 8px solid ${arrowColor};
						transform: translateX(-50%)${p.location.heading !== undefined ? ` rotate(${p.location.heading}deg)` : ""};
						display: ${p.location.heading !== undefined ? "block" : "none"};
					`;
					el.appendChild(arrow);

					// Name label below (skip for self)
					if (!isSelf) {
						const label = document.createElement("div");
						label.className = "marker-label";
						label.style.cssText = `
							position: absolute; bottom: -18px; left: 50%; transform: translateX(-50%);
							font-size: 10px; color: ${p.color}; font-weight: 600;
							white-space: nowrap; text-shadow: 0 1px 3px ${textShadow};
							font-family: system-ui, sans-serif;
						`;
						label.textContent = p.name;
						el.appendChild(label);
					}

					el.addEventListener("click", () => {
						if (this.space === "demo") {
							if (isSelf) {
								// Fly to self location
								if (this.map && p.location) this.map.flyTo({ center: [p.location.longitude, p.location.latitude], zoom: 17 });
							} else {
								this.showDemoToast("Sign up to use navigation");
							}
							return;
						}
						if (isSelf) {
							this.locateMe();
						} else {
							this.selectedParticipant = id;
							this.selectedWaypoint = null;
							this.renderNavigationPanel();
						}
					});

					const marker = new (window as any).maplibregl.Marker({ element: el })
						.setLngLat(lngLat)
						.addTo(this.map);
					this.participantMarkers.set(id, marker);
				}
			}
		}

		// Remove departed participants
		for (const [id, marker] of this.participantMarkers) {
			if (!currentIds.has(id) || !state.participants[id]?.location) {
				marker.remove();
				this.participantMarkers.delete(id);
			}
		}

		// Update waypoint markers
		const wpIds = new Set(state.waypoints.map((w) => w.id));
		for (const wp of state.waypoints) {
			if (!this.waypointMarkers.has(wp.id)) {
				const el = document.createElement("div");
				el.style.cssText = `
					width: 28px; height: 28px; border-radius: 50%;
					background: #4f46e5; border: 2px solid #818cf8;
					display: flex; align-items: center; justify-content: center;
					font-size: 14px; cursor: pointer;
				`;
				el.textContent = wp.emoji || "\u{1F4CD}";
				el.title = wp.name;
				el.addEventListener("click", () => {
					if (this.space === "demo") { this.showDemoToast("Sign up to use navigation"); return; }
					this.selectedWaypoint = wp.id;
					this.selectedParticipant = null;
					this.renderNavigationPanel();
				});
				const marker = new (window as any).maplibregl.Marker({ element: el })
					.setLngLat([wp.longitude, wp.latitude])
					.addTo(this.map);
				this.waypointMarkers.set(wp.id, marker);
			}
		}
		for (const [id, marker] of this.waypointMarkers) {
			if (!wpIds.has(id)) {
				marker.remove();
				this.waypointMarkers.delete(id);
			}
		}

		// Forward to indoor view if in indoor mode
		if (this.mapMode === "indoor" && this.indoorView) {
			this.indoorView.updateParticipants(state.participants);
		}

		// Update participant counts (header + mobile)
		const pCount = String(Object.keys(state.participants).length);
		const headerCount = this.shadow.getElementById("header-participant-count");
		if (headerCount) headerCount.textContent = pCount;
		const mobileCount = this.shadow.getElementById("mobile-friends-count");
		if (mobileCount) mobileCount.textContent = pCount;

		// Update participant list sidebar
		this.updateParticipantList(state);
	}

	private buildParticipantHTML(state: RoomState): string {
		// Dedup by name (keep most recent)
		const byName = new Map<string, ParticipantState>();
		for (const p of Object.values(state.participants)) {
			const existing = byName.get(p.name);
			if (!existing || new Date(p.lastSeen) > new Date(existing.lastSeen)) {
				byName.set(p.name, p);
			}
		}
		const entries = Array.from(byName.values());
		const myLoc = state.participants[this.participantId]?.location;
		const statusColors: Record<string, string> = { online: "#22c55e", away: "#f59e0b", ghost: "#64748b", offline: "#ef4444" };

		return entries.map((p) => {
			let distLabel = "";
			if (myLoc && p.location && p.id !== this.participantId) {
				distLabel = formatDistance(haversineDistance(myLoc.latitude, myLoc.longitude, p.location.latitude, p.location.longitude));
			}
			const statusColor = statusColors[p.status] || "#64748b";
			let ageLabel = "";
			let isStale = false;
			if (p.location) {
				const ageMs = Date.now() - new Date(p.location.timestamp).getTime();
				isStale = ageMs > STALE_THRESHOLD_MS;
				const ageSec = Math.floor(ageMs / 1000);
				ageLabel = ageSec < 60 ? `${ageSec}s ago` : ageSec < 3600 ? `${Math.floor(ageSec / 60)}m ago` : "stale";
			}
			const glowColor = isStale ? "#6b7280" : statusColor;
			return `
			<div class="participant-entry" style="display:flex;align-items:center;gap:8px;padding:8px 4px;border-bottom:1px solid var(--rs-border);${isStale ? "opacity:0.6;" : ""}">
				<div style="position:relative;flex-shrink:0;">
					<span style="font-size:20px;display:block;width:28px;text-align:center;">${this.esc(p.emoji)}</span>
					<span style="position:absolute;bottom:-1px;right:-1px;width:10px;height:10px;border-radius:50%;background:${glowColor};border:2px solid var(--rs-bg-surface);box-shadow:0 0 6px ${glowColor};"></span>
				</div>
				<div style="flex:1;min-width:0;">
					<div style="font-size:13px;font-weight:600;color:${isStale ? "#6b7280" : p.color};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${this.esc(p.name)}</div>
					<div style="font-size:10px;color:var(--rs-text-muted);">
						${p.status === "ghost" ? "ghost mode" : p.location ? (isStale ? ageLabel : "sharing") : "no location"}
						${distLabel ? ` \u2022 ${distLabel}` : ""}
					</div>
				</div>
				${p.id !== this.participantId && p.location ? `<button class="nav-to-btn" data-nav-participant="${p.id}" title="Navigate" style="background:none;border:1px solid var(--rs-border);border-radius:6px;color:var(--rs-text-muted);cursor:pointer;padding:3px 8px;font-size:12px;">&#129517;</button>` : ""}
				${p.id !== this.participantId ? `<button class="ping-btn-inline" data-ping="${p.id}" title="Ping" style="background:none;border:1px solid var(--rs-border);border-radius:6px;color:var(--rs-text-muted);cursor:pointer;padding:3px 8px;font-size:12px;">&#128276;</button>` : ""}
			</div>`;
		}).join("");
	}

	private attachParticipantListeners(container: HTMLElement) {
		if (this.space === "demo") {
			// In demo mode, wire ping/nav buttons to toast instead of sync calls
			container.querySelectorAll("[data-ping]").forEach((btn) => {
				btn.addEventListener("click", () => this.showDemoToast("Sign up to ping friends"));
			});
			container.querySelectorAll("[data-nav-participant]").forEach((btn) => {
				btn.addEventListener("click", () => this.showDemoToast("Sign up to use navigation"));
			});
			container.querySelector("#sidebar-ping-all-btn")?.addEventListener("click", () => this.showDemoToast("Sign up to ping friends"));
			container.querySelector("#sidebar-meeting-btn")?.addEventListener("click", () => this.showDemoToast("Sign up to set meeting points"));
			container.querySelector("#sidebar-import-btn")?.addEventListener("click", () => this.showDemoToast("Sign up to import places"));
			return;
		}
		container.querySelectorAll("[data-ping]").forEach((btn) => {
			btn.addEventListener("click", () => {
				const pid = (btn as HTMLElement).dataset.ping!;
				this.pushManager?.requestLocation(this.room, pid);
				(btn as HTMLElement).textContent = "\u2713";
				setTimeout(() => { (btn as HTMLElement).textContent = "\u{1F514}"; }, 2000);
			});
		});
		container.querySelectorAll("[data-nav-participant]").forEach((btn) => {
			btn.addEventListener("click", () => {
				this.selectedParticipant = (btn as HTMLElement).dataset.navParticipant!;
				this.selectedWaypoint = null;
				this.renderNavigationPanel();
			});
		});
		container.querySelector("#sidebar-ping-all-btn")?.addEventListener("click", () => {
			this.pushManager?.requestLocation(this.room, "all");
			const btn = container.querySelector("#sidebar-ping-all-btn");
			if (btn) { btn.textContent = "\u2713 Pinged!"; setTimeout(() => { btn.textContent = "\u{1F4E2} Ping All"; }, 2000); }
		});
		container.querySelector("#sidebar-meeting-btn")?.addEventListener("click", () => {
			this.showMeetingModal = true;
			this.renderMeetingPointModal();
		});
		container.querySelector("#sidebar-import-btn")?.addEventListener("click", () => {
			this.showImportModal = true;
			this.renderImportModal();
		});
	}

	private updateParticipantList(state: RoomState) {
		const list = this.shadow.getElementById("participant-list");
		const mobileList = this.shadow.getElementById("participant-list-mobile");

		const count = Object.keys(state.participants).length;
		const html = this.buildParticipantHTML(state);
		const headerHTML = `
			<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
				<span style="font-size:12px;font-weight:600;color:var(--rs-text-secondary);text-transform:uppercase;letter-spacing:0.06em;">Friends (${count})</span>
				<button id="sidebar-ping-all-btn" style="padding:3px 8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:10px;white-space:nowrap;">&#128226; Ping All</button>
			</div>
		`;
		const footerHTML = `
			<div style="display:flex;gap:6px;margin-top:10px;flex-wrap:wrap;">
				<button id="sidebar-meeting-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">&#128205; Meeting Point</button>
				<button id="sidebar-import-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">&#128229; Import Places</button>
			</div>
		`;

		// Desktop sidebar
		if (list) {
			list.innerHTML = headerHTML + html + footerHTML;
			this.attachParticipantListeners(list);
		}

		// Mobile bottom sheet
		if (mobileList) {
			mobileList.innerHTML = html + footerHTML;
			this.attachParticipantListeners(mobileList);
			const sheetCount = this.shadow.getElementById("sheet-participant-count");
			if (sheetCount) sheetCount.textContent = String(count);
		}
	}

	private updateMarkerTheme() {
		const dark = this.isDarkTheme();
		const markerBg = dark ? '#1a1a2e' : '#fafaf7';
		const textShadow = dark ? 'rgba(0,0,0,0.8)' : 'rgba(0,0,0,0.3)';
		for (const marker of this.participantMarkers.values()) {
			const el = marker.getElement?.();
			if (!el) continue;
			// Skip self-marker (always blue)
			if (el.dataset?.selfMarker) continue;
			el.style.background = markerBg;
			const label = el.querySelector('.marker-label') as HTMLElement | null;
			if (label) label.style.textShadow = `0 1px 3px ${textShadow}`;
		}
	}

	/** Periodically refresh staleness visuals on all participant markers */
	private refreshStaleness() {
		const state = this.sync?.getState();
		if (!state) return;
		for (const [id, p] of Object.entries(state.participants)) {
			if (!p.location) continue;
			const marker = this.participantMarkers.get(id);
			if (!marker) continue;
			const el = marker.getElement?.();
			if (!el) continue;
			const ageMs = Date.now() - new Date(p.location.timestamp).getTime();
			const isStale = ageMs > STALE_THRESHOLD_MS;
			el.style.opacity = isStale ? "0.5" : "1";
			el.style.borderColor = isStale ? "#6b7280" : p.color;
			const ageSec = Math.floor(ageMs / 1000);
			const ageLabel = ageSec < 60 ? `${ageSec}s ago` : ageSec < 3600 ? `${Math.floor(ageSec / 60)}m ago` : "stale";
			el.title = `${p.name} - ${p.status} (${ageLabel})`;
			const arrow = el.querySelector(".heading-arrow") as HTMLElement | null;
			if (arrow) arrow.style.borderBottomColor = isStale ? "#6b7280" : p.color;
			// Update status dot
			const statusDot = el.querySelector(".marker-status-dot") as HTMLElement | null;
			if (statusDot) {
				const dotColor = isStale ? "#6b7280" : (
					p.status === "online" ? "#22c55e" : p.status === "away" ? "#f59e0b" :
					p.status === "ghost" ? "#64748b" : "#ef4444"
				);
				statusDot.style.background = dotColor;
				statusDot.style.boxShadow = `0 0 6px ${dotColor}`;
			}
		}
	}

	private updateEmojiButton() {
		const btn = this.shadow.getElementById("emoji-picker-btn");
		if (btn) btn.textContent = this.userEmoji;
		const picker = this.shadow.getElementById("emoji-picker-dropdown");
		if (picker) picker.style.display = this.showEmojiPicker ? "flex" : "none";
	}

	private applyDarkFilter() {
		const container = this.shadow.getElementById("map-container");
		if (!container) return;
		const canvas = container.querySelector("canvas");
		if (canvas) {
			canvas.style.filter = this.isDarkTheme() ? "invert(1) hue-rotate(180deg)" : "none";
		} else {
			// Canvas may not be ready yet — retry after tiles load
			this.map?.once("load", () => {
				const c = container.querySelector("canvas");
				if (c) c.style.filter = this.isDarkTheme() ? "invert(1) hue-rotate(180deg)" : "none";
			});
		}
	}

	// ─── Location sharing ────────────────────────────────────────

	private async checkGeoPermission() {
		try {
			const result = await navigator.permissions.query({ name: "geolocation" });
			this.geoPermissionState = result.state;
			result.addEventListener("change", () => { this.geoPermissionState = result.state; });
		} catch { /* permissions API not available */ }
	}

	private toggleLocationSharing() {
		if (this.privacySettings.precision === "hidden") return; // Hidden/ghost mode prevents sharing

		if (this.sharingLocation) {
			if (this.watchId !== null) {
				navigator.geolocation.clearWatch(this.watchId);
				this.watchId = null;
			}
			this.sharingLocation = false;
			this.geoTimeoutCount = 0;
			this.sync?.clearLocation();
			try { localStorage.removeItem(`rmaps_sharing_${this.room}`); } catch {}
			this.updateShareButton();
			return;
		}

		if (!("geolocation" in navigator)) {
			this.error = "Geolocation not supported";
			return;
		}

		this.checkGeoPermission();
		let firstFix = true;
		const useHighAccuracy = this.geoTimeoutCount < 2;

		this.watchId = navigator.geolocation.watchPosition(
			(pos) => {
				this.sharingLocation = true;
				this.geoTimeoutCount = 0;
				this.updateShareButton();

				let lat = pos.coords.latitude;
				let lng = pos.coords.longitude;

				// Apply privacy fuzzing
				if (this.privacySettings.precision !== "exact") {
					const fuzzed = fuzzLocation(lat, lng, this.privacySettings.precision);
					lat = fuzzed.latitude;
					lng = fuzzed.longitude;
				}

				const loc: LocationState = {
					latitude: lat,
					longitude: lng,
					accuracy: pos.coords.accuracy,
					altitude: pos.coords.altitude ?? undefined,
					heading: pos.coords.heading ?? undefined,
					speed: pos.coords.speed ?? undefined,
					timestamp: new Date().toISOString(),
					source: "gps",
				};
				this.sync?.updateLocation(loc);

				// Persist location + auto-share preference for session restore
				try {
					localStorage.setItem(`rmaps_loc_${this.room}`, JSON.stringify({ lat: pos.coords.latitude, lng: pos.coords.longitude, ts: Date.now() }));
					localStorage.setItem(`rmaps_sharing_${this.room}`, "true");
				} catch {}

				if (firstFix && this.map) {
					this.map.flyTo({ center: [pos.coords.longitude, pos.coords.latitude], zoom: 14 });
					firstFix = false;
				}
			},
			(err) => {
				if (err.code === err.TIMEOUT) {
					this.geoTimeoutCount++;
					if (this.geoTimeoutCount >= 2 && this.watchId !== null) {
						// Restart with low accuracy
						navigator.geolocation.clearWatch(this.watchId);
						this.watchId = navigator.geolocation.watchPosition(
							(pos) => {
								this.sharingLocation = true;
								this.updateShareButton();
								let lat = pos.coords.latitude;
								let lng = pos.coords.longitude;
								if (this.privacySettings.precision !== "exact") {
									const fuzzed = fuzzLocation(lat, lng, this.privacySettings.precision);
									lat = fuzzed.latitude;
									lng = fuzzed.longitude;
								}
								this.sync?.updateLocation({
									latitude: lat, longitude: lng,
									accuracy: pos.coords.accuracy,
									altitude: pos.coords.altitude ?? undefined,
									heading: pos.coords.heading ?? undefined,
									speed: pos.coords.speed ?? undefined,
									timestamp: new Date().toISOString(),
									source: "network",
								});
							},
							() => { this.sharingLocation = false; this.updateShareButton(); },
							{ enableHighAccuracy: false, maximumAge: 10000, timeout: 30000 },
						);
					}
				} else {
					this.error = `Location error: ${err.message}`;
					this.sharingLocation = false;
					this.updateShareButton();
				}
			},
			{ enableHighAccuracy: useHighAccuracy, maximumAge: 5000, timeout: 15000 },
		);
	}

	private setPrecision(level: PrecisionLevel) {
		this.privacySettings.precision = level;
		this.privacySettings.ghostMode = level === "hidden";
		if (level === "hidden") {
			if (this.watchId !== null) {
				navigator.geolocation.clearWatch(this.watchId);
				this.watchId = null;
			}
			this.sharingLocation = false;
			this.sync?.updateStatus("ghost");
			this.sync?.clearLocation();
		} else {
			if (this.privacySettings.ghostMode) {
				// Was ghost, now switching to a visible level
				this.sync?.updateStatus("online");
			}
			this.privacySettings.ghostMode = false;
		}
		this.renderPrivacyPanel();
		this.updateShareButton();
	}

	private updateShareButton() {
		const btn = this.shadow.getElementById("share-location");
		if (btn) {
			if (this.privacySettings.precision === "hidden") {
				btn.textContent = "\u{1F47B} Hidden";
				btn.classList.remove("sharing");
				btn.classList.add("ghost");
			} else if (this.sharingLocation) {
				btn.textContent = "\u{1F4CD} Stop Sharing";
				btn.classList.add("sharing");
				btn.classList.remove("ghost");
			} else {
				btn.textContent = "\u{1F4CD} Share Location";
				btn.classList.remove("sharing");
				btn.classList.remove("ghost");
			}
		}

		// Floating share button
		const floatBtn = this.shadow.getElementById("map-share-float");
		if (floatBtn) {
			if (this.privacySettings.precision === "hidden") {
				floatBtn.innerHTML = "&#128123; Hidden";
				floatBtn.className = "map-share-float ghost";
			} else if (this.sharingLocation) {
				floatBtn.innerHTML = "&#128205; Sharing";
				floatBtn.className = "map-share-float active";
			} else {
				floatBtn.innerHTML = "&#128205; Share Location";
				floatBtn.className = "map-share-float";
			}
		}
		// Update permission indicator
		const permIndicator = this.shadow.getElementById("geo-perm-indicator");
		if (permIndicator) {
			const colors: Record<string, string> = { granted: "#22c55e", prompt: "#f59e0b", denied: "#ef4444" };
			permIndicator.style.background = colors[this.geoPermissionState] || "#64748b";
			permIndicator.title = `Geolocation: ${this.geoPermissionState || "unknown"}`;
		}
		// Update header share toggle
		const headerToggle = this.shadow.getElementById("header-share-toggle");
		if (headerToggle) {
			headerToggle.className = `map-header__share-toggle ${this.sharingLocation ? "active" : ""}`;
		}
		// Also update mobile FAB
		this.updateMobileFab();
	}

	private locateMe() {
		if (this.sharingLocation) {
			// Already sharing — fly to own location
			const state = this.sync?.getState();
			const myLoc = state?.participants[this.participantId]?.location;
			if (myLoc && this.map) {
				this.map.flyTo({ center: [myLoc.longitude, myLoc.latitude], zoom: 16 });
			}
		} else if (!this.privacySettings.ghostMode) {
			// Not sharing — start sharing first
			this.toggleLocationSharing();
		}
		// Pulse the locate button
		const btn = this.shadow.getElementById("locate-me-fab");
		if (btn) {
			btn.style.background = "#4285f4";
			btn.style.color = "#fff";
			setTimeout(() => { btn.style.background = "var(--rs-bg-surface)"; btn.style.color = "var(--rs-text-secondary)"; }, 1500);
		}
	}

	private fitToParticipants() {
		if (!this.map || !(window as any).maplibregl) return;
		const state = this.sync?.getState();
		if (!state) return;
		const bounds = new (window as any).maplibregl.LngLatBounds();
		let count = 0;
		for (const p of Object.values(state.participants)) {
			if (p.location) {
				bounds.extend([p.location.longitude, p.location.latitude]);
				count++;
			}
		}
		if (count < 1) return;
		if (count === 1) {
			const first = Object.values(state.participants).find(p => p.location)!;
			this.map.flyTo({ center: [first.location!.longitude, first.location!.latitude], zoom: 14 });
		} else {
			this.map.fitBounds(bounds, { padding: 60, maxZoom: 16 });
		}
	}

	private renderPrivacyPanel(container?: HTMLElement) {
		const panel = container || this.shadow.getElementById("privacy-panel");
		if (!panel) return;
		panel.innerHTML = "";
		const privacyEl = document.createElement("map-privacy-panel") as any;
		privacyEl.settings = this.privacySettings;
		privacyEl.addEventListener("precision-change", (e: CustomEvent) => {
			this.setPrecision(e.detail as PrecisionLevel);
		});
		panel.appendChild(privacyEl);
	}

	// ─── Waypoint drop / Meeting point modal ────────────────────

	private dropWaypoint() {
		this.showMeetingModal = true;
		this.renderMeetingPointModal();
	}

	private renderMeetingPointModal() {
		if (!this.showMeetingModal) {
			this.shadow.getElementById("meeting-modal")?.remove();
			return;
		}
		// Lazy-load sub-component
		import("./map-meeting-modal");
		const modal = document.createElement("map-meeting-modal") as any;
		modal.id = "meeting-modal";
		const center = this.map?.getCenter();
		const myLoc = this.sync?.getState().participants[this.participantId]?.location;
		if (center) modal.center = { lat: center.lat, lng: center.lng };
		if (myLoc) modal.myLocation = { lat: myLoc.latitude, lng: myLoc.longitude };

		modal.addEventListener("meeting-create", (e: CustomEvent) => {
			const { name, lat, lng, emoji } = e.detail;
			this.sync?.addWaypoint({
				id: crypto.randomUUID(), name, emoji,
				latitude: lat, longitude: lng,
				createdBy: this.participantId,
				createdAt: new Date().toISOString(),
				type: "meeting",
			});
			this.showMeetingModal = false;
		});
		modal.addEventListener("modal-close", () => { this.showMeetingModal = false; });

		this.shadow.appendChild(modal);
	}

	// ─── Share modal with QR code ───────────────────────────────

	private async renderShareModal() {
		if (!this.showShareModal) {
			this.shadow.getElementById("share-modal")?.remove();
			return;
		}
		import("./map-share-modal");
		const modal = document.createElement("map-share-modal") as any;
		modal.id = "share-modal";
		modal.url = `${window.location.origin}${getModuleApiBase("rmaps")}/${this.room}`;
		modal.room = this.room;
		modal.addEventListener("modal-close", () => { this.showShareModal = false; });
		this.shadow.appendChild(modal);
	}

	// ─── Import modal ───────────────────────────────────────────

	private renderImportModal() {
		if (!this.showImportModal) {
			this.shadow.getElementById("import-modal")?.remove();
			return;
		}
		import("./map-import-modal");
		const modal = document.createElement("map-import-modal") as any;
		modal.id = "import-modal";
		modal.addEventListener("import-places", (e: CustomEvent) => {
			for (const p of e.detail.places) {
				this.sync?.addWaypoint({
					id: crypto.randomUUID(),
					name: p.name,
					emoji: "\u{1F4CD}",
					latitude: p.lat,
					longitude: p.lng,
					createdBy: this.participantId,
					createdAt: new Date().toISOString(),
					type: "poi",
				});
			}
		});
		modal.addEventListener("modal-close", () => { this.showImportModal = false; });
		this.shadow.appendChild(modal);
	}

	// ─── Route display ──────────────────────────────────────────

	private showRoute(route: { segments: any[]; totalDistance: number; estimatedTime: number }, destination: string) {
		if (!this.map) return;
		this.clearRoute();
		this.activeRoute = { ...route, destination };

		const segmentColors: Record<string, string> = { outdoor: "#3b82f6", indoor: "#8b5cf6", transition: "#f97316" };

		route.segments.forEach((seg, i) => {
			const sourceId = `route-seg-${i}`;
			const outlineLayerId = `route-layer-outline-${i}`;
			const layerId = `route-layer-${i}`;
			this.map.addSource(sourceId, {
				type: "geojson",
				data: {
					type: "Feature",
					properties: {},
					geometry: { type: "LineString", coordinates: seg.coordinates },
				},
			});
			// Outline layer (dark stroke behind colored line)
			this.map.addLayer({
				id: outlineLayerId,
				type: "line",
				source: sourceId,
				layout: { "line-join": "round", "line-cap": "round" },
				paint: { "line-color": "#1e293b", "line-width": 8, "line-opacity": 0.6 },
			});
			this.map.addLayer({
				id: layerId,
				type: "line",
				source: sourceId,
				layout: { "line-join": "round", "line-cap": "round" },
				paint: {
					"line-color": segmentColors[seg.type] || "#3b82f6",
					"line-width": 5,
					"line-opacity": 0.8,
					...(seg.type === "transition" ? { "line-dasharray": [2, 2] } : {}),
				},
			});
		});

		this.fitMapToRoute(route);
		this.renderRoutePanel();
	}

	private clearRoute() {
		if (!this.map) return;
		// Remove all route layers/sources (including outline layers)
		for (let i = 0; i < 10; i++) {
			try { this.map.removeLayer(`route-layer-${i}`); } catch {}
			try { this.map.removeLayer(`route-layer-outline-${i}`); } catch {}
			try { this.map.removeSource(`route-seg-${i}`); } catch {}
		}
		this.activeRoute = null;
		const routePanel = this.shadow.getElementById("route-panel");
		if (routePanel) routePanel.remove();
	}

	private fitMapToRoute(route: { segments: any[] }) {
		if (!this.map || !(window as any).maplibregl) return;
		const bounds = new (window as any).maplibregl.LngLatBounds();
		for (const seg of route.segments) {
			for (const coord of seg.coordinates) {
				bounds.extend(coord);
			}
		}
		if (!bounds.isEmpty()) {
			this.map.fitBounds(bounds, { padding: 60, maxZoom: 16 });
		}
	}

	private formatRouteInstructions(segments: any[]): string {
		const parts: string[] = [];
		for (const seg of segments) {
			const label = seg.type === "indoor" ? "indoors" : seg.type === "transition" ? "transition" : "outdoors";
			parts.push(`${formatDistance(seg.distance)} ${label}`);
		}
		if (parts.length <= 1) return parts[0] ? `Walk ${parts[0]}` : "";
		return `Walk ${parts.join(", then ")}`;
	}

	private renderRoutePanel() {
		if (!this.activeRoute) return;
		let routePanel = this.shadow.getElementById("route-panel");
		if (!routePanel) {
			routePanel = document.createElement("div");
			routePanel.id = "route-panel";
			routePanel.style.cssText = `
				position:absolute;bottom:12px;left:12px;right:12px;z-index:5;
				background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);
				border-radius:10px;padding:14px;box-shadow:0 4px 16px rgba(0,0,0,0.3);
			`;
			this.shadow.getElementById("map-container")?.appendChild(routePanel);
		}

		const segTypeLabels: Record<string, string> = { outdoor: "Outdoor", indoor: "Indoor", transition: "Transition" };
		const segTypeColors: Record<string, string> = { outdoor: "#3b82f6", indoor: "#8b5cf6", transition: "#f97316" };

		routePanel.innerHTML = `
			<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
				<div style="font-size:14px;font-weight:600;color:var(--rs-text-primary);">
					Route to ${this.esc(this.activeRoute.destination)}
				</div>
				<button id="close-route" style="background:none;border:none;color:var(--rs-text-muted);cursor:pointer;font-size:16px;">\u2715</button>
			</div>
			<div style="display:flex;gap:16px;font-size:13px;color:var(--rs-text-secondary);margin-bottom:4px;">
				<span>\u{1F4CF} ${formatDistance(this.activeRoute.totalDistance)}</span>
				<span>\u{23F1} ${formatTime(this.activeRoute.estimatedTime)}</span>
			</div>
			<div style="font-size:11px;color:var(--rs-text-muted);margin-bottom:8px;">${this.formatRouteInstructions(this.activeRoute.segments)}</div>
			<div style="display:flex;gap:6px;flex-wrap:wrap;">
				${this.activeRoute.segments.map(seg => `
					<span style="font-size:10px;padding:2px 8px;border-radius:10px;border:1px solid ${segTypeColors[seg.type] || '#666'}40;color:${segTypeColors[seg.type] || '#666'};font-weight:500;">
						${segTypeLabels[seg.type] || seg.type}: ${formatDistance(seg.distance)}
					</span>
				`).join("")}
			</div>
		`;
		routePanel.querySelector("#close-route")?.addEventListener("click", () => this.clearRoute());
	}

	// ─── Route request handler ──────────────────────────────────

	private onRouteRequest(fromId: string, fromName: string, fromEmoji: string) {
		// Show toast notification
		const state = this.sync?.getState();
		const requester = state?.participants[fromId];
		if (!requester?.location) return;

		const toast = document.createElement("div");
		toast.style.cssText = `
			position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:100;
			background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);
			border-radius:12px;padding:12px 16px;box-shadow:0 8px 24px rgba(0,0,0,0.4);
			display:flex;align-items:center;gap:10px;max-width:340px;
			animation:toastIn 0.3s ease;
		`;
		toast.innerHTML = `
			<span style="font-size:20px">${fromEmoji}</span>
			<div style="flex:1;">
				<div style="font-size:13px;font-weight:600;color:var(--rs-text-primary);">${this.esc(fromName)} wants you to navigate to them</div>
			</div>
			<button id="toast-nav" style="padding:6px 12px;border-radius:6px;border:none;background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:12px;">Navigate</button>
			<button id="toast-dismiss" style="background:none;border:none;color:var(--rs-text-muted);cursor:pointer;font-size:16px;">✕</button>
		`;

		const dismiss = () => { toast.style.opacity = "0"; setTimeout(() => toast.remove(), 200); };
		toast.querySelector("#toast-dismiss")?.addEventListener("click", dismiss);
		toast.querySelector("#toast-nav")?.addEventListener("click", () => {
			dismiss();
			if (requester.location) {
				this.requestRoute(requester.location.latitude, requester.location.longitude, fromName);
			}
		});

		this.shadow.appendChild(toast);
		setTimeout(dismiss, 10000);
	}

	private showPingToast(fromName: string) {
		// Vibrate if available
		if ("vibrate" in navigator) navigator.vibrate([200, 100, 200]);
		const toast = document.createElement("div");
		toast.style.cssText = `
			position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:100;
			background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);
			border-radius:12px;padding:10px 16px;box-shadow:0 8px 24px rgba(0,0,0,0.4);
			display:flex;align-items:center;gap:8px;max-width:300px;
		`;
		toast.innerHTML = `<span style="font-size:16px">\u{1F4E2}</span><span style="font-size:13px;color:var(--rs-text-primary);">${this.esc(fromName)} pinged for your location</span>`;
		this.shadow.appendChild(toast);
		setTimeout(() => { toast.style.opacity = "0"; setTimeout(() => toast.remove(), 200); }, 4000);
	}

	// ─── Chat badge ─────────────────────────────────────────────

	private updateChatBadge() {
		const badge = this.shadow.getElementById("chat-badge");
		if (badge) {
			badge.textContent = this.unreadCount > 0 ? String(this.unreadCount) : "";
			badge.style.display = this.unreadCount > 0 ? "flex" : "none";
		}
		const mobileBadge = this.shadow.getElementById("fab-chat-badge");
		if (mobileBadge) {
			mobileBadge.textContent = this.unreadCount > 0 ? String(this.unreadCount) : "";
			mobileBadge.style.display = this.unreadCount > 0 ? "flex" : "none";
		}
	}

	private async mountChatPanel(container: HTMLElement) {
		await import("./map-chat-panel");
		const panel = document.createElement("map-chat-panel") as any;
		panel.client = this.lfClient;
		panel.participantId = this.participantId;
		panel.participantName = this.userName;
		panel.participantEmoji = this.userEmoji;
		panel.setAttribute("room", this.room);
		panel.style.cssText = "display:block;height:100%;";
		container.innerHTML = "";
		container.appendChild(panel);
	}

	// ─── Indoor/outdoor mode ────────────────────────────────────

	private async switchToIndoor(event: string) {
		this.mapMode = "indoor";
		this.indoorEvent = event;

		// Hide outdoor map
		const mapContainer = this.shadow.getElementById("map-container");
		if (mapContainer) mapContainer.style.display = "none";

		// Create indoor view container
		await import("./map-indoor-view");
		const indoorContainer = document.createElement("div");
		indoorContainer.id = "indoor-container";
		indoorContainer.style.cssText = "width:100%;height:100%;position:absolute;inset:0;";
		const mapMain = this.shadow.querySelector(".map-main");
		if (mapMain) {
			(mapMain as HTMLElement).style.position = "relative";
			mapMain.appendChild(indoorContainer);
		}

		const indoorView = document.createElement("map-indoor-view") as any;
		indoorView.cfg = { event, apiBase: this.getApiBase() };
		indoorView.addEventListener("switch-outdoor", () => this.switchToOutdoor());
		indoorContainer.appendChild(indoorView);
		this.indoorView = indoorView;

		// Forward current participants
		const state = this.sync?.getState();
		if (state) indoorView.updateParticipants(state.participants);

		// Update toggle button
		const toggleBtn = this.shadow.getElementById("indoor-toggle");
		if (toggleBtn) {
			toggleBtn.textContent = "🌍 Outdoor";
			toggleBtn.title = "Switch to outdoor map";
		}
	}

	private switchToOutdoor() {
		this.mapMode = "outdoor";
		this.indoorEvent = null;

		// Remove indoor view
		const indoorContainer = this.shadow.getElementById("indoor-container");
		if (indoorContainer) indoorContainer.remove();
		if (this.indoorView) { this.indoorView = null; }

		// Show outdoor map
		const mapContainer = this.shadow.getElementById("map-container");
		if (mapContainer) mapContainer.style.display = "";

		// Update toggle button
		const toggleBtn = this.shadow.getElementById("indoor-toggle");
		if (toggleBtn) {
			toggleBtn.textContent = "🏢 Indoor";
			toggleBtn.title = "Switch to indoor map";
		}
	}

	// ─── Navigation panel (participant/waypoint selection) ───────

	private async requestRoute(targetLat: number, targetLng: number, targetName: string) {
		// Get user's current location
		const myState = this.sync?.getState().participants[this.participantId];
		if (!myState?.location) {
			this.error = "Share your location first to get directions";
			return;
		}

		const base = this.getApiBase();
		try {
			const res = await fetch(`${base}/api/routing`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					from: { lat: myState.location.latitude, lng: myState.location.longitude },
					to: { lat: targetLat, lng: targetLng },
					mode: "walking",
				}),
				signal: AbortSignal.timeout(12000),
			});
			if (res.ok) {
				const data = await res.json();
				if (data.success && data.route) {
					this.showRoute(data.route, targetName);
				} else {
					this.error = "No route found";
				}
			}
		} catch {
			this.error = "Routing request failed";
		}
	}

	private renderNavigationPanel() {
		let navPanel = this.shadow.getElementById("nav-panel");

		// Get target details
		const state = this.sync?.getState();
		let targetName = "";
		let targetLat = 0;
		let targetLng = 0;
		let targetEmoji = "";
		let targetDetail = "";

		if (this.selectedParticipant && state) {
			const p = state.participants[this.selectedParticipant];
			if (p?.location) {
				targetName = p.name;
				targetEmoji = p.emoji;
				targetLat = p.location.latitude;
				targetLng = p.location.longitude;
				const myLoc = state.participants[this.participantId]?.location;
				if (myLoc) {
					targetDetail = formatDistance(haversineDistance(myLoc.latitude, myLoc.longitude, targetLat, targetLng)) + " away";
				}
			}
		} else if (this.selectedWaypoint && state) {
			const wp = state.waypoints.find(w => w.id === this.selectedWaypoint);
			if (wp) {
				targetName = wp.name;
				targetEmoji = wp.emoji || "\u{1F4CD}";
				targetLat = wp.latitude;
				targetLng = wp.longitude;
				const myLoc = state.participants[this.participantId]?.location;
				if (myLoc) {
					targetDetail = formatDistance(haversineDistance(myLoc.latitude, myLoc.longitude, targetLat, targetLng)) + " away";
				}
			}
		}

		if (!targetName) {
			if (navPanel) navPanel.remove();
			return;
		}

		if (!navPanel) {
			navPanel = document.createElement("div");
			navPanel.id = "nav-panel";
			navPanel.style.cssText = `
				position:absolute;top:12px;left:12px;z-index:5;
				background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);
				border-radius:10px;padding:12px;box-shadow:0 4px 16px rgba(0,0,0,0.3);
				min-width:180px;
			`;
			this.shadow.getElementById("map-container")?.appendChild(navPanel);
		}

		const isParticipant = !!this.selectedParticipant;
		navPanel.innerHTML = `
			<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
				<span style="font-size:20px;">${targetEmoji}</span>
				<div style="flex:1;">
					<div style="font-size:13px;font-weight:600;color:var(--rs-text-primary);">${this.esc(targetName)}</div>
					${targetDetail ? `<div style="font-size:11px;color:var(--rs-text-muted);">${targetDetail}</div>` : ""}
				</div>
				<button id="close-nav" style="background:none;border:none;color:var(--rs-text-muted);cursor:pointer;font-size:14px;">\u2715</button>
			</div>
			<div style="display:flex;gap:6px;">
				<button id="navigate-btn" style="flex:1;padding:8px;border-radius:6px;border:none;background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:12px;">\u{1F9ED} Navigate</button>
				${isParticipant ? `<button id="route-request-btn" style="flex:1;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);font-weight:600;cursor:pointer;font-size:12px;">📩 Ask to nav</button>` : ""}
			</div>
		`;

		navPanel.querySelector("#close-nav")?.addEventListener("click", () => {
			this.selectedParticipant = null;
			this.selectedWaypoint = null;
			navPanel?.remove();
		});
		navPanel.querySelector("#navigate-btn")?.addEventListener("click", () => {
			this.requestRoute(targetLat, targetLng, targetName);
		});
		navPanel.querySelector("#route-request-btn")?.addEventListener("click", () => {
			this.sync?.sendRouteRequest(this.participantId, this.userName, this.userEmoji);
			const btn = navPanel?.querySelector("#route-request-btn") as HTMLElement;
			if (btn) { btn.textContent = "✓ Sent"; btn.style.color = "#22c55e"; btn.style.borderColor = "#22c55e"; }
		});
	}

	// ─── Thumbnail capture ───────────────────────────────────────

	private captureThumbnail() {
		if (!this.map || !this.room) return;
		try {
			const canvas = this.map.getCanvas();
			// Downscale to 200x120
			const tmp = document.createElement("canvas");
			tmp.width = 200;
			tmp.height = 120;
			const ctx = tmp.getContext("2d");
			if (!ctx) return;
			ctx.drawImage(canvas, 0, 0, 200, 120);
			const dataUrl = tmp.toDataURL("image/jpeg", 0.6);
			updateRoomThumbnail(this.room, dataUrl);
		} catch {}
	}

	// ─── Time ago helper ─────────────────────────────────────────

	private timeAgo(iso: string): string {
		const s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
		if (s < 60) return "just now";
		const m = Math.floor(s / 60);
		if (m < 60) return `${m}m ago`;
		const h = Math.floor(m / 60);
		if (h < 24) return `${h}h ago`;
		const d = Math.floor(h / 24);
		return `${d}d ago`;
	}

	// ─── Render (room mode) ──────────────────────────────────────

	private render() {
		this.shadow.innerHTML = `
		<style>
			:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); }
			* { box-sizing: border-box; }

			/* Lobby nav */
			.rapp-nav { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; min-height: 36px; }
			.rapp-nav__back { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 13px; }
			.rapp-nav__back:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); }
			.rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

			/* Map room header — dark bar */
			.map-header {
				display:flex;align-items:center;gap:8px;padding:8px 14px;
				background:rgba(15,23,42,0.95);backdrop-filter:blur(8px);
				border-radius:10px;margin-bottom:12px;min-height:44px;
				border:1px solid rgba(255,255,255,0.08);
			}
			.map-header__left { display:flex;align-items:center;gap:6px;flex:1;min-width:0; }
			.map-header__back {
				background:none;border:none;color:#94a3b8;cursor:pointer;
				font-size:16px;padding:4px 6px;border-radius:4px;
			}
			.map-header__back:hover { color:#e2e8f0; }
			.map-header__logo { font-size:13px;font-weight:700;color:#e2e8f0;white-space:nowrap; }
			.map-header__slug { font-size:12px;color:#64748b;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
			.map-header__participants {
				display:flex;align-items:center;gap:4px;
				padding:4px 12px;border-radius:20px;
				background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);
				color:#e2e8f0;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;
			}
			.map-header__participants:hover { background:rgba(255,255,255,0.14); }
			.map-header__right { display:flex;align-items:center;gap:4px; }
			.map-header__icon-btn {
				background:none;border:none;color:#94a3b8;cursor:pointer;
				font-size:15px;padding:4px 6px;border-radius:4px;
			}
			.map-header__icon-btn:hover { color:#e2e8f0; }
			.map-header__share-toggle {
				width:32px;height:32px;border-radius:50%;
				background:transparent;border:1px solid rgba(255,255,255,0.15);
				color:#94a3b8;cursor:pointer;font-size:14px;
				display:flex;align-items:center;justify-content:center;transition:all 0.2s;
			}
			.map-header__share-toggle.active { background:#10b981;border-color:#10b981;color:#fff; }
			.map-header__share-toggle:hover { border-color:#10b981;color:#10b981; }

			.status-dot {
				width: 8px; height: 8px; border-radius: 50%; display: inline-block;
			}
			.status-connected { background: #22c55e; }
			.status-disconnected { background: #ef4444; }

			.rapp-nav__btn { padding: 6px 14px; border-radius: 6px; border: none; background: #4f46e5; color: #fff; font-weight: 600; cursor: pointer; font-size: 13px; }
			.rapp-nav__btn:hover { background: #6366f1; }

			.room-card {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px;
				padding: 16px; margin-bottom: 12px; cursor: pointer; transition: border-color 0.2s;
				display: flex; align-items: center; gap: 12px;
			}
			.room-card:hover { border-color: var(--rs-border-strong); }
			.room-icon { font-size: 24px; }
			.room-name { font-size: 15px; font-weight: 600; }

			.map-container {
				width: 100%; height: 100%; min-height: 300px;
				border-radius: 10px;
				background: var(--rs-bg-surface-sunken); border: 1px solid var(--rs-border);
				position: relative; overflow: hidden;
			}

			.map-layout {
				display: flex; gap: 12px;
			}
			.map-main { flex: 1; min-width: 0; }
			.map-sidebar {
				width: 220px; flex-shrink: 0;
				background: var(--rs-glass-bg); border: 1px solid var(--rs-border); border-radius: 10px;
				padding: 12px; max-height: 100%; overflow-y: auto;
			}
			.sidebar-title {
				font-size: 11px; font-weight: 600; color: var(--rs-text-secondary);
				text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 8px;
			}

			.controls {
				display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap;
			}
			.ctrl-btn {
				padding: 8px 16px; border-radius: 8px; border: 1px solid var(--rs-border);
				background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 13px;
			}
			.ctrl-btn:hover { border-color: var(--rs-border-strong); }
			.ctrl-btn.sharing {
				border-color: #22c55e; color: #22c55e; animation: pulse 2s infinite;
			}
			.ctrl-btn.ghost {
				border-color: #8b5cf6; color: #8b5cf6;
			}
			@keyframes pulse {
				0%, 100% { opacity: 1; }
				50% { opacity: 0.7; }
			}
			@keyframes selfPulse {
				0% { opacity: 0.6; transform: translate(-50%, -50%) scale(0.8); }
				70% { opacity: 0; transform: translate(-50%, -50%) scale(1.8); }
				100% { opacity: 0; transform: translate(-50%, -50%) scale(1.8); }
			}

			/* Locate-me FAB — always visible */
			.map-locate-fab {
				position: fixed; bottom: 24px; left: 16px; z-index: 6;
				width: 44px; height: 44px; border-radius: 50%;
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
				color: var(--rs-text-secondary); cursor: pointer; font-size: 20px;
				display: flex; align-items: center; justify-content: center;
				box-shadow: 0 2px 8px rgba(0,0,0,0.25); transition: all 0.2s;
			}
			.map-locate-fab:hover { border-color: #4285f4; color: #4285f4; }

			/* Floating share-location button on map */
			.map-share-float {
				position:absolute;bottom:20px;right:16px;z-index:6;
				padding:10px 18px;border-radius:24px;
				background:#fff;border:1px solid #e2e8f0;
				color:#1e293b;font-weight:600;font-size:13px;cursor:pointer;
				box-shadow:0 2px 10px rgba(0,0,0,0.15);transition:all 0.2s;
				font-family:system-ui,-apple-system,sans-serif;
			}
			.map-share-float.active { background:#10b981;border-color:#10b981;color:#fff; }
			.map-share-float.ghost { background:#8b5cf6;border-color:#8b5cf6;color:#fff; }
			.map-share-float:hover { box-shadow:0 4px 16px rgba(0,0,0,0.2); }

			/* Mobile floating buttons — hidden on desktop */
			.mobile-float-btns { display: none; }
			.mobile-bottom-sheet { display: none; }

			@media (max-width: 768px) {
				.map-container { height: 100%; min-height: 250px; max-height: none; border-radius: 0; border: none; }
				.map-layout { flex-direction: column; }
				.map-sidebar { display: none; }
				.controls { display: none; }
				#privacy-panel { display: none !important; }
				.map-header { position:absolute;top:0;left:0;right:0;z-index:7;margin:0;border-radius:0;border-bottom:1px solid rgba(255,255,255,0.08); }
				.map-locate-fab { bottom: 100px; }

				/* Mobile floating pill buttons */
				.mobile-float-btns { display:flex;position:fixed;bottom:16px;left:0;right:0;z-index:8;padding:0 16px;justify-content:space-between;pointer-events:none; }
				.mobile-pill-btn {
					padding:10px 16px;border-radius:24px;border:none;
					background:rgba(15,23,42,0.92);color:#e2e8f0;font-weight:600;
					font-size:13px;cursor:pointer;pointer-events:auto;
					box-shadow:0 4px 16px rgba(0,0,0,0.3);backdrop-filter:blur(4px);
					font-family:system-ui,-apple-system,sans-serif;
				}
				.mobile-pill-btn:active { opacity:0.8; }
				.map-share-float { bottom:70px; }

				/* Mobile bottom sheet */
				.mobile-bottom-sheet {
					display: block; position: fixed; bottom: 0; left: 0; right: 0; z-index: 7;
					background: var(--rs-bg-surface); border-top: 1px solid var(--rs-border);
					border-radius: 16px 16px 0 0; max-height: 60vh;
					transition: transform 0.3s ease; transform: translateY(calc(100% - 40px));
				}
				.mobile-bottom-sheet.expanded { transform: translateY(0); overflow-y: auto; }
				.sheet-handle {
					display: flex; align-items: center; justify-content: center;
					padding: 10px; cursor: pointer; user-select: none;
				}
				.sheet-handle-bar {
					width: 36px; height: 4px; border-radius: 2px; background: var(--rs-border-strong);
				}
				.sheet-header {
					display: flex; align-items: center; justify-content: space-between;
					padding: 0 16px 8px; font-size: 12px; font-weight: 600;
					color: var(--rs-text-secondary); text-transform: uppercase; letter-spacing: 0.06em;
				}
				.sheet-content { padding: 0 16px 16px; }

				/* Mobile privacy popup */
				.mobile-privacy-popup {
					position: fixed; bottom: 80px; right: 16px; z-index: 9;
					background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong);
					border-radius: 12px; padding: 12px; width: 240px;
					box-shadow: 0 8px 24px rgba(0,0,0,0.3);
				}
			}

			.share-link {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 8px;
				padding: 12px; margin-top: 12px; font-family: monospace; font-size: 12px;
				color: var(--rs-text-secondary); display: flex; align-items: center; gap: 8px;
			}
			.share-link span { flex: 1; overflow: hidden; text-overflow: ellipsis; }
			.copy-btn {
				padding: 4px 10px; border-radius: 4px; border: 1px solid var(--rs-border);
				background: var(--rs-btn-secondary-bg); color: var(--rs-text-secondary); cursor: pointer; font-size: 11px;
			}

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

			/* Section labels */
			.section-label {
				font-size: 12px; font-weight: 600; color: var(--rs-text-secondary);
				text-transform: uppercase; letter-spacing: 0.06em; margin: 20px 0 10px;
			}

			/* Room history grid */
			.room-history-grid {
				display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
				gap: 10px; margin-bottom: 16px;
			}
			.history-card {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px;
				overflow: hidden; cursor: pointer; transition: border-color 0.2s;
				position: relative;
			}
			.history-card:hover { border-color: var(--rs-border-strong); }
			.history-thumb {
				width: 100%; height: 90px; object-fit: cover; display: block;
				background: var(--rs-bg-surface-sunken);
			}
			.history-thumb-placeholder {
				width: 100%; height: 90px; display: flex; align-items: center; justify-content: center;
				background: var(--rs-bg-surface-sunken); font-size: 32px;
			}
			.history-info {
				padding: 8px 10px;
			}
			.history-name {
				font-size: 13px; font-weight: 600; color: var(--rs-text-primary);
				overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
			}
			.history-time {
				font-size: 10px; color: var(--rs-text-muted); margin-top: 2px;
			}
			.ping-btn {
				position: absolute; top: 6px; right: 6px;
				width: 26px; height: 26px; border-radius: 50%;
				background: var(--rs-glass-bg); border: 1px solid var(--rs-border);
				color: var(--rs-text-secondary); cursor: pointer; font-size: 13px;
				display: flex; align-items: center; justify-content: center;
				opacity: 0; transition: opacity 0.15s;
			}
			.history-card:hover .ping-btn { opacity: 1; }
			.ping-btn:hover { border-color: #6366f1; color: #818cf8; }

			@media (max-width: 768px) {
				.room-history-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
			}
		</style>

		${this.error ? `<div style="color:#ef5350;text-align:center;padding:12px">${this.esc(this.error)}</div>` : ""}
		${this.view === "lobby" ? this.renderLobby() : this.renderMap()}
		`;

		if (this.space === "demo") {
			this.attachDemoMapListeners();
		} else {
			this.attachListeners();
		}
		this._tour.renderOverlay();
	}

	startTour() { this._tour.start(); }

	private renderLobby(): string {
		const saved = JSON.parse(localStorage.getItem("rmaps_user") || "null");
		const lastRoom = localStorage.getItem("rmaps_last_room") || "";

		const profileSection = `
			<div style="background:var(--rs-glass-bg);border:1px solid var(--rs-border);border-radius:12px;padding:16px;margin-bottom:16px;">
				<div style="font-size:12px;font-weight:600;color:var(--rs-text-secondary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:10px;">Get Started</div>
				<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
					<div style="position:relative;">
						<span id="lobby-avatar-preview" style="font-size:32px;display:block;cursor:pointer;" title="Click to change emoji">${this.userEmoji}</span>
					</div>
					<div style="flex:1;">
						<input id="lobby-name-input" type="text" placeholder="Your display name" value="${this.esc(saved?.name || this.userName || "")}" style="
							width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--rs-border);
							background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:14px;outline:none;box-sizing:border-box;
						">
					</div>
				</div>
				<div id="lobby-emoji-grid" style="display:none;flex-wrap:wrap;gap:4px;margin-bottom:10px;justify-content:center;">
					${EMOJIS.map(e => `<button class="lobby-emoji-pick" data-lobby-emoji="${e}" style="width:34px;height:34px;border-radius:6px;border:2px solid ${e === this.userEmoji ? "#4f46e5" : "transparent"};background:${e === this.userEmoji ? "#4f46e520" : "var(--rs-bg-surface-sunken)"};font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;">${e}</button>`).join("")}
				</div>
				${lastRoom ? `
					<button id="lobby-rejoin-btn" style="
						width:100%;padding:10px;border-radius:8px;border:none;
						background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:13px;
						margin-bottom:6px;
					">Rejoin &#127760; ${this.esc(lastRoom)}</button>
				` : ""}
				<div style="font-size:11px;color:var(--rs-text-muted);text-align:center;">Profile is saved locally for quick rejoins</div>
			</div>
		`;

		const history = loadRoomHistory();
		const historyCards = history.length > 0 ? `
			<div class="section-label">Recent Rooms</div>
			<div class="room-history-grid">
				${history.map((h) => `
					<div class="history-card" data-room="${this.esc(h.slug)}" data-collab-id="room:${this.esc(h.slug)}">
						${h.thumbnail
							? `<img class="history-thumb" src="${h.thumbnail}" alt="">`
							: `<div class="history-thumb-placeholder">&#127760;</div>`
						}
						<div class="history-info">
							<div class="history-name">${this.esc(h.name)}</div>
							<div class="history-time">${this.timeAgo(h.lastVisited)}</div>
						</div>
						<button class="ping-btn" data-ping-room="${this.esc(h.slug)}" title="Ping friends in this room">&#128276;</button>
					</div>
				`).join("")}
			</div>
		` : "";

		return `
			<div class="rapp-nav">
				<span class="rapp-nav__title">Map Rooms</span>
				<span class="status-dot ${this.syncStatus === "connected" ? "status-connected" : "status-disconnected"}"></span>
				<span style="font-size:12px;color:var(--rs-text-muted);margin-right:12px">${this.syncStatus === "connected" ? "Sync online" : "Sync offline"}</span>
				<button class="rapp-nav__btn" id="create-room">+ New Room</button>
				<button class="rapp-nav__btn" id="btn-tour" style="background:transparent;border:1px solid var(--rs-border,#334155);color:var(--rs-text-secondary,#94a3b8);font-weight:500">Tour</button>
			</div>

			${profileSection}

			${this.rooms.length > 0 ? `
				<div class="section-label">Active Rooms</div>
				${this.rooms.map((r) => `
					<div class="room-card" data-room="${r}" data-collab-id="room:${r}">
						<span class="room-icon">&#128506;</span>
						<span class="room-name">${this.esc(r)}</span>
					</div>
				`).join("")}
			` : ""}

			${historyCards}

			<div class="empty">
				<p style="font-size:16px;margin-bottom:8px">Create or join a map room to share locations</p>
				<p style="font-size:13px">Share the room link with friends to see each other on the map in real-time</p>
			</div>
		`;
	}

	private renderMap(): string {
		const demoBanner = this.space === "demo" ? `
			<div style="text-align:center;padding:6px 12px;background:rgba(79,70,229,0.12);border:1px solid rgba(79,70,229,0.25);border-radius:8px;margin-bottom:8px;font-size:12px;color:#a5b4fc;">
				Demo &mdash; simulated festival meetup. <a href="/" style="color:#818cf8;font-weight:600;text-decoration:underline;">Sign up</a> to create your own room.
			</div>
		` : "";
		return `
			${demoBanner}
			<div class="map-header">
				<div class="map-header__left">
					${this._history.canGoBack ? '<button class="map-header__back" data-back="lobby">&#8592;</button>' : ''}
					<span class="map-header__logo">&#127760; rMaps</span>
					<span class="map-header__slug">/${this.esc(this.room)}</span>
					<span class="status-dot ${this.syncStatus === "connected" ? "status-connected" : "status-disconnected"}" style="margin-left:6px;"></span>
				</div>
				<button class="map-header__participants" id="header-participants-toggle" title="Show participants">
					&#128101; <span id="header-participant-count">0</span>
				</button>
				<div class="map-header__right">
					<span id="geo-perm-indicator" style="width:6px;height:6px;border-radius:50%;background:#64748b;" title="Geolocation: unknown"></span>
					<button class="map-header__icon-btn" id="bell-toggle" title="Notifications">&#128276;</button>
					<button class="map-header__icon-btn" id="share-room-btn" title="Share rMap">&#128228;</button>
					<button class="map-header__share-toggle ${this.sharingLocation ? "active" : ""}" id="header-share-toggle" title="${this.sharingLocation ? "Sharing location" : "Share location"}">
						${this.sharingLocation ? "&#128205;" : "&#128205;"}
					</button>
				</div>
			</div>

			<div class="map-layout">
				<div class="map-main">
					<div class="map-container" id="map-container"></div>
				</div>
				<div class="map-sidebar">
					<div style="display:flex;gap:4px;margin-bottom:8px;">
						<button class="sidebar-tab ${this.sidebarTab === "participants" ? "active" : ""}" data-sidebar-tab="participants" style="flex:1;padding:4px;border-radius:6px;border:1px solid ${this.sidebarTab === "participants" ? "#4f46e5" : "var(--rs-border)"};background:${this.sidebarTab === "participants" ? "#4f46e520" : "transparent"};color:${this.sidebarTab === "participants" ? "#818cf8" : "var(--rs-text-muted)"};cursor:pointer;font-size:11px;font-weight:600;">Participants</button>
						<button class="sidebar-tab ${this.sidebarTab === "chat" ? "active" : ""}" data-sidebar-tab="chat" style="flex:1;padding:4px;border-radius:6px;border:1px solid ${this.sidebarTab === "chat" ? "#4f46e5" : "var(--rs-border)"};background:${this.sidebarTab === "chat" ? "#4f46e520" : "transparent"};color:${this.sidebarTab === "chat" ? "#818cf8" : "var(--rs-text-muted)"};cursor:pointer;font-size:11px;font-weight:600;position:relative;">💬 Chat<span id="chat-badge" style="display:${this.unreadCount > 0 ? "flex" : "none"};position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;border-radius:8px;background:#ef4444;color:#fff;font-size:9px;align-items:center;justify-content:center;padding:0 4px;">${this.unreadCount || ""}</span></button>
					</div>
					<div id="participant-list" style="display:${this.sidebarTab === "participants" ? "block" : "none"};">
						<div style="color:var(--rs-text-muted);font-size:12px;padding:8px 0;">Connecting...</div>
					</div>
					<div id="chat-panel-container" style="display:${this.sidebarTab === "chat" ? "block" : "none"};height:calc(100% - 36px);"></div>
				</div>
			</div>

			<div class="controls">
				<div style="position:relative;">
					<button class="ctrl-btn" id="emoji-picker-btn" title="Change your emoji avatar" style="font-size:18px;padding:4px 10px;">${this.userEmoji}</button>
					<div id="emoji-picker-dropdown" style="display:none;position:absolute;bottom:100%;left:0;margin-bottom:6px;background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);border-radius:10px;padding:8px;gap:4px;flex-wrap:wrap;width:180px;box-shadow:0 4px 16px rgba(0,0,0,0.3);z-index:10;">
						${EMOJIS.map(e => `<button class="emoji-opt" data-emoji-pick="${e}" style="width:32px;height:32px;border-radius:6px;border:1px solid ${e === this.userEmoji ? "#4f46e5" : "transparent"};background:${e === this.userEmoji ? "#4f46e520" : "none"};font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;">${e}</button>`).join("")}
					</div>
				</div>
				<button class="ctrl-btn ${this.sharingLocation ? "sharing" : ""} ${this.privacySettings.precision === "hidden" ? "ghost" : ""}" id="share-location">${this.privacySettings.precision === "hidden" ? "\u{1F47B} Hidden" : this.sharingLocation ? "\u{1F4CD} Stop Sharing" : "\u{1F4CD} Share Location"}</button>
				<button class="ctrl-btn" id="privacy-toggle">\u{1F6E1} Privacy</button>
				<button class="ctrl-btn" id="drop-waypoint">\u{1F4CC} Drop Pin</button>
				<button class="ctrl-btn" id="share-room-btn">\u{1F4E4} Share rMap</button>
				<button class="ctrl-btn" id="indoor-toggle">${this.mapMode === "indoor" ? "\u{1F30D} Outdoor" : "\u{1F3E2} Indoor"}</button>
			</div>

			<div id="privacy-panel" style="display:${this.showPrivacyPanel ? "block" : "none"};background:var(--rs-bg-surface);border:1px solid var(--rs-border);border-radius:8px;padding:12px;margin-top:8px;"></div>

			<!-- Locate-me FAB (visible on all viewports) -->
			<button class="map-locate-fab" id="locate-me-fab" title="Center on my location">\u{1F3AF}</button>
			<button class="map-locate-fab" id="fit-all-fab" title="Show all participants" style="bottom:80px;">\u{1F465}</button>

			<!-- Mobile floating buttons (rmaps-online style) -->
			<div class="mobile-float-btns">
				<button class="mobile-pill-btn mobile-pill-left" id="mobile-friends-btn">&#128101; See Friends (<span id="mobile-friends-count">0</span>)</button>
				<button class="mobile-pill-btn mobile-pill-right" id="mobile-qr-btn">&#9641; QR</button>
			</div>

			<!-- Floating share-location button on map -->
			<button class="map-share-float ${this.sharingLocation ? "active" : ""} ${this.privacySettings.precision === "hidden" ? "ghost" : ""}" id="map-share-float">
				${this.privacySettings.precision === "hidden" ? "&#128123; Hidden" : this.sharingLocation ? "&#128205; Sharing" : "&#128205; Share Location"}
			</button>

			<!-- Mobile bottom sheet (participants) -->
			<div class="mobile-bottom-sheet" id="mobile-bottom-sheet">
				<div class="sheet-handle" id="sheet-handle">
					<div class="sheet-handle-bar"></div>
				</div>
				<div class="sheet-header">
					<span>Friends (<span id="sheet-participant-count">0</span>)</span>
					<button id="sheet-close-btn" style="background:none;border:none;color:var(--rs-text-muted);cursor:pointer;font-size:14px;padding:2px 6px;">&#10005;</button>
				</div>
				<div class="sheet-content" id="participant-list-mobile"></div>
			</div>

			<!-- Mobile privacy popup (hidden by default, triggered from bottom sheet) -->
			<div class="mobile-privacy-popup" id="mobile-privacy-popup" style="display:none;"></div>
		`;
	}

	private attachListeners() {
		this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour());
		this.shadow.getElementById("create-room")?.addEventListener("click", () => this.createRoom());

		// Lobby profile section
		const lobbyNameInput = this.shadow.getElementById("lobby-name-input") as HTMLInputElement;
		if (lobbyNameInput) {
			lobbyNameInput.addEventListener("change", () => {
				const name = lobbyNameInput.value.trim();
				if (name) {
					this.userName = name;
					localStorage.setItem("rmaps_user", JSON.stringify({
						id: this.participantId, name: this.userName,
						emoji: this.userEmoji, color: this.userColor,
					}));
				}
			});
		}
		const lobbyAvatar = this.shadow.getElementById("lobby-avatar-preview");
		const lobbyEmojiGrid = this.shadow.getElementById("lobby-emoji-grid");
		if (lobbyAvatar && lobbyEmojiGrid) {
			lobbyAvatar.addEventListener("click", () => {
				lobbyEmojiGrid.style.display = lobbyEmojiGrid.style.display === "none" ? "flex" : "none";
			});
		}
		this.shadow.querySelectorAll("[data-lobby-emoji]").forEach(btn => {
			btn.addEventListener("click", () => {
				const emoji = (btn as HTMLElement).dataset.lobbyEmoji!;
				this.userEmoji = emoji;
				if (lobbyAvatar) lobbyAvatar.textContent = emoji;
				this.shadow.querySelectorAll("[data-lobby-emoji]").forEach(b => {
					const sel = (b as HTMLElement).dataset.lobbyEmoji === emoji;
					(b as HTMLElement).style.borderColor = sel ? "#4f46e5" : "transparent";
					(b as HTMLElement).style.background = sel ? "#4f46e520" : "var(--rs-bg-surface-sunken)";
				});
				localStorage.setItem("rmaps_user", JSON.stringify({
					id: this.participantId, name: this.userName,
					emoji: this.userEmoji, color: this.userColor,
				}));
			});
		});
		this.shadow.getElementById("lobby-rejoin-btn")?.addEventListener("click", () => {
			const lastRoom = localStorage.getItem("rmaps_last_room");
			if (lastRoom) {
				// Save name from input if provided
				if (lobbyNameInput?.value.trim()) {
					this.userName = lobbyNameInput.value.trim();
					localStorage.setItem("rmaps_user", JSON.stringify({
						id: this.participantId, name: this.userName,
						emoji: this.userEmoji, color: this.userColor,
					}));
				}
				this._history.push("lobby");
				this._history.push("map", { room: lastRoom });
				this.joinRoom(lastRoom);
			}
		});

		this.shadow.querySelectorAll("[data-room]").forEach((el) => {
			el.addEventListener("click", () => {
				const room = (el as HTMLElement).dataset.room!;
				this._history.push("lobby");
				this._history.push("map", { room });
				this.joinRoom(room);
			});
		});

		this.shadow.querySelectorAll("[data-back]").forEach((el) => {
			el.addEventListener("click", () => {
				this.goBack();
			});
		});

		this.shadow.getElementById("share-location")?.addEventListener("click", () => {
			this.toggleLocationSharing();
		});

		// Header share toggle
		this.shadow.getElementById("header-share-toggle")?.addEventListener("click", () => {
			this.toggleLocationSharing();
		});

		// Header participants toggle
		this.shadow.getElementById("header-participants-toggle")?.addEventListener("click", () => {
			// Desktop: toggle sidebar visibility, Mobile: toggle bottom sheet
			const sidebar = this.shadow.querySelector(".map-sidebar") as HTMLElement;
			if (sidebar && window.innerWidth > 768) {
				sidebar.style.display = sidebar.style.display === "none" ? "" : "none";
			}
			const sheet = this.shadow.getElementById("mobile-bottom-sheet");
			if (sheet) sheet.classList.toggle("expanded");
		});

		this.shadow.getElementById("drop-waypoint")?.addEventListener("click", () => {
			this.dropWaypoint();
		});

		this.shadow.getElementById("share-room-btn")?.addEventListener("click", () => {
			this.showShareModal = true;
			this.renderShareModal();
		});

		this.shadow.getElementById("privacy-toggle")?.addEventListener("click", () => {
			this.showPrivacyPanel = !this.showPrivacyPanel;
			const panel = this.shadow.getElementById("privacy-panel");
			if (panel) {
				panel.style.display = this.showPrivacyPanel ? "block" : "none";
				if (this.showPrivacyPanel) this.renderPrivacyPanel();
			}
		});

		// Emoji picker
		this.shadow.getElementById("emoji-picker-btn")?.addEventListener("click", () => {
			this.showEmojiPicker = !this.showEmojiPicker;
			this.updateEmojiButton();
		});
		this.shadow.querySelectorAll("[data-emoji-pick]").forEach((btn) => {
			btn.addEventListener("click", () => {
				this.changeEmoji((btn as HTMLElement).dataset.emojiPick!);
			});
		});

		// Indoor toggle
		this.shadow.getElementById("indoor-toggle")?.addEventListener("click", () => {
			if (this.mapMode === "outdoor") {
				const event = prompt("c3nav event code (e.g. 39c3):", "39c3");
				if (event?.trim()) this.switchToIndoor(event.trim());
			} else {
				this.switchToOutdoor();
			}
		});

		// Sidebar tab switching
		this.shadow.querySelectorAll("[data-sidebar-tab]").forEach((btn) => {
			btn.addEventListener("click", () => {
				this.sidebarTab = (btn as HTMLElement).dataset.sidebarTab as "participants" | "chat";
				const pList = this.shadow.getElementById("participant-list");
				const cPanel = this.shadow.getElementById("chat-panel-container");
				if (pList) pList.style.display = this.sidebarTab === "participants" ? "block" : "none";
				if (cPanel) cPanel.style.display = this.sidebarTab === "chat" ? "block" : "none";

				// Update tab button styles
				this.shadow.querySelectorAll("[data-sidebar-tab]").forEach(b => {
					const isActive = (b as HTMLElement).dataset.sidebarTab === this.sidebarTab;
					(b as HTMLElement).style.borderColor = isActive ? "#4f46e5" : "var(--rs-border)";
					(b as HTMLElement).style.background = isActive ? "#4f46e520" : "transparent";
					(b as HTMLElement).style.color = isActive ? "#818cf8" : "var(--rs-text-muted)";
				});

				// Mount chat panel if switching to chat
				if (this.sidebarTab === "chat" && cPanel && !cPanel.querySelector("map-chat-panel")) {
					this.mountChatPanel(cPanel);
				}

				// Clear unread when viewing chat
				if (this.sidebarTab === "chat") {
					this.unreadCount = 0;
					this.updateChatBadge();
				}
			});
		});

		this.shadow.getElementById("bell-toggle")?.addEventListener("click", () => {
			this.pushManager?.toggle(this.room, this.participantId).then(subscribed => {
				const bell = this.shadow.getElementById("bell-toggle");
				if (bell) bell.textContent = subscribed ? "\u{1F514}" : "\u{1F515}";
			});
		});

		// Locate-me FAB
		this.shadow.getElementById("locate-me-fab")?.addEventListener("click", () => {
			this.locateMe();
		});
		// Fit-all FAB
		this.shadow.getElementById("fit-all-fab")?.addEventListener("click", () => {
			this.fitToParticipants();
		});

		// Mobile floating buttons
		this.shadow.getElementById("mobile-friends-btn")?.addEventListener("click", () => {
			const sheet = this.shadow.getElementById("mobile-bottom-sheet");
			if (sheet) sheet.classList.toggle("expanded");
		});

		this.shadow.getElementById("mobile-qr-btn")?.addEventListener("click", () => {
			this.showShareModal = true;
			this.renderShareModal();
		});

		// Floating share-location button on map
		this.shadow.getElementById("map-share-float")?.addEventListener("click", () => {
			this.toggleLocationSharing();
		});

		// Sheet close button
		this.shadow.getElementById("sheet-close-btn")?.addEventListener("click", () => {
			const s = this.shadow.getElementById("mobile-bottom-sheet");
			if (s) s.classList.remove("expanded");
		});

		// Mobile bottom sheet
		const sheet = this.shadow.getElementById("mobile-bottom-sheet");
		const sheetHandle = this.shadow.getElementById("sheet-handle");
		if (sheet && sheetHandle) {
			sheetHandle.addEventListener("click", () => {
				sheet.classList.toggle("expanded");
			});
			// Touch drag on handle
			let startY = 0;
			let sheetWasExpanded = false;
			sheetHandle.addEventListener("touchstart", (e: Event) => {
				const te = e as TouchEvent;
				startY = te.touches[0].clientY;
				sheetWasExpanded = sheet.classList.contains("expanded");
			}, { passive: true });
			sheetHandle.addEventListener("touchend", (e: Event) => {
				const te = e as TouchEvent;
				const deltaY = te.changedTouches[0].clientY - startY;
				if (sheetWasExpanded && deltaY > 40) {
					sheet.classList.remove("expanded");
				} else if (!sheetWasExpanded && deltaY < -40) {
					sheet.classList.add("expanded");
				}
			}, { passive: true });
		}

		// Ping buttons on history cards
		this.shadow.querySelectorAll("[data-ping-room]").forEach((btn) => {
			btn.addEventListener("click", (e) => {
				e.stopPropagation();
				const slug = (btn as HTMLElement).dataset.pingRoom!;
				this.pushManager?.requestLocation(slug, "all");
				(btn as HTMLElement).textContent = "\u2713";
				setTimeout(() => { (btn as HTMLElement).textContent = "\u{1F514}"; }, 2000);
			});
		});
	}

	private updateMobileFab() {
		// No-op — legacy FAB removed; share state updated via updateShareButton()
	}

	private _onViewRestored = (e: CustomEvent) => {
		if (e.detail?.moduleId !== 'rmaps') return;
		if (e.detail.view === "lobby" && this.view === "map") this.leaveRoom();
		this.view = e.detail.view;
		if (e.detail.view === "lobby") this.loadStats();
	};

	private goBack() {
		const prev = this._history.back();
		if (!prev) return;
		if (prev.view === "lobby" && this.view === "map") {
			this.leaveRoom();
		}
		this.view = prev.view;
		if (prev.view === "lobby") this.loadStats();
	}

	private esc(s: string): string {
		const d = document.createElement("div");
		d.textContent = s || "";
		return d.innerHTML;
	}
}

customElements.define("folk-map-viewer", FolkMapViewer);
