import { useEffect, useState, useCallback, useRef } from 'react';
import type { Site } from '@/types/index.ts';
import type { Preset } from '@/services/api.ts';
import { api } from '@/services/api.ts';
import { useSitesStore } from '@/store/sites.ts';
import { useCoverageStore } from '@/store/coverage.ts';
import { useSettingsStore } from '@/store/settings.ts';
import { useHistoryStore, pushToFuture, pushToPast } from '@/store/history.ts';
import { useToolStore } from '@/store/tools.ts';
import { useToastStore } from '@/components/ui/Toast.tsx';
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts.ts';
import { useUnsavedChanges } from '@/hooks/useUnsavedChanges.ts';
import { logger } from '@/utils/logger.ts';
import { db } from '@/db/schema.ts';
import MapView from '@/components/map/Map.tsx';
import GeographicHeatmap from '@/components/map/GeographicHeatmap.tsx';
import WebGLCoverageLayer from '@/components/map/WebGLCoverageLayer.tsx';
import CoverageBoundary from '@/components/map/CoverageBoundary.tsx';
import HeatmapLegend from '@/components/map/HeatmapLegend.tsx';
import SiteList from '@/components/panels/SiteList.tsx';
import ExportPanel from '@/components/panels/ExportPanel.tsx';
import ProjectPanel from '@/components/panels/ProjectPanel.tsx';
import CoverageStats from '@/components/panels/CoverageStats.tsx';
import HistoryPanel from '@/components/panels/HistoryPanel.tsx';
import BatchFrequencyChange from '@/components/panels/BatchFrequencyChange.tsx';
import ResultsPanel from '@/components/panels/ResultsPanel.tsx';
import SiteImportExport from '@/components/panels/SiteImportExport.tsx';
import { SiteConfigModal } from '@/components/modals/index.ts';
import type { SiteFormValues } from '@/components/modals/index.ts';
import ToastContainer from '@/components/ui/Toast.tsx';
import ThemeToggle from '@/components/ui/ThemeToggle.tsx';
import GPUIndicator from '@/components/ui/GPUIndicator.tsx';
import TerrainProfile from '@/components/map/TerrainProfile.tsx';
import LinkBudgetPanel from '@/components/panels/LinkBudgetPanel.tsx';
import LinkBudgetOverlay from '@/components/map/LinkBudgetOverlay.tsx';
import Button from '@/components/ui/Button.tsx';
import NumberInput from '@/components/ui/NumberInput.tsx';
import ConfirmDialog from '@/components/ui/ConfirmDialog.tsx';
import { RegionWizard } from '@/components/RegionWizard.tsx';
import { isDesktop, getDesktopApi } from '@/lib/desktop.ts';
import { wsService } from '@/services/websocket.ts';
import type { RegionInfo, CacheStats } from '@/services/api.ts';

/**
 * Restore a sites snapshot: replace all sites in IndexedDB + Zustand.
 * Used by undo/redo.
 */
async function restoreSites(snapshot: Site[]) {
  // Clear current DB entries
  await db.sites.clear();
  // Write snapshot sites to DB
  for (const site of snapshot) {
    await db.sites.put({
      id: site.id,
      data: JSON.stringify(site),
      createdAt: new Date(site.createdAt).getTime(),
      updatedAt: new Date(site.updatedAt).getTime(),
    });
  }
  // Reload from DB to sync Zustand
  await useSitesStore.getState().loadSites();
}

export default function App() {
  const loadSites = useSitesStore((s) => s.loadSites);
  const sites = useSitesStore((s) => s.sites);
  const selectedSiteId = useSitesStore((s) => s.selectedSiteId);

  const coverageResult = useCoverageStore((s) => s.result);
  const isCalculating = useCoverageStore((s) => s.isCalculating);
  const settings = useCoverageStore((s) => s.settings);
  const heatmapVisible = useCoverageStore((s) => s.heatmapVisible);
  const coverageError = useCoverageStore((s) => s.error);
  const coverageProgress = useCoverageStore((s) => s.progress);
  const partialPoints = useCoverageStore((s) => s.partialPoints);
  const calculateCoverageApi = useCoverageStore((s) => s.calculateCoverage);
  const cancelCalculation = useCoverageStore((s) => s.cancelCalculation);

  // Propagation presets from API
  const [presets, setPresets] = useState<Record<string, Preset>>({});
  const [showAdvanced, setShowAdvanced] = useState(false);

  // Elapsed time counter during calculation
  const [elapsed, setElapsed] = useState(0);
  useEffect(() => {
    if (!isCalculating) {
      setElapsed(0);
      return;
    }
    const start = Date.now();
    const interval = setInterval(() => {
      setElapsed(Math.floor((Date.now() - start) / 1000));
    }, 1000);
    return () => clearInterval(interval);
  }, [isCalculating]);

  // Load presets on mount
  useEffect(() => {
    api.getPresets().then(setPresets).catch((err) => {
      logger.warn('Failed to load presets:', err);
    });
  }, []);

  // Connect WebSocket for real-time coverage progress
  useEffect(() => {
    wsService.connect();
    return () => wsService.disconnect();
  }, []);

  const addToast = useToastStore((s) => s.addToast);

  const showTerrain = useSettingsStore((s) => s.showTerrain);
  const terrainOpacity = useSettingsStore((s) => s.terrainOpacity);
  const setTerrainOpacity = useSettingsStore((s) => s.setTerrainOpacity);
  const showGrid = useSettingsStore((s) => s.showGrid);
  const setShowGrid = useSettingsStore((s) => s.setShowGrid);
  const showElevationInfo = useSettingsStore((s) => s.showElevationInfo);

  // Tool store (centralized active tool state)
  const activeTool = useToolStore((s) => s.activeTool);
  const setActiveTool = useToolStore((s) => s.setActiveTool);
  const clearTool = useToolStore((s) => s.clearTool);
  const setShowElevationInfo = useSettingsStore((s) => s.setShowElevationInfo);
  const showBoundary = useSettingsStore((s) => s.showBoundary);
  const showElevationOverlay = useSettingsStore((s) => s.showElevationOverlay);
  const setShowElevationOverlay = useSettingsStore((s) => s.setShowElevationOverlay);
  const elevationOpacity = useSettingsStore((s) => s.elevationOpacity);
  const setElevationOpacity = useSettingsStore((s) => s.setElevationOpacity);
  const useWebGLCoverage = useSettingsStore((s) => s.useWebGLCoverage);
  const setUseWebGLCoverage = useSettingsStore((s) => s.setUseWebGLCoverage);

  // History (undo/redo)
  const canUndo = useHistoryStore((s) => s.canUndo);
  const canRedo = useHistoryStore((s) => s.canRedo);

  // Unsaved changes detection + beforeunload
  useUnsavedChanges();

  const [modalState, setModalState] = useState<{
    isOpen: boolean;
    mode: 'create' | 'edit';
    editSiteId?: string;
    initialData?: Partial<SiteFormValues>;
  }>({ isOpen: false, mode: 'create' });
  const [panelCollapsed, setPanelCollapsed] = useState(false);
  const [showShortcuts, setShowShortcuts] = useState(false);
  const [kbDeleteTarget, setKbDeleteTarget] = useState<{ id: string; name: string } | null>(null);
  const [profileEndpoints, setProfileEndpoints] = useState<{ start: [number, number]; end: [number, number] } | null>(null);
  const [showLinkBudget, setShowLinkBudget] = useState(false);
  const [linkBudgetRxPoint, setLinkBudgetRxPoint] = useState<{ lat: number; lon: number } | null>(null);

  // Region wizard for first-run (desktop mode only)
  const [showWizard, setShowWizard] = useState(false);
  const [cachedRegions, setCachedRegions] = useState<RegionInfo[]>([]);
  const [cacheStats, setCacheStats] = useState<CacheStats | null>(null);

  const refreshCacheStatus = useCallback(() => {
    api.getRegions().then(setCachedRegions).catch(() => {});
    api.getCacheStats().then(setCacheStats).catch(() => {});
  }, []);

  useEffect(() => {
    // Load cache status on mount
    refreshCacheStatus();

    if (!isDesktop()) return;
    const skipped = localStorage.getItem('rfcp_region_wizard_skipped');
    if (skipped) return;

    api.getRegions()
      .then((regions) => {
        const hasDownloaded = regions.some((r) => r.downloaded);
        if (!hasDownloaded) {
          setShowWizard(true);
        }
      })
      .catch(() => {
        // Backend not ready yet, skip wizard
      });
  }, [refreshCacheStatus]);

  // Resizable sidebar
  const PANEL_MIN = 300;
  const PANEL_MAX = 600;
  const PANEL_DEFAULT = 380;
  const [panelWidth, setPanelWidth] = useState(() => {
    const saved = localStorage.getItem('rfcp-panel-width');
    const n = saved ? Number(saved) : PANEL_DEFAULT;
    return n >= PANEL_MIN && n <= PANEL_MAX ? n : PANEL_DEFAULT;
  });
  const isDragging = useRef(false);

  const handleDragStart = useCallback((e: React.MouseEvent) => {
    e.preventDefault();
    isDragging.current = true;
    document.body.style.cursor = 'col-resize';
    document.body.style.userSelect = 'none';

    const onMove = (ev: MouseEvent) => {
      if (!isDragging.current) return;
      const newWidth = window.innerWidth - ev.clientX;
      const clamped = Math.max(PANEL_MIN, Math.min(PANEL_MAX, newWidth));
      setPanelWidth(clamped);
    };

    const onUp = () => {
      isDragging.current = false;
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      setPanelWidth((w) => {
        localStorage.setItem('rfcp-panel-width', String(w));
        return w;
      });
    };

    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  }, []);

  // Load sites from IndexedDB on mount
  useEffect(() => {
    loadSites();
  }, [loadSites]);

  // Handle site placement from map click
  const handleSitePlacement = useCallback(
    (lat: number, lon: number) => {
      setModalState({
        isOpen: true,
        mode: 'create',
        initialData: { lat, lon },
      });
      // Tool store clearTool() is called by MapClickHandler after placement
    },
    []
  );

  // Handle RX point placement for Link Budget
  const handleRxPlacement = useCallback(
    (lat: number, lon: number) => {
      setLinkBudgetRxPoint({ lat, lon });
      // Tool store clearTool() is called by MapClickHandler after placement
    },
    []
  );

  const handleEditSite = useCallback((site: Site) => {
    setModalState({
      isOpen: true,
      mode: 'edit',
      editSiteId: site.id,
      initialData: {
        name: site.name,
        lat: site.lat,
        lon: site.lon,
        power: site.power,
        gain: site.gain,
        frequency: site.frequency,
        height: site.height,
        antennaType: site.antennaType,
        azimuth: site.azimuth ?? 0,
        beamwidth: site.beamwidth ?? 65,
        notes: site.notes ?? '',
      },
    });
  }, []);

  const handleAddManual = useCallback(() => {
    setModalState({
      isOpen: true,
      mode: 'create',
    });
  }, []);

  const handleCloseModal = useCallback(() => {
    setModalState((prev) => ({ ...prev, isOpen: false }));
  }, []);

  // Modal save handler
  const handleModalSave = useCallback(async (data: SiteFormValues) => {
    const addSite = useSitesStore.getState().addSite;
    const updateSite = useSitesStore.getState().updateSite;

    if (modalState.mode === 'edit' && modalState.editSiteId) {
      await updateSite(modalState.editSiteId, {
        name: data.name,
        lat: data.lat,
        lon: data.lon,
        power: data.power,
        gain: data.gain,
        frequency: data.frequency,
        height: data.height,
        antennaType: data.antennaType,
        azimuth: data.antennaType === 'sector' ? data.azimuth : undefined,
        beamwidth: data.antennaType === 'sector' ? data.beamwidth : undefined,
        notes: data.notes || undefined,
      });
      addToast('Site updated', 'success');
    } else {
      await addSite({
        name: data.name,
        lat: data.lat,
        lon: data.lon,
        power: data.power,
        gain: data.gain,
        frequency: data.frequency,
        height: data.height,
        antennaType: data.antennaType,
        azimuth: data.antennaType === 'sector' ? data.azimuth : undefined,
        beamwidth: data.antennaType === 'sector' ? data.beamwidth : undefined,
        color: '',
        visible: true,
        notes: data.notes || undefined,
      });
      addToast('Site added', 'success');
    }
    handleCloseModal();
  }, [modalState.mode, modalState.editSiteId, addToast, handleCloseModal]);

  const handleModalDelete = useCallback(async () => {
    if (modalState.editSiteId) {
      const site = sites.find((s) => s.id === modalState.editSiteId);
      await useSitesStore.getState().deleteSite(modalState.editSiteId);
      if (site) {
        addToast(`"${site.name}" deleted`, 'info');
      }
      handleCloseModal();
    }
  }, [modalState.editSiteId, sites, addToast, handleCloseModal]);

  // Keyboard delete confirmation handler
  const handleKbDeleteConfirmed = useCallback(async () => {
    if (!kbDeleteTarget) return;
    const { id, name } = kbDeleteTarget;
    const siteData = sites.find((s) => s.id === id);
    setKbDeleteTarget(null);

    await useSitesStore.getState().deleteSite(id);

    if (siteData) {
      addToast(`"${name}" deleted`, 'info', {
        duration: 10000,
        action: {
          label: 'Undo',
          onClick: async () => {
            await useSitesStore.getState().addSite({
              name: siteData.name,
              lat: siteData.lat,
              lon: siteData.lon,
              height: siteData.height,
              power: siteData.power,
              gain: siteData.gain,
              frequency: siteData.frequency,
              antennaType: siteData.antennaType,
              azimuth: siteData.azimuth,
              beamwidth: siteData.beamwidth,
              color: siteData.color,
              visible: siteData.visible,
              notes: siteData.notes,
              equipment: siteData.equipment,
            });
            addToast(`"${siteData.name}" restored`, 'success');
          },
        },
      });
    }
  }, [kbDeleteTarget, sites, addToast]);

  // === Undo / Redo ===
  const handleUndo = useCallback(async () => {
    const snapshot = useHistoryStore.getState().undo();
    if (!snapshot) return;

    // Save current state to redo stack before restoring
    const currentSites = useSitesStore.getState().sites;
    const currentSettings = useCoverageStore.getState().settings;
    pushToFuture({
      sites: structuredClone(currentSites),
      settings: { ...currentSettings },
      timestamp: Date.now(),
      action: 'before undo',
    });

    // Restore sites from snapshot
    await restoreSites(snapshot.sites);
    useCoverageStore.getState().updateSettings(snapshot.settings);
    useCoverageStore.getState().clearCoverage();
    addToast('Undo', 'info');
  }, [addToast]);

  const handleRedo = useCallback(async () => {
    const snapshot = useHistoryStore.getState().redo();
    if (!snapshot) return;

    // Save current state to undo stack before restoring
    const currentSites = useSitesStore.getState().sites;
    const currentSettings = useCoverageStore.getState().settings;
    pushToPast({
      sites: structuredClone(currentSites),
      settings: { ...currentSettings },
      timestamp: Date.now(),
      action: 'before redo',
    });

    // Restore sites from snapshot
    await restoreSites(snapshot.sites);
    useCoverageStore.getState().updateSettings(snapshot.settings);
    useCoverageStore.getState().clearCoverage();
    addToast('Redo', 'info');
  }, [addToast]);

  // Calculate coverage via backend API
  const handleCalculate = useCallback(async () => {
    const currentSites = useSitesStore.getState().sites;
    if (currentSites.length === 0) {
      addToast('Add at least one site to calculate coverage', 'error');
      return;
    }

    const currentSettings = useCoverageStore.getState().settings;

    // Validation
    if (currentSettings.radius > 50) {
      addToast('Radius too large (max 50km)', 'error');
      return;
    }
    if (currentSettings.resolution < 50) {
      addToast('Resolution too fine (min 50m)', 'error');
      return;
    }

    try {
      await calculateCoverageApi();

      // After calculateCoverageApi returns, check if WS took over.
      // In WS mode, the function returns immediately and result arrives asynchronously.
      const state = useCoverageStore.getState();
      if (state.isCalculating && state.activeCalcId) {
        // WebSocket mode — toast will be shown from the WS onResult callback
        return;
      }

      // HTTP mode — result is ready now
      const result = state.result;
      const error = state.error;

      if (error) {
        let userMessage = 'Calculation failed';
        if (error.includes('timeout') || error.includes('Timeout')) {
          userMessage = 'Calculation timeout. Try reducing radius or increasing resolution.';
        } else if (error.includes('fetch') || error.includes('network') || error.includes('Failed')) {
          userMessage = `API error: ${error}. Check your connection.`;
        } else {
          userMessage = `Calculation failed: ${error}`;
        }
        addToast(userMessage, 'error');
      } else if (result) {
        if (result.points.length === 0) {
          addToast(
            'No coverage points found. Try increasing radius or lowering threshold.',
            'warning'
          );
        } else {
          const timeStr = result.calculationTime.toFixed(1);
          const firstSite = sites.find((s) => s.visible);
          const freqStr = firstSite ? ` \u2022 ${firstSite.frequency} MHz` : '';
          const presetStr = settings.preset ? ` \u2022 ${settings.preset}` : '';
          const modelsStr = result.modelsUsed?.length
            ? ` \u2022 ${result.modelsUsed.length} models`
            : '';
          addToast(
            `${result.totalPoints.toLocaleString()} pts \u2022 ${timeStr}s${presetStr}${freqStr}${modelsStr}`,
            'success'
          );
        }
      }
    } catch (err) {
      logger.error('Coverage calculation error:', err);
      const msg = err instanceof Error ? err.message : 'Unknown error';
      addToast(`Calculation failed: ${msg}`, 'error');
    }
  }, [calculateCoverageApi, addToast]);

  // Save site from modal and trigger calculation
  const handleModalSaveAndCalculate = useCallback(async (data: SiteFormValues) => {
    await handleModalSave(data);
    setTimeout(() => handleCalculate(), 50);
  }, [handleModalSave, handleCalculate]);

  // Keyboard shortcuts
  useKeyboardShortcuts({
    onCalculate: handleCalculate,
    onCloseForm: handleCloseModal,
    onShowShortcuts: useCallback(() => setShowShortcuts(true), []),
    onDeleteRequest: useCallback((id: string, name: string) => {
      setKbDeleteTarget({ id, name });
    }, []),
    onUndo: handleUndo,
    onRedo: handleRedo,
  });

  return (
    <div className="h-screen w-screen flex flex-col bg-gray-100 dark:bg-dark-bg">
      {/* Header */}
      <header className="bg-slate-800 dark:bg-slate-900 text-white px-4 py-2 flex items-center justify-between flex-shrink-0 z-[1010]">
        <div className="flex items-center gap-2">
          <span className="text-base font-bold">RFCP</span>
          <span className="text-xs text-slate-400 hidden sm:inline">
            RF Coverage Planner
          </span>
        </div>
        <div className="flex items-center gap-3 mr-4">
          <GPUIndicator />
          <ThemeToggle />
          {/* Undo / Redo buttons */}
          <div className="hidden sm:flex items-center gap-1">
            <button
              onClick={handleUndo}
              disabled={!canUndo}
              className="text-slate-400 hover:text-white disabled:text-slate-600 disabled:cursor-not-allowed p-1 transition-colors"
              title="Undo (Ctrl+Z)"
            >
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4">
                <path fillRule="evenodd" d="M7.793 2.232a.75.75 0 0 1-.025 1.06L3.622 7.25h10.003a5.375 5.375 0 0 1 0 10.75H10.75a.75.75 0 0 1 0-1.5h2.875a3.875 3.875 0 0 0 0-7.75H3.622l4.146 3.957a.75.75 0 0 1-1.036 1.085l-5.5-5.25a.75.75 0 0 1 0-1.085l5.5-5.25a.75.75 0 0 1 1.06.025Z" clipRule="evenodd" />
              </svg>
            </button>
            <button
              onClick={handleRedo}
              disabled={!canRedo}
              className="text-slate-400 hover:text-white disabled:text-slate-600 disabled:cursor-not-allowed p-1 transition-colors"
              title="Redo (Ctrl+Shift+Z)"
            >
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4">
                <path fillRule="evenodd" d="M12.207 2.232a.75.75 0 0 0 .025 1.06l4.146 3.958H6.375a5.375 5.375 0 0 0 0 10.75H9.25a.75.75 0 0 0 0-1.5H6.375a3.875 3.875 0 0 1 0-7.75h10.003l-4.146 3.957a.75.75 0 0 0 1.036 1.085l5.5-5.25a.75.75 0 0 0 0-1.085l-5.5-5.25a.75.75 0 0 0-1.06.025Z" clipRule="evenodd" />
              </svg>
            </button>
          </div>
          <button
            onClick={() => setShowShortcuts(!showShortcuts)}
            className="text-slate-400 hover:text-white text-sm hidden sm:inline"
            title="Keyboard shortcuts"
          >
            ?
          </button>
          {isCalculating ? (
            <Button
              size="sm"
              variant="secondary"
              onClick={cancelCalculation}
            >
              <span className="flex items-center gap-1">
                <span className="animate-spin inline-block w-3 h-3 border-2 border-white border-t-transparent rounded-full" />
                {coverageProgress
                  ? `${coverageProgress.phase} ${Math.round(coverageProgress.progress * 100)}%`
                  : `Cancel${elapsed > 0 ? ` (${elapsed}s)` : '...'}`}
              </span>
            </Button>
          ) : (
            <Button
              size="sm"
              variant="primary"
              onClick={handleCalculate}
              disabled={sites.length === 0}
            >
              Calculate Coverage
            </Button>
          )}
          <button
            onClick={() => setPanelCollapsed(!panelCollapsed)}
            className="text-slate-400 hover:text-white text-sm sm:hidden min-w-[44px] min-h-[44px] flex items-center justify-center"
          >
            {panelCollapsed ? 'Show' : 'Hide'}
          </button>
        </div>
      </header>

      {/* Shortcuts modal */}
      {showShortcuts && (
        <div
          className="fixed inset-0 z-[9000] bg-black/40 flex items-center justify-center"
          onClick={() => setShowShortcuts(false)}
        >
          <div
            className="bg-white dark:bg-dark-surface rounded-lg shadow-xl p-5 max-w-sm mx-4 border border-gray-200 dark:border-dark-border"
            onClick={(e) => e.stopPropagation()}
          >
            <h3 className="font-semibold text-gray-800 dark:text-dark-text mb-3">
              Keyboard Shortcuts
            </h3>
            <div className="space-y-3 text-sm text-gray-600 dark:text-dark-muted">
              {/* Coverage */}
              <div>
                <h4 className="text-xs font-semibold text-gray-400 dark:text-dark-muted uppercase mb-1">Coverage</h4>
                <ul className="space-y-1">
                  <li className="flex justify-between gap-4">
                    <span>Calculate coverage</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">Ctrl+Enter</kbd>
                  </li>
                  <li className="flex justify-between gap-4">
                    <span>Clear coverage</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">Shift+C</kbd>
                  </li>
                  <li className="flex justify-between gap-4">
                    <span>Toggle heatmap</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">H</kbd>
                  </li>
                </ul>
              </div>
              {/* Sites */}
              <div>
                <h4 className="text-xs font-semibold text-gray-400 dark:text-dark-muted uppercase mb-1">Sites</h4>
                <ul className="space-y-1">
                  <li className="flex justify-between gap-4">
                    <span>New site (place mode)</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">Shift+S</kbd>
                  </li>
                  <li className="flex justify-between gap-4">
                    <span>Delete selected</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">Delete</kbd>
                  </li>
                </ul>
              </div>
              {/* View */}
              <div>
                <h4 className="text-xs font-semibold text-gray-400 dark:text-dark-muted uppercase mb-1">View</h4>
                <ul className="space-y-1">
                  <li className="flex justify-between gap-4">
                    <span>Toggle grid</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">G</kbd>
                  </li>
                  <li className="flex justify-between gap-4">
                    <span>Toggle terrain</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">T</kbd>
                  </li>
                  <li className="flex justify-between gap-4">
                    <span>Toggle ruler</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">R</kbd>
                  </li>
                  <li className="flex justify-between gap-4">
                    <span>Fit to coverage</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">F</kbd>
                  </li>
                </ul>
              </div>
              {/* Edit */}
              <div>
                <h4 className="text-xs font-semibold text-gray-400 dark:text-dark-muted uppercase mb-1">Edit</h4>
                <ul className="space-y-1">
                  <li className="flex justify-between gap-4">
                    <span>Undo</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">Ctrl+Z</kbd>
                  </li>
                  <li className="flex justify-between gap-4">
                    <span>Redo</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">Ctrl+Shift+Z</kbd>
                  </li>
                </ul>
              </div>
              {/* General */}
              <div>
                <h4 className="text-xs font-semibold text-gray-400 dark:text-dark-muted uppercase mb-1">General</h4>
                <ul className="space-y-1">
                  <li className="flex justify-between gap-4">
                    <span>Cancel / Close</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">Esc</kbd>
                  </li>
                  <li className="flex justify-between gap-4">
                    <span>Show shortcuts</span>
                    <kbd className="px-1.5 py-0.5 bg-gray-100 dark:bg-dark-border rounded text-xs font-mono">?</kbd>
                  </li>
                </ul>
              </div>
            </div>
            <button
              onClick={() => setShowShortcuts(false)}
              className="mt-4 w-full py-2 text-sm bg-gray-100 dark:bg-dark-border dark:text-dark-text rounded-md hover:bg-gray-200 dark:hover:bg-dark-muted transition-colors"
            >
              Close
            </button>
          </div>
        </div>
      )}

      {/* Main content */}
      <div className="flex-1 flex overflow-hidden relative">
        {/* Map */}
        <div className="flex-1 relative">
          <MapView
            onSitePlacement={handleSitePlacement}
            onRxPlacement={handleRxPlacement}
            onEditSite={handleEditSite}
            onProfileRequest={(start, end) => setProfileEndpoints({ start, end })}
            showLinkBudget={showLinkBudget}
            onToggleLinkBudget={() => setShowLinkBudget(!showLinkBudget)}
          >
            {/* Show partial results during tiled calculation, or final result */}
            {(coverageResult || (isCalculating && partialPoints.length > 0)) && (
              <>
                {/* Only render ONE layer - WebGL or Canvas, never both */}
                {useWebGLCoverage && (
                  <WebGLCoverageLayer
                    key="webgl-coverage"
                    points={isCalculating && partialPoints.length > 0 ? partialPoints : (coverageResult?.points ?? [])}
                    visible={heatmapVisible}
                    opacity={settings.heatmapOpacity}
                    minRsrp={-130}
                    maxRsrp={-50}
                    onWebGLFailed={() => setUseWebGLCoverage(false)}
                  />
                )}
                {!useWebGLCoverage && (
                  <GeographicHeatmap
                    key="canvas-coverage"
                    points={isCalculating && partialPoints.length > 0 ? partialPoints : (coverageResult?.points ?? [])}
                    visible={heatmapVisible}
                    opacity={settings.heatmapOpacity}
                    radiusMeters={settings.heatmapRadius}
                    rsrpThreshold={settings.rsrpThreshold}
                  />
                )}
                {coverageResult && (
                  <CoverageBoundary
                    points={coverageResult.points.filter(p => p.rsrp >= settings.rsrpThreshold)}
                    visible={showBoundary}
                    resolution={settings.resolution}
                    boundary={coverageResult.boundary}
                  />
                )}
              </>
            )}
            {/* Link Budget TX-RX overlay */}
            {showLinkBudget && linkBudgetRxPoint && (() => {
              const txSite = sites.find(s => s.id === selectedSiteId);
              return (
                <LinkBudgetOverlay
                  txPoint={txSite ? { lat: txSite.lat, lon: txSite.lon } : null}
                  rxPoint={linkBudgetRxPoint}
                  onRxDrag={(lat, lon) => setLinkBudgetRxPoint({ lat, lon })}
                />
              );
            })()}
          </MapView>
          {activeTool === 'rx-placement' && (
            <div className="absolute top-4 left-1/2 -translate-x-1/2 z-[2000] bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg text-sm font-medium flex items-center gap-2">
              <span>Click on map to set RX point</span>
              <button
                onClick={() => clearTool()}
                className="text-white/70 hover:text-white ml-2"
              >
                Cancel
              </button>
            </div>
          )}
          <HeatmapLegend />
          <ResultsPanel />
          {profileEndpoints && (
            <TerrainProfile
              start={profileEndpoints.start}
              end={profileEndpoints.end}
              onClose={() => setProfileEndpoints(null)}
            />
          )}
          {showLinkBudget && (
            <div className="absolute top-20 left-4 z-[1500]">
              <LinkBudgetPanel
                rxPoint={linkBudgetRxPoint}
                onRequestMapClick={() => setActiveTool('rx-placement')}
                onClose={() => {
                  setShowLinkBudget(false);
                  clearTool();
                  setLinkBudgetRxPoint(null);
                }}
              />
            </div>
          )}
        </div>

        {/* Side panel */}
        <div
          className={`${
            panelCollapsed ? 'hidden' : 'flex'
          } flex-col bg-gray-50 dark:bg-dark-bg border-l border-gray-200 dark:border-dark-border
            overflow-y-auto absolute sm:relative inset-0 sm:inset-auto z-[1001]`}
          style={{ width: window.innerWidth >= 640 ? panelWidth : undefined }}
        >
          {/* Resize drag handle (desktop only) */}
          <div
            onMouseDown={handleDragStart}
            className="hidden sm:block absolute left-0 top-0 bottom-0 w-1 cursor-col-resize z-10
              hover:bg-blue-400/50 active:bg-blue-500/60 transition-colors"
          />
          {/* Mobile drag handle + close */}
          <div className="sm:hidden flex flex-col items-center pt-2 pb-1">
            <div className="w-12 h-1 bg-gray-300 dark:bg-dark-border rounded-full mb-2" />
            <button
              onClick={() => setPanelCollapsed(true)}
              className="text-gray-500 dark:text-dark-muted hover:text-gray-700 dark:hover:text-dark-text text-sm min-h-[44px]"
            >
              Close Panel
            </button>
          </div>

          <div className="p-3 space-y-3 flex-1 overflow-y-auto">
            {/* Site list */}
            <SiteList onEditSite={handleEditSite} onAddSite={handleAddManual} />

            {/* Quick frequency change */}
            <div className="bg-white dark:bg-dark-surface border border-gray-200 dark:border-dark-border rounded-lg shadow-sm">
              <BatchFrequencyChange />
            </div>

            {/* Coverage settings */}
            <div className="bg-white dark:bg-dark-surface border border-gray-200 dark:border-dark-border rounded-lg shadow-sm p-4 space-y-3">
              <h3 className="text-sm font-semibold text-gray-800 dark:text-dark-text">
                Coverage Settings
              </h3>
              <div className="space-y-3">
                <NumberInput
                  label="Radius"
                  value={settings.radius}
                  onChange={(v) => {
                    const clamped = Math.min(v, 50);
                    useCoverageStore.getState().updateSettings({ radius: clamped });
                  }}
                  min={1}
                  max={50}
                  step={5}
                  unit="km"
                  hint="Calculation area around each site (max 50km)"
                />
                <NumberInput
                  label="Resolution"
                  value={settings.resolution}
                  onChange={(v) =>
                    useCoverageStore.getState().updateSettings({ resolution: v })
                  }
                  min={50}
                  max={500}
                  step={50}
                  unit="m"
                  hint="Grid spacing — lower = more accurate but slower"
                />
                <NumberInput
                  label="Min Signal"
                  value={settings.rsrpThreshold}
                  onChange={(v) =>
                    useCoverageStore.getState().updateSettings({ rsrpThreshold: v })
                  }
                  min={-140}
                  max={-50}
                  step={5}
                  unit="dBm"
                  hint="RSRP threshold — points below this are hidden"
                />
                <NumberInput
                  label="Heatmap Opacity"
                  value={Math.round(settings.heatmapOpacity * 100)}
                  onChange={(v) =>
                    useCoverageStore.getState().updateSettings({ heatmapOpacity: v / 100 })
                  }
                  min={30}
                  max={100}
                  step={5}
                  unit="%"
                  hint="Transparency of the RF coverage overlay"
                />
                <div className="flex items-center justify-between">
                  <div>
                    <label className="text-sm font-medium text-gray-700 dark:text-dark-text">
                      Smooth Rendering
                    </label>
                    <p className="text-xs text-gray-400 dark:text-dark-muted">
                      WebGL interpolation for smooth gradients
                    </p>
                  </div>
                  <button
                    onClick={() => setUseWebGLCoverage(!useWebGLCoverage)}
                    className={`relative w-11 h-6 rounded-full transition-colors ${
                      useWebGLCoverage ? 'bg-blue-500' : 'bg-gray-300 dark:bg-dark-border'
                    }`}
                  >
                    <span
                      className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${
                        useWebGLCoverage ? 'translate-x-5' : ''
                      }`}
                    />
                  </button>
                </div>
                {!useWebGLCoverage && (
                <div>
                  <label className="text-sm font-medium text-gray-700 dark:text-dark-text">
                    Heatmap Quality
                  </label>
                  <select
                    value={settings.heatmapRadius}
                    onChange={(e) =>
                      useCoverageStore
                        .getState()
                        .updateSettings({
                          heatmapRadius: Number(e.target.value),
                        })
                    }
                    className="w-full mt-1 px-2 py-1.5 text-sm bg-white dark:bg-dark-border border border-gray-300 dark:border-dark-border rounded-md text-gray-700 dark:text-dark-text"
                  >
                    <option value={200}>200m — Fast</option>
                    <option value={400}>400m — Balanced</option>
                    <option value={600}>600m — Smooth</option>
                    <option value={800}>800m — Wide</option>
                  </select>
                  <p className="mt-1 text-xs text-gray-400 dark:text-dark-muted">
                    Pixel radius per point — larger = smoother, smaller = sharper
                  </p>
                  {settings.heatmapRadius >= 600 && settings.resolution > 200 && (
                    <p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
                      Wide radius works best with fine resolution (200m or less). Current: {settings.resolution}m
                    </p>
                  )}
                </div>
                )}
                {/* Propagation Model Preset */}
                <div>
                  <label className="text-sm font-medium text-gray-700 dark:text-dark-text">
                    Propagation Model
                  </label>
                  <select
                    value={settings.preset || 'standard'}
                    onChange={(e) => {
                      const key = e.target.value;
                      const preset = presets[key];
                      if (preset) {
                        useCoverageStore.getState().updateSettings({
                          preset: key as 'fast' | 'standard' | 'detailed' | 'full',
                          use_terrain: preset.use_terrain,
                          use_buildings: preset.use_buildings,
                          use_materials: preset.use_materials,
                          use_dominant_path: preset.use_dominant_path,
                          use_street_canyon: preset.use_street_canyon,
                          use_reflections: preset.use_reflections,
                          use_water_reflection: preset.use_water_reflection,
                          use_vegetation: preset.use_vegetation,
                        });
                      }
                    }}
                    disabled={isCalculating}
                    className="w-full mt-1 px-2 py-1.5 text-sm bg-white dark:bg-dark-border border border-gray-300 dark:border-dark-border rounded-md text-gray-700 dark:text-dark-text disabled:opacity-50"
                  >
                    {Object.keys(presets).length > 0 ? (
                      Object.entries(presets).map(([key, preset]) => (
                        <option key={key} value={key}>
                          {key.charAt(0).toUpperCase() + key.slice(1)} — {preset.estimated_speed}
                        </option>
                      ))
                    ) : (
                      <>
                        <option value="fast">Fast</option>
                        <option value="standard">Standard</option>
                        <option value="detailed">Detailed</option>
                        <option value="full">Full</option>
                      </>
                    )}
                  </select>
                  {presets[settings.preset || 'standard'] && (
                    <p className="mt-1 text-xs text-gray-400 dark:text-dark-muted">
                      {presets[settings.preset || 'standard'].description}
                    </p>
                  )}
                </div>

                {/* Advanced Propagation Toggles */}
                <div>
                  <button
                    type="button"
                    onClick={() => setShowAdvanced(!showAdvanced)}
                    className="flex items-center gap-1 text-xs font-medium text-gray-500 dark:text-dark-muted hover:text-gray-700 dark:hover:text-dark-text transition-colors"
                  >
                    <span className="text-[10px]">{showAdvanced ? '▼' : '▶'}</span>
                    Advanced Propagation
                  </button>
                  {showAdvanced && (
                    <div className="mt-2 space-y-1.5 pl-1">
                      {[
                        { key: 'use_terrain' as const, label: 'Terrain (SRTM)', disabled: false },
                        { key: 'use_buildings' as const, label: 'Buildings (OSM)', disabled: false },
                        { key: 'use_materials' as const, label: 'Building Materials', disabled: !settings.use_buildings },
                        { key: 'use_dominant_path' as const, label: 'Dominant Path', disabled: false },
                        { key: 'use_street_canyon' as const, label: 'Street Canyon', disabled: false },
                        { key: 'use_reflections' as const, label: 'Reflections', disabled: false },
                        { key: 'use_water_reflection' as const, label: 'Water Reflection', disabled: false },
                        { key: 'use_vegetation' as const, label: 'Vegetation Loss', disabled: false },
                      ].map(({ key, label, disabled }) => (
                        <label
                          key={key}
                          className={`flex items-center gap-2 cursor-pointer text-sm ${
                            disabled || isCalculating
                              ? 'text-gray-400 dark:text-dark-muted cursor-not-allowed'
                              : 'text-gray-700 dark:text-dark-text'
                          }`}
                        >
                          <input
                            type="checkbox"
                            checked={settings[key] ?? false}
                            onChange={(e) => {
                              useCoverageStore.getState().updateSettings({
                                [key]: e.target.checked,
                                preset: undefined,  // Clear preset when manually toggling
                              });
                            }}
                            disabled={disabled || isCalculating}
                            className="w-3.5 h-3.5 rounded border-gray-300 dark:border-dark-border accent-blue-600"
                          />
                          {label}
                        </label>
                      ))}
                      {/* Season selector (only relevant when vegetation is enabled) */}
                      {settings.use_vegetation && (
                        <div className="mt-1.5 pl-5">
                          <label className="text-xs text-gray-500 dark:text-dark-muted">Season</label>
                          <select
                            value={settings.season || 'summer'}
                            onChange={(e) =>
                              useCoverageStore.getState().updateSettings({
                                season: e.target.value as 'summer' | 'winter' | 'spring' | 'autumn',
                              })
                            }
                            disabled={isCalculating}
                            className="w-full mt-0.5 px-2 py-1 text-xs bg-white dark:bg-dark-border border border-gray-300 dark:border-dark-border rounded text-gray-700 dark:text-dark-text disabled:opacity-50"
                          >
                            <option value="summer">Summer (full foliage)</option>
                            <option value="autumn">Autumn (70%)</option>
                            <option value="spring">Spring (60%)</option>
                            <option value="winter">Winter (30%)</option>
                          </select>
                        </div>
                      )}

                      {/* Atmospheric absorption toggle */}
                      <label
                        className={`flex items-center gap-2 cursor-pointer text-sm ${
                          isCalculating
                            ? 'text-gray-400 dark:text-dark-muted cursor-not-allowed'
                            : 'text-gray-700 dark:text-dark-text'
                        }`}
                      >
                        <input
                          type="checkbox"
                          checked={settings.use_atmospheric ?? false}
                          onChange={(e) => {
                            useCoverageStore.getState().updateSettings({
                              use_atmospheric: e.target.checked,
                              preset: undefined,
                            });
                          }}
                          disabled={isCalculating}
                          className="w-3.5 h-3.5 rounded border-gray-300 dark:border-dark-border accent-blue-600"
                        />
                        Atmospheric Absorption
                      </label>
                      {settings.use_atmospheric && (
                        <div className="mt-1.5 pl-5 space-y-1.5">
                          <div>
                            <label className="text-xs text-gray-500 dark:text-dark-muted">Temperature</label>
                            <select
                              value={settings.temperature_c ?? 15}
                              onChange={(e) =>
                                useCoverageStore.getState().updateSettings({
                                  temperature_c: Number(e.target.value),
                                })
                              }
                              disabled={isCalculating}
                              className="w-full mt-0.5 px-2 py-1 text-xs bg-white dark:bg-dark-border border border-gray-300 dark:border-dark-border rounded text-gray-700 dark:text-dark-text disabled:opacity-50"
                            >
                              <option value={-10}>-10°C (cold)</option>
                              <option value={0}>0°C (freezing)</option>
                              <option value={15}>15°C (mild)</option>
                              <option value={25}>25°C (warm)</option>
                              <option value={35}>35°C (hot)</option>
                            </select>
                          </div>
                          <div>
                            <label className="text-xs text-gray-500 dark:text-dark-muted">Humidity</label>
                            <select
                              value={settings.humidity_percent ?? 50}
                              onChange={(e) =>
                                useCoverageStore.getState().updateSettings({
                                  humidity_percent: Number(e.target.value),
                                })
                              }
                              disabled={isCalculating}
                              className="w-full mt-0.5 px-2 py-1 text-xs bg-white dark:bg-dark-border border border-gray-300 dark:border-dark-border rounded text-gray-700 dark:text-dark-text disabled:opacity-50"
                            >
                              <option value={20}>20% (dry)</option>
                              <option value={50}>50% (normal)</option>
                              <option value={70}>70% (humid)</option>
                              <option value={90}>90% (very humid)</option>
                            </select>
                          </div>
                        </div>
                      )}

                      {/* Weather / Rain section */}
                      <div className="mt-2 pt-2 border-t border-gray-200 dark:border-dark-border">
                        <p className="text-[10px] font-semibold text-gray-400 dark:text-dark-muted uppercase mb-1.5">Environment</p>
                        <div>
                          <label className="text-xs text-gray-500 dark:text-dark-muted">Rain Conditions</label>
                          <select
                            value={settings.rain_rate ?? 0}
                            onChange={(e) =>
                              useCoverageStore.getState().updateSettings({
                                rain_rate: Number(e.target.value),
                                preset: undefined,
                              })
                            }
                            disabled={isCalculating}
                            className="w-full mt-0.5 px-2 py-1 text-xs bg-white dark:bg-dark-border border border-gray-300 dark:border-dark-border rounded text-gray-700 dark:text-dark-text disabled:opacity-50"
                          >
                            <option value={0}>No Rain</option>
                            <option value={2.5}>Drizzle</option>
                            <option value={5}>Light Rain</option>
                            <option value={12.5}>Moderate Rain</option>
                            <option value={25}>Heavy Rain</option>
                            <option value={50}>Very Heavy Rain</option>
                          </select>
                        </div>
                      </div>

                      {/* Indoor penetration section */}
                      <div className="mt-1.5">
                        <label className="text-xs text-gray-500 dark:text-dark-muted">Indoor Coverage</label>
                        <select
                          value={settings.indoor_loss_type ?? 'none'}
                          onChange={(e) =>
                            useCoverageStore.getState().updateSettings({
                              indoor_loss_type: e.target.value,
                              preset: undefined,
                            })
                          }
                          disabled={isCalculating}
                          className="w-full mt-0.5 px-2 py-1 text-xs bg-white dark:bg-dark-border border border-gray-300 dark:border-dark-border rounded text-gray-700 dark:text-dark-text disabled:opacity-50"
                        >
                          <option value="none">Outdoor Only</option>
                          <option value="light">Light Building (wood, glass)</option>
                          <option value="medium">Medium Building (brick)</option>
                          <option value="heavy">Heavy Building (concrete)</option>
                          <option value="vehicle">Inside Vehicle</option>
                        </select>
                      </div>

                      {/* Fading margin */}
                      <div className="mt-2 pt-2 border-t border-gray-200 dark:border-dark-border">
                        <NumberInput
                          label="Fading Margin"
                          value={settings.fading_margin ?? 0}
                          onChange={(v) => useCoverageStore.getState().updateSettings({ fading_margin: v })}
                          min={0}
                          max={20}
                          step={1}
                          unit="dB"
                          hint="Safety margin subtracted from signal"
                        />
                      </div>
                    </div>
                  )}
                </div>

                <NumberInput
                  label="Terrain Opacity"
                  value={Math.round(terrainOpacity * 100)}
                  onChange={(v) => setTerrainOpacity(v / 100)}
                  min={10}
                  max={100}
                  step={5}
                  unit="%"
                  hint={!showTerrain ? 'Enable terrain overlay first' : undefined}
                />
              </div>
            </div>

            {/* Map Tools */}
            <div className="bg-white dark:bg-dark-surface border border-gray-200 dark:border-dark-border rounded-lg shadow-sm p-4 space-y-3">
              <h3 className="text-sm font-semibold text-gray-800 dark:text-dark-text">
                Map Tools
              </h3>
              <div className="space-y-2">
                <label className="flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-dark-text">
                  <input
                    type="checkbox"
                    checked={showGrid}
                    onChange={(e) => setShowGrid(e.target.checked)}
                    className="w-4 h-4 rounded border-gray-300 dark:border-dark-border accent-green-600"
                  />
                  Coordinate Grid
                </label>
                <label className="flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-dark-text">
                  <input
                    type="checkbox"
                    checked={activeTool === 'ruler'}
                    onChange={(e) => e.target.checked ? setActiveTool('ruler') : clearTool()}
                    className="w-4 h-4 rounded border-gray-300 dark:border-dark-border accent-orange-600"
                  />
                  Distance Measurement
                </label>
                {activeTool === 'ruler' && (
                  <p className="text-xs text-gray-400 dark:text-dark-muted pl-6">
                    Click start and end points. Esc to cancel.
                  </p>
                )}
                <label className="flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-dark-text">
                  <input
                    type="checkbox"
                    checked={showElevationInfo}
                    onChange={(e) => setShowElevationInfo(e.target.checked)}
                    className="w-4 h-4 rounded border-gray-300 dark:border-dark-border accent-amber-600"
                  />
                  Cursor Elevation
                </label>
                <label className="flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-dark-text">
                  <input
                    type="checkbox"
                    checked={showElevationOverlay}
                    onChange={(e) => setShowElevationOverlay(e.target.checked)}
                    className="w-4 h-4 rounded border-gray-300 dark:border-dark-border accent-amber-600"
                  />
                  Elevation Colors
                </label>
                {showElevationOverlay && (
                  <div className="pl-6">
                    <NumberInput
                      label="Opacity"
                      value={Math.round(elevationOpacity * 100)}
                      onChange={(v) => setElevationOpacity(v / 100)}
                      min={10}
                      max={100}
                      step={10}
                      unit="%"
                    />
                  </div>
                )}
                </div>
            </div>

            {/* Data Cache Status */}
            <div className="bg-white dark:bg-dark-surface border border-gray-200 dark:border-dark-border rounded-lg shadow-sm p-4 space-y-2">
              <h3 className="text-sm font-semibold text-gray-800 dark:text-dark-text">
                Data Cache
              </h3>
              {cachedRegions.length > 0 ? (
                <>
                  <div className="space-y-1">
                    {cachedRegions.filter((r) => r.downloaded || r.download_progress > 0).length > 0 ? (
                      cachedRegions
                        .filter((r) => r.downloaded || r.download_progress > 0)
                        .map((r) => (
                          <div key={r.id} className="flex items-center gap-2 text-xs">
                            <span
                              className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
                                r.downloaded ? 'bg-emerald-500' : 'bg-amber-500'
                              }`}
                            />
                            <span className="text-gray-700 dark:text-dark-text truncate">{r.name}</span>
                            {!r.downloaded && (
                              <span className="text-gray-400 dark:text-dark-muted ml-auto">
                                {Math.round(r.download_progress)}%
                              </span>
                            )}
                          </div>
                        ))
                    ) : (
                      <p className="text-xs text-gray-400 dark:text-dark-muted">No regions cached</p>
                    )}
                  </div>
                  {cacheStats && (
                    <p className="text-[11px] text-gray-400 dark:text-dark-muted">
                      {cacheStats.terrain_tiles} terrain tiles ({cacheStats.terrain_mb} MB)
                    </p>
                  )}
                </>
              ) : (
                <p className="text-xs text-gray-400 dark:text-dark-muted">Loading...</p>
              )}
              <div className="flex gap-2 pt-1">
                <button
                  onClick={() => setShowWizard(true)}
                  className="text-xs px-2 py-1 bg-slate-100 dark:bg-dark-border text-gray-600 dark:text-dark-text rounded hover:bg-slate-200 dark:hover:bg-dark-muted transition-colors"
                >
                  Download Regions
                </button>
                {isDesktop() && (
                  <button
                    onClick={async () => {
                      const desktop = getDesktopApi();
                      if (!desktop) return;
                      const result = await desktop.importRegionData();
                      if (result.success) {
                        addToast(result.message, 'success');
                        refreshCacheStatus();
                      } else {
                        if (result.message !== 'Cancelled') {
                          addToast(result.message, 'error');
                        }
                      }
                    }}
                    className="text-xs px-2 py-1 bg-slate-100 dark:bg-dark-border text-gray-600 dark:text-dark-text rounded hover:bg-slate-200 dark:hover:bg-dark-muted transition-colors"
                  >
                    Import Data
                  </button>
                )}
              </div>
            </div>

            {/* Coverage error */}
            {coverageError && (
              <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
                <p className="text-xs text-red-600 dark:text-red-400">
                  {coverageError}
                </p>
              </div>
            )}

            {/* Coverage statistics */}
            <CoverageStats
              points={coverageResult?.points ?? []}
              resolution={settings.resolution}
              stats={coverageResult?.stats}
              calculationTime={coverageResult?.calculationTime}
              modelsUsed={coverageResult?.modelsUsed}
            />

            {/* Session history */}
            <HistoryPanel />

            {/* Export coverage data */}
            <ExportPanel />

            {/* Site import/export */}
            <SiteImportExport />

            {/* Projects save/load */}
            <ProjectPanel />
          </div>
        </div>
      </div>

      {/* Site configuration modal */}
      <SiteConfigModal
        isOpen={modalState.isOpen}
        mode={modalState.mode}
        initialData={modalState.initialData}
        onClose={handleCloseModal}
        onSave={handleModalSave}
        onSaveAndCalculate={handleModalSaveAndCalculate}
        onDelete={modalState.mode === 'edit' ? handleModalDelete : undefined}
      />

      {/* Keyboard delete confirmation dialog */}
      {kbDeleteTarget && (
        <ConfirmDialog
          title="Delete Site?"
          message={`Are you sure you want to delete "${kbDeleteTarget.name}"? This action can be undone for 10 seconds.`}
          confirmLabel="Delete"
          cancelLabel="Cancel"
          danger
          onConfirm={handleKbDeleteConfirmed}
          onCancel={() => setKbDeleteTarget(null)}
        />
      )}

      <ToastContainer />

      {/* First-run region download wizard (desktop only) */}
      {showWizard && (
        <RegionWizard onComplete={() => { setShowWizard(false); refreshCacheStatus(); }} />
      )}
    </div>
  );
}
