<!--
Phase 11 map active view: integrates the Phase 9 renderer with the
per-game `GameStateStore` provided through context by
`routes/games/[id]/+layout.svelte`. The view mounts the renderer
once the store has produced a report and re-mounts when the
report's turn changes (a refresh that returns the same turn keeps
the existing renderer instance alive). Empty-planet reports render
the empty world without errors — the regression test in
`tests/e2e/game-shell-map.spec.ts` covers this.

Phase 9 owns the renderer's hit-test and pan/zoom semantics. Phase 13
plugs map clicks into the inspector by translating the renderer's
`clicked` event into a hit-test, looking the planet up by id in the
report, and calling `SelectionStore.selectPlanet`. The selection
store, set in the layout, drives both the desktop sidebar inspector
tab and the mobile bottom-sheet — the map view itself does not need
to know which surface is showing the result.

Phase 29 wires the wrap-mode toggle on top of the per-game `wrapMode`
preference the store already manages.
-->
<script lang="ts">
	import { getContext, onDestroy, onMount, untrack } from "svelte";
	import { goto } from "$app/navigation";
	import { page } from "$app/state";
	import { i18n } from "$lib/i18n/index.svelte";
	import {
		createRenderer,
		minScaleNoWrap,
		type RendererHandle,
	} from "../../map/index";
	import { buildCargoRouteLines } from "../../map/cargo-routes";
	import { buildPendingSendLines } from "../../map/pending-send-routes";
	import { computeReachCircles } from "../../map/reach-circles";
	import { reachStore } from "$lib/calculator/reach.svelte";
	import {
		reportToWorld,
		type HitTarget,
		type MapCategory,
	} from "../../map/state-binding";
	import {
		computeFogCircles,
		computeHiddenIds,
		computeHiddenPlanetNumbers,
		fingerprintHiddenPlanets,
	} from "../../map/visibility";
	import type { PrimitiveID } from "../../map/world";
	import {
		ORDER_DRAFT_CONTEXT_KEY,
		OrderDraftStore,
	} from "../../sync/order-draft.svelte";
	import type { OrderCommand } from "../../sync/order-types";
	import {
		GAME_STATE_CONTEXT_KEY,
		type GameStateStore,
		type MapToggles,
	} from "$lib/game-state.svelte";
	import {
		SELECTION_CONTEXT_KEY,
		type SelectionStore,
	} from "$lib/selection.svelte";
	import {
		RENDERED_REPORT_CONTEXT_KEY,
		type RenderedReportSource,
	} from "$lib/rendered-report.svelte";
	import {
		MAP_PICK_CONTEXT_KEY,
		MapPickService,
	} from "$lib/map-pick.svelte";
	import {
		installRendererDebugSurface,
		registerMapCameraProvider,
		registerMapFogProvider,
		registerMapModeProvider,
		registerMapPickStateProvider,
		registerMapPrimitivesProvider,
		registerMapRenderCountProvider,
		type MapCameraSnapshot,
		type MapFogSnapshot,
		type MapPickStateSnapshot,
		type MapPrimitiveSnapshot,
	} from "$lib/debug-surface.svelte";
	import MapTogglesControl from "./map-toggles.svelte";

	const store = getContext<GameStateStore | undefined>(GAME_STATE_CONTEXT_KEY);
	const renderedReport = getContext<RenderedReportSource | undefined>(
		RENDERED_REPORT_CONTEXT_KEY,
	);
	const selection = getContext<SelectionStore | undefined>(SELECTION_CONTEXT_KEY);
	// `MapPickService` is owned by the in-game shell layout (set on
	// the context tree the inspector subsections also descend from).
	// Renderer changes attach / detach via `bindResolver` so a
	// remount mid-pick does not orphan a pending promise. The map
	// view is mounted only beneath the layout, so the service is
	// always present in production; tests render the map in isolation
	// and may omit it.
	const pickService = getContext<MapPickService | undefined>(
		MAP_PICK_CONTEXT_KEY,
	);
	// Pending Send commands turn into green dashed tracks on the
	// map overlay; the draft is read alongside the report so the
	// overlay stays in lock-step with the order tab.
	const orderDraft = getContext<OrderDraftStore | undefined>(
		ORDER_DRAFT_CONTEXT_KEY,
	);

	let canvasEl: HTMLCanvasElement | null = $state(null);
	let containerEl: HTMLDivElement | null = $state(null);
	let mountError: string | null = $state(null);

	let handle: RendererHandle | null = null;
	let hitLookup = new Map<PrimitiveID, HitTarget>();
	// currentCategories / currentPlanetDependents are populated by
	// `reportToWorld` inside `mountRenderer` and consumed by the
	// Phase 29 hide-set computation on every effect re-run (mount or
	// toggle change). Both maps cover the base world; extras
	// (cargo-routes, pending-Send) are gated upstream via
	// `skipPlanets`, so they never need a categories entry.
	let currentCategories: ReadonlyMap<PrimitiveID, MapCategory> = new Map();
	let currentPlanetDependents: ReadonlyMap<
		number,
		ReadonlySet<PrimitiveID>
	> = new Map();
	// currentFogCircles mirrors the latest `setVisibilityFog` input so
	// the debug surface can report it to Playwright. The renderer
	// keeps the Graphics, not the data; recomputing on every read
	// would duplicate work.
	let currentFogCircles: ReadonlyArray<{
		x: number;
		y: number;
		radius: number;
	}> = [];
	let mountedTurn: number | null = null;
	let mountedGameId: string | null = null;
	let onResize: (() => void) | null = null;
	let detachClick: (() => void) | null = null;
	let detachDebugProviders: (() => void) | null = null;
	let detachDebugSurface: (() => void) | null = null;
	// `mounted` must be `$state` so the renderer-mount effect re-runs
	// once `onMount` flips it true. On the first map navigation the
	// effect's initial pass returns early (gameState is still hydrating
	// → `report` is null), and the subsequent server-driven `report`
	// transition re-fires the effect after `onMount` has already
	// completed. On a second navigation back to /map the report is
	// already loaded — without reactivity here the effect's first
	// pass would gate on `mounted === false`, and there would be no
	// later state change to wake it up. The visible symptom is a
	// black canvas (renderer never re-mounted on the new DOM).
	let mounted = $state(false);
	// Mount serialization. The `$effect` may re-fire while the
	// async `mountRenderer` is mid-flight (e.g. report transitions
	// from null → populated → overlay-mutated during boot). Without
	// the in-progress gate, parallel `createRenderer` awaits would
	// leave both old and new viewport listeners on the canvas,
	// double-firing every click. The gate is intentionally a plain
	// `let` (not `$state`) so reads from the effect do not register
	// as a reactive dependency.
	let mountInProgress = false;
	let pendingMountSignal = $state(0);
	// Track the latest cargo-route fingerprint we pushed to the
	// renderer so a no-op push (e.g. report refresh that yields the
	// same overlay) doesn't churn Pixi graphics needlessly.
	let lastExtrasFingerprint: string | null = null;

	$effect(() => {
		// Read the overlay-applied report so the map labels reflect
		// pending renames immediately. Falls back to raw report when
		// the rendered source is missing (e.g. component used outside
		// the in-game shell layout).
		const report = renderedReport?.report ?? store?.report;
		const status = store?.status ?? "idle";
		// Track the wrap mode so the renderer remounts when Phase 29's
		// toggle UI flips it; the read here also subscribes the effect.
		const mode = store?.wrapMode ?? "torus";
		// Track the Phase 29 visibility toggles so the effect re-runs
		// when the gear popover flips any flag. The hide set + fog +
		// extras filter all derive from this rune.
		const toggles = store?.mapToggles;
		const gameId = store?.gameId ?? "";
		if (!mounted || canvasEl === null || containerEl === null) return;
		if (status !== "ready" || !report || toggles === undefined) return;

		// Explicit reads of every toggle key — Svelte 5's deep proxy
		// tracks per-property access, and the actual consumers
		// (computeHiddenIds, computeFogCircles, buildExtras) run
		// inside `untrack` blocks or async continuations where the
		// tracking would otherwise be lost. Touching every key here
		// synchronously guarantees a flip triggers the effect.
		void toggles.hyperspaceGroups;
		void toggles.incomingGroups;
		void toggles.unidentifiedGroups;
		void toggles.foreignPlanets;
		void toggles.uninhabitedPlanets;
		void toggles.unidentifiedPlanets;
		void toggles.unreachablePlanets;
		void toggles.cargoRoutes;
		void toggles.battleMarkers;
		void toggles.bombingMarkers;
		void toggles.visibleHyperspace;

		// Subscribe to the calculator's published reach so the rings
		// redraw as the design or the selected planet changes.
		void reachStore.origin;
		void reachStore.speedPerTurn;

		// Phase 29 visibility derivation. Cargo routes and pending-
		// Send overlay are extras (no Pixi remount on flip); the
		// cascade-filtering happens here so the extras list shrinks
		// when a destination planet hides. The hide set + fog are
		// applied after mount / on every toggle change without a
		// remount.
		const hiddenPlanetNumbers = computeHiddenPlanetNumbers(report, toggles);
		const hiddenPlanetFingerprint =
			fingerprintHiddenPlanets(hiddenPlanetNumbers);

		// Cargo-route arrows and pending-Send tracks are pushed onto
		// the live renderer via `setExtraPrimitives` so the overlay
		// can change inside a single turn without disposing the Pixi
		// `Application` — Pixi 8 does not reliably re-init on the
		// same canvas. The fingerprint guard avoids redundant Pixi
		// rebuilds when the overlay computation re-runs but the
		// routes / pending-Send content is unchanged (e.g. status
		// transitions valid → submitting → applied for the same
		// command). The Phase 29 cascade + cargoRoutes toggle are
		// folded into the fingerprint so a toggle flip that changes
		// the visible set reliably triggers a push.
		const draftCommands = orderDraft?.commands ?? [];
		const draftStatuses = orderDraft?.statuses ?? {};
		const reachOrigin = reachStore.origin;
		const reachFingerprint =
			reachOrigin === null
				? ""
				: `${reachOrigin.x},${reachOrigin.y},${reachStore.speedPerTurn}`;
		const extrasFingerprint =
			`cr=${toggles.cargoRoutes ? "1" : "0"}|hp=${hiddenPlanetFingerprint}|` +
			`reach=${reachFingerprint}|` +
			computeRoutesFingerprint(report.routes) +
			"|" +
			computePendingSendFingerprint(draftCommands, draftStatuses);

		const sameSnapshot =
			mountedTurn === report.turn &&
			mountedGameId === gameId &&
			handle !== null;
		if (sameSnapshot) {
			// Apply wrap-mode flips in-place via the renderer's own
			// `setMode` — a full re-mount is unnecessary (the world,
			// primitives, and camera are unchanged) and Pixi 8 does
			// not reliably re-init on the same canvas (the symptom is
			// a crashed tab when the wrap-mode radio fires).
			if (handle !== null && handle.getMode() !== mode) {
				untrack(() => {
					handle?.setMode(mode);
				});
			}
			// Always re-apply hide set + fog on a same-snapshot pass:
			// toggle flips bypass the extras fingerprint when they
			// only change which baked-world primitives are hidden,
			// and a no-op `setHiddenPrimitiveIds` is cheap.
			untrack(() => {
				applyVisibilityState(report, toggles, hiddenPlanetNumbers);
			});
			if (lastExtrasFingerprint !== extrasFingerprint) {
				untrack(() => {
					handle?.setExtraPrimitives(
						buildExtras(
							report,
							draftCommands,
							draftStatuses,
							toggles,
							hiddenPlanetNumbers,
							mode,
						),
					);
				});
				lastExtrasFingerprint = extrasFingerprint;
			}
			return;
		}

		// Read the pending-mount signal so the effect re-runs after
		// the in-flight mount completes (it bumps the signal in its
		// finally block). Without this, a dep change observed while
		// `mountInProgress` is true would be silently dropped.
		void pendingMountSignal;
		if (mountInProgress) return;
		untrack(() => {
			void runSerializedMount(
				report,
				mode,
				toggles,
				hiddenPlanetNumbers,
				extrasFingerprint,
				draftCommands,
				draftStatuses,
			);
		});
	});

	function buildExtras(
		report: NonNullable<GameStateStore["report"]>,
		draftCommands: readonly OrderCommand[],
		draftStatuses: Readonly<Record<string, string>>,
		toggles: MapToggles,
		hiddenPlanetNumbers: ReadonlySet<number>,
		mode: "torus" | "no-wrap",
	): import("../../map/world").Primitive[] {
		const skip = hiddenPlanetNumbers.size > 0 ? hiddenPlanetNumbers : undefined;
		const cargo = toggles.cargoRoutes
			? buildCargoRouteLines(report, skip ? { skipPlanets: skip } : undefined)
			: [];
		const pending = buildPendingSendLines(
			report,
			draftCommands,
			draftStatuses,
			skip ? { skipPlanets: skip } : undefined,
		);
		// Reach circles published by the ship-class calculator. Empty
		// when no own planet is selected or the design is invalid, so
		// this is a no-op for the rest of the map.
		const reachOrigin = reachStore.origin;
		const reach =
			reachOrigin !== null && reachStore.speedPerTurn > 0
				? computeReachCircles(
						reachOrigin,
						reachStore.speedPerTurn,
						report.mapWidth,
						report.mapHeight,
						mode,
					)
				: [];
		return [...cargo, ...pending, ...reach];
	}

	function applyVisibilityState(
		report: NonNullable<GameStateStore["report"]>,
		toggles: MapToggles,
		hiddenPlanetNumbers: ReadonlySet<number>,
	): void {
		if (handle === null) return;
		const hiddenIds = computeHiddenIds(
			currentCategories,
			currentPlanetDependents,
			hiddenPlanetNumbers,
			toggles,
		);
		handle.setHiddenPrimitiveIds(hiddenIds);
		const fogCircles = computeFogCircles(report, toggles);
		currentFogCircles = fogCircles;
		handle.setVisibilityFog(fogCircles);
	}

	async function runSerializedMount(
		report: NonNullable<GameStateStore["report"]>,
		mode: "torus" | "no-wrap",
		toggles: MapToggles,
		hiddenPlanetNumbers: ReadonlySet<number>,
		extrasFingerprint: string,
		draftCommands: readonly OrderCommand[],
		draftStatuses: Readonly<Record<string, string>>,
	): Promise<void> {
		mountInProgress = true;
		try {
			await mountRenderer(report, mode);
			if (handle === null) return;
			applyVisibilityState(report, toggles, hiddenPlanetNumbers);
			handle.setExtraPrimitives(
				buildExtras(
					report,
					draftCommands,
					draftStatuses,
					toggles,
					hiddenPlanetNumbers,
					mode,
				),
			);
			lastExtrasFingerprint = extrasFingerprint;
		} finally {
			mountInProgress = false;
			// Bump the reactive signal so any dep change observed
			// while the gate was up gets a fresh effect run with the
			// current state.
			pendingMountSignal += 1;
		}
	}

	function computeRoutesFingerprint(
		routes: NonNullable<GameStateStore["report"]>["routes"],
	): string {
		if (routes.length === 0) return "";
		const parts = routes.map((route) => {
			const entries = route.entries
				.map((entry) => `${entry.loadType}->${entry.destinationPlanetNumber}`)
				.join(",");
			return `${route.sourcePlanetNumber}:${entries}`;
		});
		return parts.join(";");
	}

	function computePendingSendFingerprint(
		commands: readonly OrderCommand[],
		statuses: Readonly<Record<string, string>>,
	): string {
		const parts: string[] = [];
		for (const cmd of commands) {
			if (cmd.kind !== "sendShipGroup") continue;
			const status = statuses[cmd.id];
			if (status === "rejected" || status === "invalid") continue;
			parts.push(`${cmd.groupId}->${cmd.destinationPlanetNumber}`);
		}
		return parts.join(";");
	}

	async function mountRenderer(
		report: NonNullable<GameStateStore["report"]>,
		mode: "torus" | "no-wrap",
	): Promise<void> {
		if (canvasEl === null || containerEl === null) return;
		// Capture camera state before disposing so a remount inside
		// the same game (e.g. cargo-route overlay change) keeps the
		// user's pan/zoom. A new game / first mount has no prior
		// camera, so `previousCamera` stays null and the default
		// centring path runs.
		const previousGameId = mountedGameId;
		const targetGameId = store?.gameId ?? "";
		const previousCamera =
			handle !== null && previousGameId === targetGameId
				? handle.getCamera()
				: null;
		if (detachClick !== null) {
			detachClick();
			detachClick = null;
		}
		// Detach the previous resolver before disposing — the
		// renderer's `dispose` already calls `onPick(null)` on any
		// open session, which `bindResolver(null)` would also do, so
		// we route the cancel through one path only.
		pickService?.bindResolver(null);
		if (detachDebugProviders !== null) {
			detachDebugProviders();
			detachDebugProviders = null;
		}
		if (handle !== null) {
			handle.dispose();
			handle = null;
		}
		try {
			const {
				world,
				hitLookup: nextHitLookup,
				categories,
				planetDependents,
			} = reportToWorld(report);
			hitLookup = nextHitLookup;
			currentCategories = categories;
			currentPlanetDependents = planetDependents;
			handle = await createRenderer({
				canvas: canvasEl,
				world,
				mode,
				preference: ["webgpu", "webgl"],
			});
			const minScale = minScaleNoWrap(
				{
					widthPx: containerEl.clientWidth,
					heightPx: containerEl.clientHeight,
				},
				world,
			);
			if (previousCamera !== null) {
				// Same-game remount — preserve pan/zoom. Clamp zoom
				// to `minScale` so a remount that re-derives the
				// minimum (e.g. a viewport resize between renderers)
				// does not strand the user below the current floor.
				handle.viewport.moveCenter(
					previousCamera.centerX,
					previousCamera.centerY,
				);
				handle.viewport.setZoom(
					Math.max(previousCamera.scale, minScale),
					true,
				);
			} else {
				handle.viewport.moveCenter(world.width / 2, world.height / 2);
				handle.viewport.setZoom(minScale * 1.05, true);
			}
			if (mode === "no-wrap") handle.setMode("no-wrap");
			detachClick = handle.onClick(handleMapClick);
			pickService?.bindResolver(({ sourcePlanetNumber, reachableIds, onResolve }) => {
				if (handle === null) {
					onResolve(null);
					return null;
				}
				const planet = report.planets.find(
					(p) => p.number === sourcePlanetNumber,
				);
				if (planet === undefined) {
					onResolve(null);
					return null;
				}
				return handle.setPickMode({
					sourcePrimitiveId: sourcePlanetNumber,
					sourceX: planet.x,
					sourceY: planet.y,
					reachableIds,
					onPick: onResolve,
				});
			});
			const detachPrim = registerMapPrimitivesProvider(() => {
				const h = handle;
				if (h === null) return [];
				return h.getPrimitives().map<MapPrimitiveSnapshot>((p) => ({
					id: p.id,
					kind: p.kind,
					priority: p.priority,
					alpha: h.getPrimitiveAlpha(p.id),
					fillColor: p.style.fillColor ?? null,
					strokeColor: p.style.strokeColor ?? null,
					x: p.kind === "point" ? p.x : null,
					y: p.kind === "point" ? p.y : null,
					visible: !h.isPrimitiveHidden(p.id),
				}));
			});
			const detachPick = registerMapPickStateProvider(() => {
				const h = handle;
				if (h === null) {
					return {
						active: false,
						sourcePlanetNumber: null,
						reachableIds: [],
						hoveredId: null,
					} satisfies MapPickStateSnapshot;
				}
				const state = h.getPickState();
				return {
					active: state.active,
					sourcePlanetNumber:
						state.sourcePrimitiveId === null
							? null
							: Number(state.sourcePrimitiveId),
					reachableIds:
						state.reachableIds === null
							? []
							: Array.from(state.reachableIds).map((id) => Number(id)),
					hoveredId:
						state.hoveredId === null ? null : Number(state.hoveredId),
				} satisfies MapPickStateSnapshot;
			});
			const detachCamera = registerMapCameraProvider(() => {
				const h = handle;
				if (h === null) return null;
				const camera = h.getCamera();
				const viewport = h.getViewport();
				const rect = canvasEl?.getBoundingClientRect();
				return {
					camera,
					viewport,
					canvasOrigin: {
						x: rect?.left ?? 0,
						y: rect?.top ?? 0,
					},
				} satisfies MapCameraSnapshot;
			});
			const detachFog = registerMapFogProvider(() => ({
				circles: currentFogCircles.map((c) => ({ ...c })),
			}) satisfies MapFogSnapshot);
			const detachMode = registerMapModeProvider(() =>
				handle === null ? null : handle.getMode(),
			);
			const detachRenderCount = registerMapRenderCountProvider(() =>
				handle === null ? null : handle.getRenderCount(),
			);
			detachDebugProviders = (): void => {
				detachPrim();
				detachPick();
				detachCamera();
				detachFog();
				detachMode();
				detachRenderCount();
			};
			mountedTurn = report.turn;
			mountedGameId = targetGameId;
			// runSerializedMount immediately pushes the visibility
			// state + extras after this resolves; clearing the
			// fingerprint here is defensive in case the post-mount
			// path is ever bypassed (e.g. mount-then-throw before the
			// extras push). The hide set / fog are applied by the
			// caller too, so we do not call them here.
			lastExtrasFingerprint = null;
			mountError = null;
		} catch (err) {
			mountError = err instanceof Error ? err.message : String(err);
		}
	}

	// handleMapClick translates a renderer click into a selection
	// update. A click that misses every primitive (empty space) is a
	// deliberate no-op: the selection rule from Phase 13 is that only
	// the explicit close button on the mobile sheet clears the
	// current selection. The Phase 19 ship-group surface dispatches
	// through the same `hit-test` plumbing — the hitLookup map keyed
	// by primitive id resolves a hit back to either a planet or a
	// ship-group selection variant.
	function handleMapClick(cursorPx: { x: number; y: number }): void {
		if (handle === null || store?.report === undefined || store.report === null) {
			return;
		}
		if (selection === undefined) return;
		const hit = handle.hitAt(cursorPx);
		if (hit === null) return;
		const target = hitLookup.get(hit.primitive.id);
		if (target === undefined) return;
		switch (target.kind) {
			case "planet":
				if (hit.primitive.kind !== "point") return;
				selection.selectPlanet(target.number);
				break;
			case "shipGroup":
				if (hit.primitive.kind !== "point") return;
				selection.selectShipGroup(target.ref);
				break;
			case "battle": {
				const gameId = page.params.id ?? "";
				const turn = store?.report?.turn ?? 0;
				void goto(
					`/games/${gameId}/battle/${target.battleId}?turn=${turn}`,
				);
				break;
			}
			case "bombing": {
				const gameId = page.params.id ?? "";
				void goto(
					`/games/${gameId}/report#report-bombings`,
				).then(() => {
					if (typeof document === "undefined") return;
					const row = document.querySelector(
						`[data-testid="report-bombing-row"][data-planet="${target.planet}"]`,
					);
					if (row && row.scrollIntoView) {
						row.scrollIntoView({ behavior: "smooth", block: "center" });
					}
				});
				break;
			}
		}
	}

	onMount(() => {
		mounted = true;
		onResize = (): void => {
			if (handle === null || containerEl === null) return;
			handle.resize(containerEl.clientWidth, containerEl.clientHeight);
		};
		window.addEventListener("resize", onResize);
		// In DEV the in-game shell mounts on a fresh document load
		// (`page.goto`), which discards anything the
		// `/__debug/store` route may have installed earlier in the
		// session. The renderer-side accessors are still useful for
		// e2e specs driving the map, so we install them here too.
		if (import.meta.env.DEV) {
			detachDebugSurface = installRendererDebugSurface();
		}
	});

	onDestroy(() => {
		mounted = false;
		if (onResize !== null) {
			window.removeEventListener("resize", onResize);
			onResize = null;
		}
		if (detachClick !== null) {
			detachClick();
			detachClick = null;
		}
		pickService?.bindResolver(null);
		if (detachDebugProviders !== null) {
			detachDebugProviders();
			detachDebugProviders = null;
		}
		if (detachDebugSurface !== null) {
			detachDebugSurface();
			detachDebugSurface = null;
		}
		if (handle !== null) {
			handle.dispose();
			handle = null;
		}
	});
</script>

<section class="active-view" data-testid="active-view-map" data-status={store?.status ?? "idle"}>
	{#if store?.status === "error"}
		<p class="overlay error" role="alert" data-testid="map-error">
			{store.error ?? "request failed"}
		</p>
	{:else if mountError !== null}
		<p class="overlay error" role="alert" data-testid="map-mount-error">
			{mountError}
		</p>
	{:else if store?.status !== "ready"}
		<p class="overlay" data-testid="map-loading">{i18n.t("common.loading")}</p>
	{/if}
	<div
		class="canvas-wrap"
		data-testid="map-canvas-wrap"
		data-planet-count={store?.report?.planets.length ?? 0}
		bind:this={containerEl}
	>
		<canvas bind:this={canvasEl}></canvas>
		{#if store !== undefined && store.status === "ready"}
			<MapTogglesControl {store} />
		{/if}
	</div>
</section>

<style>
	.active-view {
		position: relative;
		display: flex;
		flex-direction: column;
		height: 100%;
		min-height: 0;
	}
	.canvas-wrap {
		flex: 1;
		min-height: 0;
		position: relative;
		overflow: hidden;
		background: var(--color-bg);
	}
	canvas {
		display: block;
		width: 100%;
		height: 100%;
	}
	.overlay {
		position: absolute;
		top: 0.75rem;
		left: 50%;
		transform: translateX(-50%);
		padding: 0.4rem 0.9rem;
		background: rgba(20, 24, 42, 0.85);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: 6px;
		z-index: 10;
		font-family: system-ui, sans-serif;
		font-size: 0.9rem;
		margin: 0;
	}
	.overlay.error {
		background: var(--color-danger-subtle);
		border-color: var(--color-danger);
		color: var(--color-danger);
	}
</style>
