import * as d3 from 'd3';
import * as topojson from 'topojson-client';
import { escapeHtml } from '@/utils/sanitize';
import { getCSSColor } from '@/utils';
import type { Topology, GeometryCollection } from 'topojson-specification';
import type { Feature, Geometry } from 'geojson';
import type { MapLayers, Hotspot, NewsItem, InternetOutage, RelatedAsset, AssetType, AisDisruptionEvent, AisDensityZone, CableAdvisory, RepairShip, SocialUnrestEvent, MilitaryFlight, MilitaryVessel, MilitaryFlightCluster, MilitaryVesselCluster, NaturalEvent, CyberThreat, CableHealthRecord, MilitaryBase } from '@/types';
import type { AirportDelayAlert, PositionSample } from '@/services/aviation';
import type { Earthquake } from '@/services/earthquakes';
import { type IranEvent, getIranEventCssColor, getIranEventSize } from '@/services/conflict';
import type { TechHubActivity } from '@/services/tech-activity';
import type { GeoHubActivity } from '@/services/geo-activity';
import { getNaturalEventIcon } from '@/services/eonet';
import type { WeatherAlert } from '@/services/weather';
import type { RadiationObservation } from '@/services/radiation';
import { getSeverityColor } from '@/services/weather';
import { startSmartPollLoop, type SmartPollLoopHandle } from '@/services/smart-poll-loop';
import { scheduleAfterFirstPaint, yieldToMain } from '@/utils/after-paint';
import { measure, mutate } from '@/utils/layout-batch';
import { getCachedMilitaryBases, preloadMilitaryBases } from '@/services/military-base-config';
import {
  INTEL_HOTSPOTS,
  CONFLICT_ZONES,
  GAMMA_IRRADIATORS,
  PIPELINES,
  PIPELINE_COLORS,
  STRATEGIC_WATERWAYS,
  PORTS,
  SITE_VARIANT,
  // Finance variant data
  STOCK_EXCHANGES,
  FINANCIAL_CENTERS,
  CENTRAL_BANKS,
  COMMODITY_HUBS,
} from '@/config';
// Tech-geo + ai-datacenters + geo-map tables imported directly so their chunks stay
// off the eager @/config barrel and load only with this lazy renderer (#4404).
import { STARTUP_HUBS, ACCELERATORS, TECH_HQS, CLOUD_REGIONS } from '@/config/tech-geo';
import { AI_DATA_CENTERS } from '@/config/ai-datacenters';
import { worldTopologyUrl, UNDERSEA_CABLES, NUCLEAR_FACILITIES, SANCTIONED_COUNTRIES, ECONOMIC_CENTERS, SPACEPORTS, CRITICAL_MINERALS } from '@/config/geo-map';
import { pinWebcam, isPinned } from '@/services/webcams/pinned-store';
import type { WebcamEntry, WebcamCluster } from '@/generated/client/worldmonitor/webcam/v1/service_client';
import { tokenizeForMatch, matchKeyword, findMatchingKeywords } from '@/utils/keyword-match';
import { MapPopup } from './MapPopup';
import type { GetChokepointStatusResponse } from '@/services/supply-chain';
import type { AcledConflictEvent } from '@/generated/client/worldmonitor/conflict/v1/service_client';
import {
  updateHotspotEscalation,
  getHotspotEscalation,
  setMilitaryData,
  setCIIGetter,
  setGeoAlertGetter,
} from '@/services/hotspot-escalation';
import { getCountryScore } from '@/services/country-instability';
import { getCachedCountryScoreValue } from '@/services/cached-risk-scores';
import { getAlertsNearLocation } from '@/services/geo-convergence';
import { getCountryAtCoordinates, getCountryBbox } from '@/services/country-geometry';
import type { CountryClickPayload } from './DeckGLMap';
import { t } from '@/services/i18n';
import type { ScenarioVisualState } from '@/config/scenario-templates';
import { setTrustedHtml, trustedHtml } from '@/utils/dom-utils';
import {
  getLayerExplanation,
  getLayersForVariant,
  hasCuratedLayerExplanation,
  resolveLayerLabel,
  type MapVariant,
} from '@/config/map-layer-definitions';
import { renderLayerExplanationCard } from '@/utils/layer-explanation-card';
import {
  createCountryClickGestureTracker,
  finishCountryClickGesture,
  shouldSuppressCountryClick,
  startCountryClickGesture,
  updateCountryClickGestureDrag,
} from './map-interaction-guard';


export type TimeRange = '1h' | '6h' | '24h' | '48h' | '7d' | 'all';
export type MapView = 'global' | 'america' | 'mena' | 'eu' | 'asia' | 'latam' | 'africa' | 'oceania';

export interface MapState {
  zoom: number;
  pan: { x: number; y: number };
  view: MapView;
  layers: MapLayers;
  timeRange: TimeRange;
}

export interface MapComponentOptions {
  chrome?: boolean;
  isMobile?: boolean;
}

interface HotspotWithBreaking extends Hotspot {
  hasBreaking?: boolean;
}

interface TechEventMarker {
  id: string;
  title: string;
  location: string;
  lat: number;
  lng: number;
  country: string;
  startDate: string;
  endDate: string;
  url: string | null;
  daysUntil: number;
}

interface WorldTopology extends Topology {
  objects: {
    countries: GeometryCollection;
  };
}

export class MapComponent {
  private static readonly MOBILE_MIN_EARTHQUAKE_MAGNITUDE = 5;
  private static readonly MOBILE_MAX_IRAN_EVENTS = 50;
  private static readonly LAYER_ZOOM_THRESHOLDS: Partial<
    Record<keyof MapLayers, { minZoom: number; showLabels?: number }>
  > = {
      bases: { minZoom: 3, showLabels: 5 },
      nuclear: { minZoom: 2 },
      conflicts: { minZoom: 1, showLabels: 3 },
      economic: { minZoom: 2 },
      natural: { minZoom: 1, showLabels: 2 },
    };

  private container: HTMLElement;
  private svg: d3.Selection<SVGSVGElement, unknown, null, undefined>;
  private wrapper: HTMLElement;
  private overlays: HTMLElement;
  private clusterCanvas: HTMLCanvasElement;
  private clusterGl: WebGLRenderingContext | null = null;
  private state: MapState;
  private layerExplanationOutsideClickHandler: ((event: MouseEvent) => void) | null = null;
  private worldData: WorldTopology | null = null;
  private countryFeatures: Feature<Geometry>[] | null = null;
  private isResizing = false;
  private baseLayerGroup: d3.Selection<SVGGElement, unknown, null, undefined> | null = null;
  private dynamicLayerGroup: d3.Selection<SVGGElement, unknown, null, undefined> | null = null;
  private baseRendered = false;
  private baseWidth = 0;
  private baseHeight = 0;
  private hotspots: HotspotWithBreaking[];
  private earthquakes: Earthquake[] = [];
  private weatherAlerts: WeatherAlert[] = [];
  private radiationObservations: RadiationObservation[] = [];
  private outages: InternetOutage[] = [];
  private aisDisruptions: AisDisruptionEvent[] = [];
  private aisDensity: AisDensityZone[] = [];
  private cableAdvisories: CableAdvisory[] = [];
  private repairShips: RepairShip[] = [];
  private healthByCableId: Record<string, CableHealthRecord> = {};
  private conflictEvents: AcledConflictEvent[] = [];
  private protests: SocialUnrestEvent[] = [];
  private flightDelays: AirportDelayAlert[] = [];
  private aircraftPositions: PositionSample[] = [];
  private militaryFlights: MilitaryFlight[] = [];
  private militaryFlightClusters: MilitaryFlightCluster[] = [];
  private militaryVessels: MilitaryVessel[] = [];
  private militaryVesselClusters: MilitaryVesselCluster[] = [];
  private naturalEvents: NaturalEvent[] = [];
  private firmsFireData: Array<{ lat: number; lon: number; brightness: number; frp: number; confidence: number; region: string; acq_date: string; daynight: string }> = [];
  private techEvents: TechEventMarker[] = [];
  private techActivities: TechHubActivity[] = [];
  private geoActivities: GeoHubActivity[] = [];
  private iranEvents: IranEvent[] = [];
  private aptGroups: import('@/types').APTGroup[] = [];
  private aptGroupsLoaded = false;
  private webcamData: Array<WebcamEntry | WebcamCluster> = [];
  private news: NewsItem[] = [];
  private onTechHubClick?: (hub: TechHubActivity) => void;
  private onGeoHubClick?: (hub: GeoHubActivity) => void;
  private popup: MapPopup;
  private onHotspotClick?: (hotspot: Hotspot) => void;
  private onTimeRangeChange?: (range: TimeRange) => void;
  private onLayerChange?: (layer: keyof MapLayers, enabled: boolean, source: 'user' | 'programmatic') => void;
  private layerZoomOverrides: Partial<Record<keyof MapLayers, boolean>> = {};
  private onStateChange?: (state: MapState) => void;
  private onCountryClick?: (country: CountryClickPayload) => void;
  private highlightedAssets: Record<AssetType, Set<string>> = {
    pipeline: new Set(),
    cable: new Set(),
    datacenter: new Set(),
    base: new Set(),
    nuclear: new Set(),
  };
  private boundVisibilityHandler!: () => void;
  private handleThemeChange: () => void;
  private resizeObserver: ResizeObserver | null = null;
  private renderScheduled = false;
  private lastRenderTime = 0;
  private readonly MIN_RENDER_INTERVAL_MS = 100;
  private healthCheckLoop: SmartPollLoopHandle | null = null;
  // First render paints the base map (countries) synchronously for LCP, then defers the
  // heavy dynamic-overlay pass off the first-paint critical path (#4429). Mobile uses this
  // SVG renderer and its synchronous overlay build was the #1 boot-scripting cost (~1.3s).
  private initialDynamicRendered = false;
  private initialDynamicScheduled = false;
  // Bumped on every dynamic-layer build; lets the chunked first-paint pass bail mid-yield
  // when a newer (synchronous) render has superseded it (#4442 re-entrancy guard).
  private dynamicRenderToken = 0;
  private militaryBasesLoadPending = false;
  // Set in destroy(); guards render() (incl. the deferred first-paint callback and the
  // resize/visibility rAF callbacks) from running on a torn-down instance.
  private destroyed = false;
  // Mobile loads the lighter 110m country topology (U6); passed in from MapContainer.
  private readonly isMobile: boolean;
  private overlayAppendTarget: ParentNode | null = null;
  private labelVisibilityScheduled = false;
  private pendingLabelVisibilityZoom = 1;
  private lastContainerSize = { width: 0, height: 0 };
  // Desktop measures label overlap from the start; mobile defers until the first
  // interaction. The effective value is set in the constructor (= !this.isMobile);
  // false here documents the mobile-off default.
  private mobileLabelVisibilityArmed = false;
  // All container/document interaction listeners are registered with this signal so
  // destroy() can remove them in one shot. The container node is reused across
  // renderer switches (MapContainer keeps one element and rebuilds MapComponent on it),
  // so listeners left attached would retain every destroyed instance forever.
  private readonly listenerAbort = new AbortController();

  constructor(container: HTMLElement, initialState: MapState, options: MapComponentOptions = {}) {
    this.container = container;
    this.state = initialState;
    this.hotspots = [...INTEL_HOTSPOTS];
    const chrome = options.chrome ?? true;
    this.isMobile = options.isMobile ?? false;
    this.mobileLabelVisibilityArmed = !this.isMobile;

    this.wrapper = document.createElement('div');
    this.wrapper.className = 'map-wrapper';
    this.wrapper.id = 'mapWrapper';

    const svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svgElement.classList.add('map-svg');
    svgElement.id = 'mapSvg';
    this.wrapper.appendChild(svgElement);

    this.clusterCanvas = document.createElement('canvas');
    this.clusterCanvas.className = 'map-cluster-canvas';
    this.clusterCanvas.id = 'mapClusterCanvas';
    this.wrapper.appendChild(this.clusterCanvas);

    // Overlays inside wrapper so they transform together on zoom/pan
    this.overlays = document.createElement('div');
    this.overlays.id = 'mapOverlays';
    this.wrapper.appendChild(this.overlays);

    container.appendChild(this.wrapper);
    if (chrome) {
      container.appendChild(this.createControls());
      container.appendChild(this.createTimeSlider());
      container.appendChild(this.createLayerToggles());
      container.appendChild(this.createLegend());
      this.healthCheckLoop = startSmartPollLoop(() => { this.runHealthCheck(); }, {
        intervalMs: 30_000,
        pauseWhenHidden: true,
        refreshOnVisible: false,
        runImmediately: false,
        jitterFraction: 0,
      });
    }

    this.svg = d3.select(svgElement);
    this.baseLayerGroup = this.svg.append('g').attr('class', 'map-base');
    this.dynamicLayerGroup = this.svg.append('g').attr('class', 'map-dynamic');
    this.popup = new MapPopup(container);
    this.initClusterRenderer();

    this.setupZoomHandlers();
    this.loadMapData();
    this.setupResizeObserver();

    this.handleThemeChange = () => {
      this.baseRendered = false;
      this.render();
    };
    window.addEventListener('theme-changed', this.handleThemeChange);

    // Kick off lazy APT load if cyberThreats is already on at init (e.g. from URL/localStorage)
    if (this.state.layers.cyberThreats && SITE_VARIANT !== 'tech' && SITE_VARIANT !== 'happy') {
      this.loadAptGroups();
    }
  }

  private getMilitaryBasesForRender(): MilitaryBase[] {
    const bases = getCachedMilitaryBases();
    if (bases.length === 0) this.requestMilitaryBasesRender();
    return bases;
  }

  private requestMilitaryBasesRender(): void {
    if (this.militaryBasesLoadPending) return;
    this.militaryBasesLoadPending = true;
    void preloadMilitaryBases()
      .then(() => {
        this.militaryBasesLoadPending = false;
        if (!this.destroyed) this.render();
      })
      .catch((error) => {
        this.militaryBasesLoadPending = false;
        console.warn('[Map] Military base config unavailable:', error);
      });
  }

  private setupResizeObserver(): void {
    let lastWidth = 0;
    let lastHeight = 0;
    this.resizeObserver = new ResizeObserver((entries) => {
      if (this.isResizing) return;
      for (const entry of entries) {
        const { width, height } = entry.contentRect;
        if (width > 0 && height > 0 && (width !== lastWidth || height !== lastHeight)) {
          lastWidth = width;
          lastHeight = height;
          this.rememberContainerSize({ width, height });
          this.scheduleRender();
        }
      }
    });
    this.resizeObserver.observe(this.container);

    // Re-render when page becomes visible again (after browser throttling)
    this.boundVisibilityHandler = () => {
      if (!document.hidden) {
        this.scheduleRender();
      }
    };
    document.addEventListener('visibilitychange', this.boundVisibilityHandler);
  }

  public setIsResizing(value: boolean): void {
    const wasResizing = this.isResizing;
    this.isResizing = value;
    if (wasResizing && !value) {
      this.scheduleRender();
    }
  }

  public resize(): void {
    this.scheduleRender();
  }

  public destroy(): void {
    this.destroyed = true;
    this.listenerAbort.abort();
    window.removeEventListener('theme-changed', this.handleThemeChange);
    document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
      this.resizeObserver = null;
    }
    if (this.healthCheckLoop) {
      this.healthCheckLoop.stop();
      this.healthCheckLoop = null;
    }
  }

  private createControls(): HTMLElement {
    const controls = document.createElement('div');
    controls.className = 'map-controls';
    setTrustedHtml(controls, trustedHtml(`
      <button class="map-control-btn" data-action="zoom-in" aria-label="Zoom in">+</button>
      <button class="map-control-btn" data-action="zoom-out" aria-label="Zoom out">−</button>
      <button class="map-control-btn" data-action="reset" aria-label="Reset rotation">⟲</button>
    `, "legacy direct innerHTML migration"));

    controls.addEventListener('click', (e) => {
      const target = e.target as HTMLElement;
      const action = target.dataset.action;
      if (action === 'zoom-in') this.zoomIn();
      else if (action === 'zoom-out') this.zoomOut();
      else if (action === 'reset') this.reset();
    });

    return controls;
  }

  private createTimeSlider(): HTMLElement {
    const slider = document.createElement('div');
    slider.className = 'time-slider';
    slider.id = 'timeSlider';

    const ranges: { value: TimeRange; label: string }[] = [
      { value: '1h', label: '1H' },
      { value: '6h', label: '6H' },
      { value: '24h', label: '24H' },
      { value: '48h', label: '48H' },
      { value: '7d', label: '7D' },
      { value: 'all', label: 'ALL' },
    ];

    setTrustedHtml(slider, trustedHtml(`
      <span class="time-slider-label">TIME RANGE</span>
      <div class="time-slider-buttons">
        ${ranges
        .map(
          (r) =>
            `<button class="time-btn ${this.state.timeRange === r.value ? 'active' : ''}" data-range="${r.value}">${r.label}</button>`
        )
        .join('')}
      </div>
    `, "legacy direct innerHTML migration"));

    slider.addEventListener('click', (e) => {
      const target = e.target as HTMLElement;
      if (target.classList.contains('time-btn')) {
        const range = target.dataset.range as TimeRange;
        this.setTimeRange(range);
        slider.querySelectorAll('.time-btn').forEach((btn) => btn.classList.remove('active'));
        target.classList.add('active');
      }
    });

    return slider;
  }

  private updateTimeSliderButtons(): void {
    const slider = this.container.querySelector('#timeSlider');
    if (!slider) return;
    slider.querySelectorAll('.time-btn').forEach((btn) => {
      const range = (btn as HTMLElement).dataset.range as TimeRange | undefined;
      btn.classList.toggle('active', range === this.state.timeRange);
    });
  }

  public setTimeRange(range: TimeRange): void {
    this.state.timeRange = range;
    this.onTimeRangeChange?.(range);
    this.updateTimeSliderButtons();
    this.render();
  }

  private getTimeRangeMs(): number {
    const ranges: Record<TimeRange, number> = {
      '1h': 60 * 60 * 1000,
      '6h': 6 * 60 * 60 * 1000,
      '24h': 24 * 60 * 60 * 1000,
      '48h': 48 * 60 * 60 * 1000,
      '7d': 7 * 24 * 60 * 60 * 1000,
      'all': Infinity,
    };
    return ranges[this.state.timeRange];
  }



  private getLayerControlLabel(layer: keyof MapLayers): string {
    if (layer === 'sanctions') return t('components.deckgl.layerHelp.labels.sanctions');

    const def = getLayersForVariant((SITE_VARIANT || 'full') as MapVariant, 'flat').find(item => item.key === layer);
    return def ? resolveLayerLabel(def, t) : String(layer);
  }

  private createLayerToggles(): HTMLElement {
    const toggles = document.createElement('div');
    toggles.className = 'layer-toggles';
    toggles.id = 'layerToggles';

    // Variant-aware layer buttons
    const fullLayers: (keyof MapLayers)[] = [
      'iranAttacks',                                      // Iran conflict
      'conflicts', 'hotspots', 'sanctions', 'protests',  // geopolitical
      'bases', 'nuclear', 'irradiators',                 // military/strategic
      'military',                                         // military tracking (flights + vessels)
      'cables', 'pipelines', 'outages', 'datacenters',   // infrastructure
      // cyberThreats is intentionally hidden on SVG/mobile fallback (DeckGL desktop only).
      // storageFacilities + fuelShortages are also DeckGL-only — this file has no
      // SVG render path for them (see grep for existing 'pipelines' render at :1100).
      // Adding them here would surface a toggle that produces zero output. They're
      // already restricted to ['flat'] in LAYER_REGISTRY to hide from globe mode too.
      'ais', 'flights', 'gpsJamming',                      // transport/interference
      'natural', 'weather',                               // natural
      'economic',                                         // economic
      'waterways',                                        // labels
      'ciiChoropleth',                                    // CII heat-map (DeckGL only, shown as disabled toggle)
    ];
    const techLayers: (keyof MapLayers)[] = [
      'cables', 'datacenters', 'outages',                // tech infrastructure
      'startupHubs', 'cloudRegions', 'accelerators', 'techHQs', 'techEvents', // tech ecosystem
      'natural', 'weather',                               // natural events
      'economic',                                         // economic/geographic
    ];
    const financeLayers: (keyof MapLayers)[] = [
      'stockExchanges', 'financialCenters', 'centralBanks', 'commodityHubs', // finance ecosystem
      'cables', 'pipelines', 'outages',                   // infrastructure
      'sanctions', 'economic', 'waterways',               // geopolitical/economic
      'natural', 'weather',                               // natural events
    ];
    const happyLayers: (keyof MapLayers)[] = [
      'positiveEvents', 'kindness', 'happiness', 'speciesRecovery', 'renewableInstallations',
    ];
    // Energy variant — SVG/mobile fallback. Only include keys that actually render
    // in this file (commodityPorts/climate/tradeRoutes/resilienceScore/dayNight do
    // not, so they're omitted). Mirrors VARIANT_LAYER_ORDER.energy in
    // src/config/map-layer-definitions.ts but filtered to the SVG-capable subset.
    const energyLayers: (keyof MapLayers)[] = [
      'pipelines',                            // oil + gas pipeline registry (Week 2)
      'waterways',                            // strategic chokepoints
      'ais',                                  // tanker positions at chokepoints
      'commodityHubs',                        // energy exchanges / hubs
      'minerals',                             // critical-minerals + energy-transition overlap
      'sanctions',                            // energy sanctions flows
      'outages',                              // power / energy system status
      'natural',                              // earthquakes near energy infrastructure
      'weather', 'fires',                     // operational risk
      'economic',                             // infrastructure context
    ];
    const layers = SITE_VARIANT === 'tech' ? techLayers
                 : SITE_VARIANT === 'finance' ? financeLayers
                 : SITE_VARIANT === 'happy' ? happyLayers
                 : SITE_VARIANT === 'energy' ? energyLayers
                 : fullLayers;
    const MAX_SVG_LAYERS = 9;
    const enforceLayerLimit = () => {
      const allBtns = Array.from(toggles.querySelectorAll<HTMLButtonElement>('.layer-toggle'));
      const activeBtns = allBtns.filter(b => b.classList.contains('active'));
      if (activeBtns.length > MAX_SVG_LAYERS) {
        const excess = activeBtns.slice(MAX_SVG_LAYERS);
        for (const btn of excess) {
          btn.classList.remove('active');
          const layer = btn.dataset.layer as keyof MapLayers | undefined;
          if (layer) this.toggleLayer(layer);
        }
      }
      const activeCount = allBtns.filter(b => b.classList.contains('active')).length;
      allBtns.forEach(b => {
        if (!b.classList.contains('active')) {
          b.disabled = activeCount >= MAX_SVG_LAYERS;
          b.classList.toggle('limit-reached', activeCount >= MAX_SVG_LAYERS);
        } else {
          b.disabled = false;
          b.classList.remove('limit-reached');
        }
      });
    };

    layers.forEach((layer) => {
      const layerLabel = this.getLayerControlLabel(layer);
      const explainLabel = `Explain ${layerLabel} layer`;
      const row = document.createElement('div');
      row.className = 'layer-toggle-row';
      row.dataset.layer = layer;

      const btn = document.createElement('button');
      btn.className = `layer-toggle ${this.state.layers[layer] ? 'active' : ''}`;
      btn.dataset.layer = layer;
      btn.textContent = layerLabel;
      btn.addEventListener('click', () => {
        this.toggleLayer(layer);
        enforceLayerLimit();
      });
      row.appendChild(btn);

      const explainBtn = document.createElement('button');
      explainBtn.type = 'button';
      explainBtn.className = `layer-explain-btn ${hasCuratedLayerExplanation(layer) ? 'has-layer-explanation' : ''}`;
      explainBtn.dataset.layer = layer;
      explainBtn.textContent = 'i';
      explainBtn.title = explainLabel;
      explainBtn.setAttribute('aria-label', explainLabel);
      explainBtn.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        this.showLayerExplanation(layer);
      });
      row.appendChild(explainBtn);

      toggles.appendChild(row);
    });

    // Add help button
    const helpBtn = document.createElement('button');
    helpBtn.className = 'layer-help-btn';
    helpBtn.textContent = '?';
    helpBtn.title = t('components.deckgl.layerGuide');
    helpBtn.setAttribute('aria-label', t('components.deckgl.layerGuide'));
    helpBtn.addEventListener('click', () => this.showLayerHelp());
    toggles.appendChild(helpBtn);
    enforceLayerLimit();

    return toggles;
  }

  private clearLayerExplanationOutsideClickHandler(): void {
    if (!this.layerExplanationOutsideClickHandler) return;
    document.removeEventListener('click', this.layerExplanationOutsideClickHandler);
    this.layerExplanationOutsideClickHandler = null;
  }

  private showLayerExplanation(layer: keyof MapLayers): void {
    const existing = this.container.querySelector('.layer-explanation-popup') as HTMLElement | null;
    this.clearLayerExplanationOutsideClickHandler();
    if (existing?.dataset.layer === layer) {
      existing.remove();
      this.container.querySelector(`.layer-explain-btn[data-layer="${layer}"]`)?.classList.remove('active');
      return;
    }
    existing?.remove();
    this.container.querySelector('.layer-help-popup')?.remove();
    this.container.querySelectorAll('.layer-explain-btn.active').forEach(btn => btn.classList.remove('active'));

    const popup = document.createElement('div');
    popup.className = 'layer-explanation-popup';
    popup.dataset.layer = layer;
    setTrustedHtml(popup, trustedHtml(
      renderLayerExplanationCard(this.getLayerControlLabel(layer), getLayerExplanation(layer)),
      "static layer explanation metadata",
    ));

    const closePopup = (): void => {
      this.clearLayerExplanationOutsideClickHandler();
      popup.remove();
      this.container.querySelector(`.layer-explain-btn[data-layer="${layer}"]`)?.classList.remove('active');
    };

    popup.querySelector('.layer-explanation-close')?.addEventListener('click', closePopup);
    const content = popup.querySelector('.layer-explanation-content');
    content?.addEventListener('wheel', (e) => e.stopPropagation(), { passive: false });
    content?.addEventListener('touchmove', (e) => e.stopPropagation(), { passive: false });

    setTimeout(() => {
      const closeHandler = (e: MouseEvent) => {
        if (!popup.contains(e.target as Node)) {
          closePopup();
        }
      };
      this.layerExplanationOutsideClickHandler = closeHandler;
      document.addEventListener('click', closeHandler);
    }, 100);

    this.container.appendChild(popup);
    this.container.querySelector(`.layer-explain-btn[data-layer="${layer}"]`)?.classList.add('active');
  }

  private showLayerHelp(): void {
    const existing = this.container.querySelector('.layer-help-popup');
    if (existing) {
      existing.remove();
      return;
    }
    this.container.querySelector('.layer-explanation-popup')?.remove();
    this.clearLayerExplanationOutsideClickHandler();
    this.container.querySelectorAll('.layer-explain-btn.active').forEach(btn => btn.classList.remove('active'));

    const popup = document.createElement('div');
    popup.className = 'layer-help-popup';

    const label = (layerKey: string): string => t(`components.deckgl.layers.${layerKey}`).toUpperCase();
    const staticLabel = (labelKey: string): string => t(`components.deckgl.layerHelp.labels.${labelKey}`).toUpperCase();
    const helpItem = (layerLabel: string, descriptionKey: string): string =>
      `<div class="layer-help-item"><span>${layerLabel}</span> ${t(`components.deckgl.layerHelp.descriptions.${descriptionKey}`)}</div>`;
    const helpSection = (titleKey: string, items: string[], noteKey?: string): string => `
      <div class="layer-help-section">
        <div class="layer-help-title">${t(`components.deckgl.layerHelp.sections.${titleKey}`)}</div>
        ${items.join('')}
        ${noteKey ? `<div class="layer-help-note">${t(`components.deckgl.layerHelp.notes.${noteKey}`)}</div>` : ''}
      </div>
    `;
    const helpHeader = `
      <div class="layer-help-header">
        <span>${t('components.deckgl.layerHelp.title')}</span>
        <button class="layer-help-close" aria-label="Close">×</button>
      </div>
    `;

    const techHelpContent = `
      ${helpHeader}
      <div class="layer-help-content">
        ${helpSection('techEcosystem', [
      helpItem(label('startupHubs'), 'techStartupHubs'),
      helpItem(label('cloudRegions'), 'techCloudRegions'),
      helpItem(label('techHQs'), 'techHQs'),
      helpItem(label('accelerators'), 'techAccelerators'),
      helpItem(label('techEvents'), 'techEvents'),
    ])}
        ${helpSection('infrastructure', [
      helpItem(label('underseaCables'), 'infraCables'),
      helpItem(label('aiDataCenters'), 'infraDatacenters'),
      helpItem(label('internetOutages'), 'infraOutages'),
      helpItem(label('cyberThreats'), 'techCyberThreats'),
    ])}
        ${helpSection('naturalEconomic', [
      helpItem(label('naturalEvents'), 'naturalEventsTech'),
      helpItem(label('fires'), 'techFires'),
      helpItem(staticLabel('countries'), 'countriesOverlay'),
    ])}
      </div>
    `;

    const financeHelpContent = `
      ${helpHeader}
      <div class="layer-help-content">
        ${helpSection('financeCore', [
      helpItem(label('stockExchanges'), 'financeExchanges'),
      helpItem(label('financialCenters'), 'financeCenters'),
      helpItem(label('centralBanks'), 'financeCentralBanks'),
      helpItem(label('commodityHubs'), 'financeCommodityHubs'),
      helpItem(label('gulfInvestments'), 'financeGulfInvestments'),
    ])}
        ${helpSection('infrastructureRisk', [
      helpItem(label('underseaCables'), 'financeCables'),
      helpItem(label('pipelines'), 'financePipelines'),
      helpItem(label('internetOutages'), 'financeOutages'),
      helpItem(label('cyberThreats'), 'financeCyberThreats'),
    ])}
        ${helpSection('macroContext', [
      helpItem(label('economicCenters'), 'economicCenters'),
      helpItem(label('strategicWaterways'), 'macroWaterways'),
      helpItem(label('weatherAlerts'), 'weatherAlertsMarket'),
      helpItem(label('naturalEvents'), 'naturalEventsMacro'),
    ])}
      </div>
    `;

    const fullHelpContent = `
      ${helpHeader}
      <div class="layer-help-content">
        ${helpSection('timeFilter', [
      helpItem(staticLabel('timeRecent'), 'timeRecent'),
      helpItem(staticLabel('timeExtended'), 'timeExtended'),
    ], 'timeAffects')}
        ${helpSection('geopolitical', [
      helpItem(label('conflictZones'), 'geoConflicts'),
      helpItem(label('intelHotspots'), 'geoHotspots'),
      helpItem(staticLabel('sanctions'), 'geoSanctions'),
      helpItem(label('protests'), 'geoProtests'),
      helpItem(label('ucdpEvents'), 'geoUcdpEvents'),
      helpItem(label('displacementFlows'), 'geoDisplacement'),
    ])}
        ${helpSection('militaryStrategic', [
      helpItem(label('militaryBases'), 'militaryBases'),
      helpItem(label('nuclearSites'), 'militaryNuclear'),
      helpItem(label('gammaIrradiators'), 'militaryIrradiators'),
      helpItem(label('militaryActivity'), 'militaryActivity'),
      helpItem(label('spaceports'), 'militarySpaceports'),
    ])}
        ${helpSection('infrastructure', [
      helpItem(label('underseaCables'), 'infraCablesFull'),
      helpItem(label('pipelines'), 'infraPipelinesFull'),
      helpItem(label('internetOutages'), 'infraOutages'),
      helpItem(label('aiDataCenters'), 'infraDatacentersFull'),
      helpItem(label('cyberThreats'), 'infraCyberThreats'),
    ])}
        ${helpSection('transport', [
      helpItem(label('shipTraffic'), 'transportShipping'),
      helpItem(label('flightDelays'), 'transportDelays'),
    ])}
        ${helpSection('naturalEconomic', [
      helpItem(label('naturalEvents'), 'naturalEventsFull'),
      helpItem(label('fires'), 'firesFull'),
      helpItem(label('weatherAlerts'), 'weatherAlerts'),
      helpItem(label('climateAnomalies'), 'climateAnomalies'),
      helpItem(label('economicCenters'), 'economicCenters'),
      helpItem(label('criticalMinerals'), 'mineralsFull'),
    ])}
        ${helpSection('labels', [
      helpItem(staticLabel('countries'), 'countriesOverlay'),
      helpItem(label('strategicWaterways'), 'waterwaysLabels'),
    ])}
      </div>
    `;

    setTrustedHtml(popup, trustedHtml(SITE_VARIANT === 'tech'
      ? techHelpContent
      : SITE_VARIANT === 'finance'
        ? financeHelpContent
        : fullHelpContent, "legacy direct innerHTML migration"));

    popup.querySelector('.layer-help-close')?.addEventListener('click', () => popup.remove());

    // Prevent scroll events from propagating to map
    const content = popup.querySelector('.layer-help-content');
    if (content) {
      content.addEventListener('wheel', (e) => e.stopPropagation(), { passive: false });
      content.addEventListener('touchmove', (e) => e.stopPropagation(), { passive: false });
    }

    // Close on click outside
    setTimeout(() => {
      const closeHandler = (e: MouseEvent) => {
        if (!popup.contains(e.target as Node)) {
          popup.remove();
          document.removeEventListener('click', closeHandler);
        }
      };
      document.addEventListener('click', closeHandler);
    }, 100);

    this.container.appendChild(popup);
  }

  private syncLayerButtons(): void {
    this.container.querySelectorAll<HTMLButtonElement>('.layer-toggle').forEach((btn) => {
      const layer = btn.dataset.layer as keyof MapLayers | undefined;
      if (!layer) return;
      btn.classList.toggle('active', this.state.layers[layer]);
    });
  }

  private createLegend(): HTMLElement {
    const legend = document.createElement('div');
    legend.className = 'map-legend';

    if (SITE_VARIANT === 'tech') {
      // Tech variant legend
      setTrustedHtml(legend, trustedHtml(`
        <div class="map-legend-item"><span class="legend-dot" style="background:#8b5cf6"></span>${escapeHtml(t('components.deckgl.layers.techHQs').toUpperCase())}</div>
        <div class="map-legend-item"><span class="legend-dot" style="background:#06b6d4"></span>${escapeHtml(t('components.deckgl.layers.startupHubs').toUpperCase())}</div>
        <div class="map-legend-item"><span class="legend-dot" style="background:#f59e0b"></span>${escapeHtml(t('components.deckgl.layers.cloudRegions').toUpperCase())}</div>
        <div class="map-legend-item"><span class="map-legend-icon" style="color:#a855f7">📅</span>${escapeHtml(t('components.deckgl.layers.techEvents').toUpperCase())}</div>
        <div class="map-legend-item"><span class="map-legend-icon" style="color:#4ecdc4">💾</span>${escapeHtml(t('components.deckgl.layers.aiDataCenters').toUpperCase())}</div>
      `, "legacy direct innerHTML migration"));
    } else if (SITE_VARIANT === 'happy') {
      // Happy variant legend — natural events only
      setTrustedHtml(legend, trustedHtml(`
        <div class="map-legend-item"><span class="map-legend-icon earthquake">●</span>${escapeHtml(t('components.deckgl.layers.naturalEvents').toUpperCase())}</div>
      `, "legacy direct innerHTML migration"));
    } else {
      // Geopolitical variant legend
      setTrustedHtml(legend, trustedHtml(`
        <div class="map-legend-item"><span class="legend-dot high"></span>${escapeHtml((t('popups.hotspot.levels.high') ?? 'HIGH').toUpperCase())}</div>
        <div class="map-legend-item"><span class="legend-dot elevated"></span>${escapeHtml((t('popups.hotspot.levels.elevated') ?? 'ELEVATED').toUpperCase())}</div>
        <div class="map-legend-item"><span class="legend-dot low"></span>${escapeHtml((t('popups.monitoring') ?? 'MONITORING').toUpperCase())}</div>
        <div class="map-legend-item"><span class="map-legend-icon conflict">⚔</span>${escapeHtml(t('modals.search.types.conflict').toUpperCase())}</div>
        <div class="map-legend-item"><span class="map-legend-icon earthquake">●</span>${escapeHtml(t('modals.search.types.earthquake').toUpperCase())}</div>
        <div class="map-legend-item"><span class="map-legend-icon apt">⚠</span>APT</div>
      `, "legacy direct innerHTML migration"));
    }
    return legend;
  }

  private runHealthCheck(): void {
    const svgNode = this.svg.node();
    if (!svgNode) return;

    // Verify base layer exists and has content
    const baseGroup = svgNode.querySelector('.map-base');
    const countryCount = baseGroup?.querySelectorAll('.country').length ?? 0;

    // If we have country data but no rendered countries, something is wrong
    if (this.countryFeatures && this.countryFeatures.length > 0 && countryCount === 0) {
      console.warn('[Map] Health check: Base layer missing countries, initiating recovery');
      this.baseRendered = false;
      // Also check if d3 selection is stale
      if (baseGroup && this.baseLayerGroup?.node() !== baseGroup) {
        console.warn('[Map] Health check: Stale d3 selection detected');
      }
      this.render();
    }
  }

  private setupZoomHandlers(): void {
    const signal = this.listenerAbort.signal;
    let isDragging = false;
    let lastPos = { x: 0, y: 0 };
    let lastTouchDist = 0;
    let lastTouchCenter = { x: 0, y: 0 };
    const countryClickGesture = createCountryClickGestureTracker();
    const shouldIgnoreInteractionStart = (target: EventTarget | null): boolean => {
      if (!(target instanceof Element)) return false;
      return Boolean(
        target.closest(
          '.map-controls, .time-slider, .layer-toggles, .map-legend, .layer-help-popup, .map-popup, button, select, input, textarea, a'
        )
      );
    };

    // Resume mobile label-overlap measurement on the first direct map interaction.
    // Mobile-only: on desktop the flag is always armed, so this would only ever
    // early-return inside resumeMobileLabelVisibility().
    if (this.isMobile) {
      this.container.addEventListener('pointerdown', (e) => {
        if (shouldIgnoreInteractionStart(e.target)) return;
        this.resumeMobileLabelVisibility();
      }, { signal });
    }

    // Wheel zoom with smooth delta
    this.container.addEventListener(
      'wheel',
      (e) => {
        e.preventDefault();

        // Check if this is a pinch gesture (ctrlKey is set for trackpad pinch)
        if (e.ctrlKey) {
          // Pinch-to-zoom on trackpad
          const zoomDelta = -e.deltaY * 0.01;
          this.state.zoom = Math.max(1, Math.min(10, this.state.zoom + zoomDelta));
        } else {
          // Two-finger scroll for pan, regular scroll for zoom
          if (Math.abs(e.deltaX) > Math.abs(e.deltaY) * 0.5 || e.shiftKey) {
            // Horizontal scroll or shift+scroll = pan
            const panSpeed = 2 / this.state.zoom;
            this.state.pan.x -= e.deltaX * panSpeed;
            this.state.pan.y -= e.deltaY * panSpeed;
          } else {
            // Vertical scroll = zoom
            const zoomDelta = e.deltaY > 0 ? -0.15 : 0.15;
            this.state.zoom = Math.max(1, Math.min(10, this.state.zoom + zoomDelta));
          }
        }
        this.applyTransform();
      },
      { passive: false, signal }
    );

    // Mouse drag for panning
    this.container.addEventListener('mousedown', (e) => {
      if (shouldIgnoreInteractionStart(e.target)) return;
      if (e.button === 0) { // Left click
        isDragging = true;
        lastPos = { x: e.clientX, y: e.clientY };
        startCountryClickGesture(countryClickGesture, { x: e.clientX, y: e.clientY });
        this.container.style.cursor = 'grabbing';
      }
    }, { signal });

    document.addEventListener('mousemove', (e) => {
      if (!isDragging) return;

      const dx = e.clientX - lastPos.x;
      const dy = e.clientY - lastPos.y;
      updateCountryClickGestureDrag(countryClickGesture, { x: e.clientX, y: e.clientY });

      const panSpeed = 1 / this.state.zoom;
      this.state.pan.x += dx * panSpeed;
      this.state.pan.y += dy * panSpeed;

      lastPos = { x: e.clientX, y: e.clientY };
      this.applyTransform();
    }, { signal });

    document.addEventListener('mouseup', () => {
      if (isDragging) {
        isDragging = false;
        finishCountryClickGesture(countryClickGesture);
        this.container.style.cursor = 'grab';
      }
    }, { signal });

    let touchStartPos = { x: 0, y: 0 };
    let touchDragActive = false;
    let lastDragEndTime = 0;
    const TOUCH_DRAG_THRESHOLD = 8;
    const touchHistory: Array<{ x: number; y: number; t: number }> = [];
    let inertiaRaf = 0;

    this.container.addEventListener('touchstart', (e) => {
      if (shouldIgnoreInteractionStart(e.target)) return;
      this.resumeMobileLabelVisibility();
      cancelAnimationFrame(inertiaRaf);
      const touch1 = e.touches[0];
      const touch2 = e.touches[1];

      if (e.touches.length === 2 && touch1 && touch2) {
        e.preventDefault();
        touchDragActive = false;
        lastTouchDist = Math.hypot(
          touch2.clientX - touch1.clientX,
          touch2.clientY - touch1.clientY
        );
        lastTouchCenter = {
          x: (touch1.clientX + touch2.clientX) / 2,
          y: (touch1.clientY + touch2.clientY) / 2,
        };
      } else if (e.touches.length === 1 && touch1) {
        isDragging = true;
        touchDragActive = false;
        touchStartPos = { x: touch1.clientX, y: touch1.clientY };
        lastPos = { x: touch1.clientX, y: touch1.clientY };
        touchHistory.length = 0;
        touchHistory.push({ x: touch1.clientX, y: touch1.clientY, t: performance.now() });
      }
    }, { passive: false, signal });

    this.container.addEventListener('touchmove', (e) => {
      const touch1 = e.touches[0];
      const touch2 = e.touches[1];

      if (e.touches.length === 2 && touch1 && touch2) {
        e.preventDefault();

        const dist = Math.hypot(
          touch2.clientX - touch1.clientX,
          touch2.clientY - touch1.clientY
        );
        const scale = dist / lastTouchDist;
        this.state.zoom = Math.max(1, Math.min(10, this.state.zoom * scale));
        lastTouchDist = dist;

        const center = {
          x: (touch1.clientX + touch2.clientX) / 2,
          y: (touch1.clientY + touch2.clientY) / 2,
        };
        const panSpeed = 1 / this.state.zoom;
        this.state.pan.x += (center.x - lastTouchCenter.x) * panSpeed;
        this.state.pan.y += (center.y - lastTouchCenter.y) * panSpeed;
        lastTouchCenter = center;

        this.applyTransform();
      } else if (e.touches.length === 1 && isDragging && touch1) {
        if (!touchDragActive) {
          const dx0 = touch1.clientX - touchStartPos.x;
          const dy0 = touch1.clientY - touchStartPos.y;
          if (Math.hypot(dx0, dy0) < TOUCH_DRAG_THRESHOLD) return;
          touchDragActive = true;
        }

        e.preventDefault();

        const dx = touch1.clientX - lastPos.x;
        const dy = touch1.clientY - lastPos.y;

        const panSpeed = 1 / this.state.zoom;
        this.state.pan.x += dx * panSpeed;
        this.state.pan.y += dy * panSpeed;

        lastPos = { x: touch1.clientX, y: touch1.clientY };
        const now = performance.now();
        touchHistory.push({ x: touch1.clientX, y: touch1.clientY, t: now });
        if (touchHistory.length > 4) touchHistory.shift();

        this.applyTransform();
      }
    }, { passive: false, signal });

    this.container.addEventListener('touchend', () => {
      if (touchDragActive && touchHistory.length >= 2) {
        const last = touchHistory[touchHistory.length - 1]!;
        const first = touchHistory[0]!;
        const dt = (last.t - first.t) / 1000;
        if (dt > 0 && dt < 0.3) {
          let vx = (last.x - first.x) / dt;
          let vy = (last.y - first.y) / dt;
          const panSpeed = 1 / this.state.zoom;
          const decay = 0.92;
          const animate = () => {
            vx *= decay;
            vy *= decay;
            if (Math.abs(vx) < 10 && Math.abs(vy) < 10) return;
            this.state.pan.x += (vx / 60) * panSpeed;
            this.state.pan.y += (vy / 60) * panSpeed;
            this.applyTransform();
            inertiaRaf = requestAnimationFrame(animate);
          };
          inertiaRaf = requestAnimationFrame(animate);
        }
      }
      isDragging = false;
      if (touchDragActive) lastDragEndTime = performance.now();
      touchDragActive = false;
      lastTouchDist = 0;
      touchHistory.length = 0;
    }, { signal });

    this.container.addEventListener('click', (e) => {
      if (!this.onCountryClick) return;
      if (performance.now() - lastDragEndTime < 300) return;
      if (shouldSuppressCountryClick(countryClickGesture)) return;
      const containerRect = this.container.getBoundingClientRect();
      const zoom = this.state.zoom;
      const width = this.container.clientWidth;
      const height = this.container.clientHeight;
      const centerOffsetX = (width / 2) * (1 - zoom);
      const centerOffsetY = (height / 2) * (1 - zoom);
      const tx = centerOffsetX + this.state.pan.x * zoom;
      const ty = centerOffsetY + this.state.pan.y * zoom;
      const rawX = (e.clientX - containerRect.left - tx) / zoom;
      const rawY = (e.clientY - containerRect.top - ty) / zoom;
      const projection = this.getProjection(width, height);
      if (!projection.invert) return;
      const coords = projection.invert([rawX, rawY]);
      if (!coords) return;
      const [lon, lat] = coords;
      const hit = getCountryAtCoordinates(lat, lon);
      if (hit) {
        this.onCountryClick({ lat, lon, code: hit.code, name: hit.name });
      }
    }, { signal });

    this.container.style.cursor = 'grab';
  }

  private async loadMapData(): Promise<void> {
    try {
      const worldResponse = await fetch(worldTopologyUrl(this.isMobile));
      this.worldData = await worldResponse.json();
      if (this.worldData) {
        const countries = topojson.feature(
          this.worldData,
          this.worldData.objects.countries
        );
        this.countryFeatures = 'features' in countries ? countries.features : [countries];
      }
      this.baseRendered = false;
      this.render();
      // Re-render after layout stabilizes to catch full container width.
      this.scheduleRender();
    } catch (e) {
      console.error('Failed to load map data:', e);
    }
  }

  private initClusterRenderer(): void {
    // WebGL clustering disabled - just get context for clearing canvas
    const gl = this.clusterCanvas.getContext('webgl');
    if (!gl) return;
    this.clusterGl = gl;
  }

  private clearClusterCanvas(): void {
    if (!this.clusterGl) return;
    this.clusterGl.clearColor(0, 0, 0, 0);
    this.clusterGl.clear(this.clusterGl.COLOR_BUFFER_BIT);
  }

  private renderClusterLayer(_projection: d3.GeoProjection): void {
    // WebGL clustering disabled - all layers use HTML markers for visual fidelity
    // (severity colors, emoji icons, magnitude sizing, animations)
    this.wrapper.classList.toggle('cluster-active', false);
    this.clearClusterCanvas();
  }

  public scheduleRender(): void {
    if (this.renderScheduled) return;
    this.renderScheduled = true;
    measure(() => {
      const { width, height } = this.readContainerSize();
      const measuredAt = performance.now();
      mutate(() => {
        if (this.destroyed) {
          this.renderScheduled = false;
          return;
        }
        if (measuredAt - this.lastRenderTime < this.MIN_RENDER_INTERVAL_MS) {
          this.renderScheduled = false;
          this.scheduleRender();
          return;
        }
        this.lastRenderTime = measuredAt;
        this.renderScheduled = false;
        this.renderWithSize(width, height);
      });
    });
  }

  private rememberContainerSize(size: { width: number; height: number }): { width: number; height: number } {
    this.lastContainerSize = size;
    return size;
  }

  private readContainerSize(): { width: number; height: number } {
    return this.rememberContainerSize({
      width: this.container.clientWidth,
      height: this.container.clientHeight,
    });
  }

  private getKnownContainerSize(): { width: number; height: number } {
    return this.lastContainerSize.width > 0 && this.lastContainerSize.height > 0
      ? this.lastContainerSize
      : this.readContainerSize();
  }

  private appendOverlay(node: Node): void {
    (this.overlayAppendTarget ?? this.overlays).appendChild(node);
  }

  public render(): void {
    if (this.destroyed) return;
    const now = performance.now();
    if (now - this.lastRenderTime < this.MIN_RENDER_INTERVAL_MS) {
      this.scheduleRender();
      return;
    }
    this.lastRenderTime = now;

    const { width, height } = this.readContainerSize();
    this.renderWithSize(width, height);
  }

  private renderWithSize(width: number, height: number): void {
    if (this.destroyed) return;
    this.rememberContainerSize({ width, height });

    // Skip render if container has no dimensions (tab throttled, hidden, etc.)
    if (width === 0 || height === 0) {
      return;
    }

    // Simple viewBox matching container - keeps SVG and overlays aligned
    if (!this.svg) return;
    this.svg.attr('viewBox', `0 0 ${width} ${height}`);

    // CRITICAL: Always refresh d3 selections from actual DOM to prevent stale references
    // D3 selections can become stale if the DOM is modified externally
    const svgNode = this.svg.node();
    if (!svgNode) return;

    // Query DOM directly for layer groups
    const existingBase = svgNode.querySelector('.map-base') as SVGGElement | null;
    const existingDynamic = svgNode.querySelector('.map-dynamic') as SVGGElement | null;

    // Recreate layer groups if missing or if d3 selections are stale
    const baseStale = !existingBase || this.baseLayerGroup?.node() !== existingBase;
    const dynamicStale = !existingDynamic || this.dynamicLayerGroup?.node() !== existingDynamic;

    if (baseStale || dynamicStale) {
      // Clear any orphaned groups and create fresh ones
      svgNode.querySelectorAll('.map-base, .map-dynamic').forEach(el => el.remove());
      this.baseLayerGroup = this.svg.append('g').attr('class', 'map-base');
      this.dynamicLayerGroup = this.svg.append('g').attr('class', 'map-dynamic');
      this.baseRendered = false;
      console.warn('[Map] Layer groups recreated - baseStale:', baseStale, 'dynamicStale:', dynamicStale);
    }

    // Double-check selections are valid after recreation
    if (!this.baseLayerGroup?.node() || !this.dynamicLayerGroup?.node()) {
      console.error('[Map] Failed to create layer groups');
      return;
    }

    // Check if base layer has actual country content (not just empty group)
    const countryCount = this.baseLayerGroup.node()!.querySelectorAll('.country').length;
    const shouldRenderBase = !this.baseRendered || countryCount === 0 || width !== this.baseWidth || height !== this.baseHeight;

    // Debug: log when base layer needs re-render
    if (shouldRenderBase && countryCount === 0 && this.baseRendered) {
      console.warn('[Map] Base layer missing countries, forcing re-render. countryFeatures:', this.countryFeatures?.length ?? 'null');
    }

    if (shouldRenderBase) {
      this.baseWidth = width;
      this.baseHeight = height;
      // Use native DOM clear for guaranteed effect
      const baseNode = this.baseLayerGroup.node()!;
      while (baseNode.firstChild) baseNode.removeChild(baseNode.firstChild);

      // Background - extend well beyond viewBox to cover pan/zoom transforms
      // 3x size in each direction ensures no black bars when panning
      this.baseLayerGroup
        .append('rect')
        .attr('x', -width)
        .attr('y', -height)
        .attr('width', width * 3)
        .attr('height', height * 3)
        .attr('fill', getCSSColor('--map-bg'));

      // Grid
      this.renderGrid(this.baseLayerGroup, width, height);

      // Setup projection for base elements
      const baseProjection = this.getProjection(width, height);
      const basePath = d3.geoPath().projection(baseProjection);

      // Graticule
      this.renderGraticule(this.baseLayerGroup, basePath);

      // Countries
      this.renderCountries(this.baseLayerGroup, basePath);
      this.baseRendered = true;
    }

    // Defer the first dynamic-overlay pass off the first-paint critical path. The base map
    // (countries) above is enough for LCP; the dynamic layer below (cables/pipelines/
    // conflicts/AIS/cluster markers/overlays) is the heavy synchronous cost (#4429 — ~1.3s
    // of mobile boot scripting, the #1 mobile-TBT contributor since mobile uses this SVG
    // renderer). After first paint, render fully so interactions update overlays immediately.
    if (!this.initialDynamicRendered) {
      if (!this.initialDynamicScheduled) {
        this.initialDynamicScheduled = true;
        // First paint: build the dynamic layers off the critical path AND chunked into
        // sub-50ms tasks (#4442), so the overlay build is neither blocking nor one long task.
        scheduleAfterFirstPaint(() => { void this.renderInitialDynamicPass(); });
      }
      this.applyTransform();
      return;
    }

    // Steady state (post first-paint): build the dynamic layers synchronously so interactions
    // (zoom/pan/toggle/theme) update overlays immediately. renderDynamicLayers takes no await
    // when chunk=false, so this runs to completion before returning.
    void this.renderDynamicLayers(width, height);

    // POST-RENDER VERIFICATION: Ensure base layer actually rendered
    // This catches silent failures where d3 operations didn't stick
    if (this.baseRendered && this.countryFeatures && this.countryFeatures.length > 0) {
      const verifyCount = this.baseLayerGroup?.node()?.querySelectorAll('.country').length ?? 0;
      if (verifyCount === 0) {
        console.error('[Map] POST-RENDER: Countries failed to render despite baseRendered=true. Forcing full rebuild.');
        this.baseRendered = false;
        // Schedule a retry on next frame instead of immediate recursion
        this.scheduleRender();
        return;
      }
    }

    this.applyTransform();
  }

  // Builds the dynamic overlay layers (cables/pipelines/conflicts/AIS/cluster/overlays).
  // When `chunk` is true, yields between layers so the build runs as several sub-50ms tasks
  // instead of one long task (#4442). Steady-state callers pass chunk=false → no await is
  // reached, so it runs synchronously. The re-entrancy token lets a chunked pass bail once a
  // newer render (which bumps the token and rebuilds) has superseded it.
  private async renderDynamicLayers(width: number, height: number, chunk = false): Promise<void> {
    const dynamicGroup = this.dynamicLayerGroup;
    const dynamicNode = dynamicGroup?.node();
    if (!dynamicGroup || !dynamicNode) return;
    const token = ++this.dynamicRenderToken;

    // Rebuild dynamic layer - native DOM clear for reliability
    while (dynamicNode.firstChild) dynamicNode.removeChild(dynamicNode.firstChild);
    dynamicGroup.append('g').attr('class', 'overlays-svg');

    const projection = this.getProjection(width, height);
    // Update country fills (sanctions toggle without rebuilding geometry)
    this.updateCountryFills();

    const steps: Array<() => void> = [];
    if (this.state.layers.cables) steps.push(() => this.renderCables(projection));
    if (this.state.layers.pipelines) steps.push(() => this.renderPipelines(projection));
    if (this.state.layers.conflicts) steps.push(() => this.renderConflicts(projection));
    if (this.state.layers.ais) steps.push(() => this.renderAisDensity(projection));
    steps.push(() => this.renderClusterLayer(projection));
    steps.push(() => this.renderOverlays(projection));

    for (let i = 0; i < steps.length; i++) {
      if (chunk && (this.destroyed || token !== this.dynamicRenderToken)) return;
      steps[i]?.();
      if (chunk && i < steps.length - 1) await yieldToMain();
    }
  }

  // First-paint dynamic pass: the base map is already painted, so build the overlays chunked
  // (off the critical path + split into sub-50ms tasks). Steady-state renders run synchronously.
  private async renderInitialDynamicPass(): Promise<void> {
    if (this.destroyed || !this.svg) return;
    const { width, height } = this.getKnownContainerSize();
    if (this.destroyed) return;
    if (width === 0 || height === 0) return; // next real render handles it
    this.initialDynamicRendered = true;
    await this.renderDynamicLayers(width, height, true);
    if (!this.destroyed) this.applyTransform();
  }

  private renderGrid(
    group: d3.Selection<SVGGElement, unknown, null, undefined>,
    width: number,
    height: number,
    yStart = 0
  ): void {
    const gridGroup = group.append('g').attr('class', 'grid');

    for (let x = 0; x < width; x += 20) {
      gridGroup
        .append('line')
        .attr('x1', x)
        .attr('y1', yStart)
        .attr('x2', x)
        .attr('y2', yStart + height)
        .attr('stroke', getCSSColor('--map-grid'))
        .attr('stroke-width', 0.5);
    }

    for (let y = yStart; y < yStart + height; y += 20) {
      gridGroup
        .append('line')
        .attr('x1', 0)
        .attr('y1', y)
        .attr('x2', width)
        .attr('y2', y)
        .attr('stroke', getCSSColor('--map-grid'))
        .attr('stroke-width', 0.5);
    }
  }

  private getProjection(width: number, height: number): d3.GeoProjection {
    // Equirectangular with cropped latitude range (72°N to 56°S = 128°)
    // Shows Greenland/Iceland while trimming extreme polar regions
    const LAT_NORTH = 72;  // Includes Greenland (extends to ~83°N but 72 shows most)
    const LAT_SOUTH = -56; // Just below Tierra del Fuego
    const LAT_RANGE = LAT_NORTH - LAT_SOUTH; // 128°
    const LAT_CENTER = (LAT_NORTH + LAT_SOUTH) / 2; // 8°N

    // Scale to fit: 360° longitude in width, 128° latitude in height
    const scaleForWidth = width / (2 * Math.PI);
    const scaleForHeight = height / (LAT_RANGE * Math.PI / 180);
    const scale = Math.min(scaleForWidth, scaleForHeight);

    return d3
      .geoEquirectangular()
      .scale(scale)
      .center([0, LAT_CENTER])
      .translate([width / 2, height / 2]);
  }

  private renderGraticule(
    group: d3.Selection<SVGGElement, unknown, null, undefined>,
    path: d3.GeoPath
  ): void {
    const graticule = d3.geoGraticule();
    group
      .append('path')
      .datum(graticule())
      .attr('class', 'graticule')
      .attr('d', path)
      .attr('fill', 'none')
      .attr('stroke', getCSSColor('--map-stroke'))
      .attr('stroke-width', 0.4);
  }

  private renderCountries(
    group: d3.Selection<SVGGElement, unknown, null, undefined>,
    path: d3.GeoPath
  ): void {
    if (!this.countryFeatures) return;

    group
      .selectAll('.country')
      .data(this.countryFeatures)
      .enter()
      .append('path')
      .attr('class', 'country')
      .attr('d', path as unknown as string)
      .attr('fill', getCSSColor('--map-country'))
      .attr('stroke', getCSSColor('--map-stroke'))
      .attr('stroke-width', 0.7);
  }

  private renderCables(projection: d3.GeoProjection): void {
    if (!this.dynamicLayerGroup) return;
    const cableGroup = this.dynamicLayerGroup.append('g').attr('class', 'cables');

    UNDERSEA_CABLES.forEach((cable) => {
      const lineGenerator = d3
        .line<[number, number]>()
        .x((d) => projection(d)?.[0] ?? 0)
        .y((d) => projection(d)?.[1] ?? 0)
        .curve(d3.curveCardinal);

      const isHighlighted = this.highlightedAssets.cable.has(cable.id);
      const cableAdvisory = this.getCableAdvisory(cable.id);
      const advisoryClass = cableAdvisory ? `cable-${cableAdvisory.severity}` : '';
      const healthRecord = this.healthByCableId[cable.id];
      const healthClass = healthRecord?.status === 'fault' ? 'cable-health-fault' : healthRecord?.status === 'degraded' ? 'cable-health-degraded' : '';
      const highlightClass = isHighlighted ? 'asset-highlight asset-highlight-cable' : '';

      const path = cableGroup
        .append('path')
        .attr('class', `cable-path ${advisoryClass} ${healthClass} ${highlightClass}`.trim())
        .attr('d', lineGenerator(cable.points));

      path.append('title').text(cable.name);

      path.on('click', (event: MouseEvent) => {
        event.stopPropagation();
        const rect = this.container.getBoundingClientRect();
        this.popup.show({
          type: 'cable',
          data: cable,
          x: event.clientX - rect.left,
          y: event.clientY - rect.top,
        });
      });
    });
  }

  private renderPipelines(projection: d3.GeoProjection): void {
    if (!this.dynamicLayerGroup) return;
    const pipelineGroup = this.dynamicLayerGroup.append('g').attr('class', 'pipelines');

    PIPELINES.forEach((pipeline) => {
      const lineGenerator = d3
        .line<[number, number]>()
        .x((d) => projection(d)?.[0] ?? 0)
        .y((d) => projection(d)?.[1] ?? 0)
        .curve(d3.curveCardinal.tension(0.5));

      const color = PIPELINE_COLORS[pipeline.type] || getCSSColor('--text-dim');
      const opacity = 0.85;
      const dashArray = pipeline.status === 'construction' ? '4,2' : 'none';

      const isHighlighted = this.highlightedAssets.pipeline.has(pipeline.id);
      const path = pipelineGroup
        .append('path')
        .attr('class', `pipeline-path pipeline-${pipeline.type} pipeline-${pipeline.status}${isHighlighted ? ' asset-highlight asset-highlight-pipeline' : ''}`)
        .attr('d', lineGenerator(pipeline.points))
        .attr('fill', 'none')
        .attr('stroke', color)
        .attr('stroke-width', 2.5)
        .attr('stroke-opacity', opacity)
        .attr('stroke-linecap', 'round')
        .attr('stroke-linejoin', 'round');

      if (dashArray !== 'none') {
        path.attr('stroke-dasharray', dashArray);
      }

      path.append('title').text(`${pipeline.name} (${pipeline.type.toUpperCase()})`);

      path.on('click', (event: MouseEvent) => {
        event.stopPropagation();
        const rect = this.container.getBoundingClientRect();
        this.popup.show({
          type: 'pipeline',
          data: pipeline,
          x: event.clientX - rect.left,
          y: event.clientY - rect.top,
        });
      });
    });
  }

  private renderConflicts(projection: d3.GeoProjection): void {
    if (!this.dynamicLayerGroup) return;
    const conflictGroup = this.dynamicLayerGroup.append('g').attr('class', 'conflicts');

    CONFLICT_ZONES.forEach((zone) => {
      const points = zone.coords
        .map((c) => projection(c as [number, number]))
        .filter((p): p is [number, number] => p !== null);

      if (points.length > 0) {
        conflictGroup
          .append('polygon')
          .attr('class', 'conflict-zone')
          .attr('points', points.map((p) => p.join(',')).join(' '));
        // Labels are now rendered as HTML overlays in renderConflictLabels()
      }
    });
  }


  private updateCountryFills(): void {
    if (!this.baseLayerGroup || !this.countryFeatures) return;

    const sanctionColors: Record<string, string> = {
      severe: 'rgba(255, 0, 0, 0.35)',
      high: 'rgba(255, 100, 0, 0.25)',
      moderate: 'rgba(255, 200, 0, 0.2)',
    };
    const defaultFill = getCSSColor('--map-country');
    const useSanctions = this.state.layers.sanctions;

    this.baseLayerGroup.selectAll('.country').each(function (datum) {
      const el = d3.select(this);
      const id = datum as { id?: number };
      if (!useSanctions) {
        el.attr('fill', defaultFill);
        return;
      }
      if (id?.id !== undefined && SANCTIONED_COUNTRIES[id.id]) {
        const level = SANCTIONED_COUNTRIES[id.id];
        if (level) {
          el.attr('fill', sanctionColors[level] || defaultFill);
          return;
        }
      }
      el.attr('fill', defaultFill);
    });
  }

  // Generic marker clustering - groups markers within pixelRadius into clusters
  // groupKey function ensures only items with same key can cluster (e.g., same city)
  private clusterMarkers<T extends { lat: number; lon: number }>(
    items: T[],
    projection: d3.GeoProjection,
    pixelRadius: number,
    getGroupKey?: (item: T) => string
  ): Array<{ items: T[]; center: [number, number]; pos: [number, number] }> {
    const clusters: Array<{ items: T[]; center: [number, number]; pos: [number, number] }> = [];
    const assigned = new Set<number>();

    for (let i = 0; i < items.length; i++) {
      if (assigned.has(i)) continue;

      const item = items[i]!;
      if (!Number.isFinite(item.lat) || !Number.isFinite(item.lon)) continue;
      const pos = projection([item.lon, item.lat]);
      if (!pos || !Number.isFinite(pos[0]) || !Number.isFinite(pos[1])) continue;

      const cluster: T[] = [item];
      assigned.add(i);
      const itemKey = getGroupKey?.(item);

      // Find nearby items (must share same group key if provided)
      for (let j = i + 1; j < items.length; j++) {
        if (assigned.has(j)) continue;
        const other = items[j]!;

        // Skip if different group keys (e.g., different cities)
        if (getGroupKey && getGroupKey(other) !== itemKey) continue;

        if (!Number.isFinite(other.lat) || !Number.isFinite(other.lon)) continue;
        const otherPos = projection([other.lon, other.lat]);
        if (!otherPos || !Number.isFinite(otherPos[0]) || !Number.isFinite(otherPos[1])) continue;

        const dx = pos[0] - otherPos[0];
        const dy = pos[1] - otherPos[1];
        const dist = Math.sqrt(dx * dx + dy * dy);

        if (dist <= pixelRadius) {
          cluster.push(other);
          assigned.add(j);
        }
      }

      // Calculate cluster center
      let sumLat = 0, sumLon = 0;
      for (const c of cluster) {
        sumLat += c.lat;
        sumLon += c.lon;
      }
      const centerLat = sumLat / cluster.length;
      const centerLon = sumLon / cluster.length;
      const centerPos = projection([centerLon, centerLat]);
      const finalPos = (centerPos && Number.isFinite(centerPos[0]) && Number.isFinite(centerPos[1]))
        ? centerPos : pos;

      clusters.push({
        items: cluster,
        center: [centerLon, centerLat],
        pos: finalPos,
      });
    }

    return clusters;
  }

  private renderOverlays(projection: d3.GeoProjection): void {
    setTrustedHtml(this.overlays, trustedHtml('', "legacy direct innerHTML migration"));
    this.labelVisibilityScheduled = false;
    const fragment = document.createDocumentFragment();
    const previousTarget = this.overlayAppendTarget;
    this.overlayAppendTarget = fragment;

    try {
    // Strategic waterways
    if (this.state.layers.waterways) {
      this.renderWaterways(projection);
    }

    if (this.state.layers.ais) {
      this.renderAisDisruptions(projection);
      this.renderPorts(projection);
    }

    // APT groups — rendered only when cyberThreats layer is active, loaded lazily
    if (this.state.layers.cyberThreats && SITE_VARIANT !== 'tech' && this.aptGroups.length > 0) {
      this.renderAPTMarkers(projection);
    }

    // Nuclear facilities (always HTML - shapes convey status)
    if (this.state.layers.nuclear) {
      NUCLEAR_FACILITIES.forEach((facility) => {
        const pos = projection([facility.lon, facility.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        const isHighlighted = this.highlightedAssets.nuclear.has(facility.id);
        div.className = `nuclear-marker ${facility.status}${isHighlighted ? ' asset-highlight asset-highlight-nuclear' : ''}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.title = `${facility.name} (${facility.type})`;

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'nuclear',
            data: facility,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Gamma irradiators (IAEA DIIF) - no labels, click to see details
    if (this.state.layers.irradiators) {
      GAMMA_IRRADIATORS.forEach((irradiator) => {
        const pos = projection([irradiator.lon, irradiator.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = 'irradiator-marker';
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.title = `${irradiator.city}, ${irradiator.country}`;

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'irradiator',
            data: irradiator,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Conflict zone click areas
    if (this.state.layers.conflicts) {
      CONFLICT_ZONES.forEach((zone) => {
        const centerPos = projection(zone.center as [number, number]);
        if (!centerPos) return;

        const clickArea = document.createElement('div');
        clickArea.className = 'conflict-click-area';
        clickArea.style.left = `${centerPos[0] - 40}px`;
        clickArea.style.top = `${centerPos[1] - 20}px`;
        clickArea.style.width = '80px';
        clickArea.style.height = '40px';
        clickArea.style.cursor = 'pointer';

        clickArea.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'conflict',
            data: zone,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
          this.popup.loadConflictHistory(zone);
        });

        this.appendOverlay(clickArea);
      });
      this.renderConflictEventMarkers(projection);
    }

    // Iran events (severity-colored circles matching DeckGL layer)
    if (this.state.layers.iranAttacks && this.iranEvents.length > 0) {
      const iranEventsForRender = this.isMobile
        ? this.iranEvents.slice(0, MapComponent.MOBILE_MAX_IRAN_EVENTS)
        : this.iranEvents;
      iranEventsForRender.forEach((ev) => {
        const pos = projection([ev.longitude, ev.latitude]);
        if (!pos || !Number.isFinite(pos[0]) || !Number.isFinite(pos[1])) return;

        const size = getIranEventSize(ev.severity);
        const color = getIranEventCssColor(ev);

        const div = document.createElement('div');
        div.className = 'iran-event-marker';
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.style.width = `${size}px`;
        div.style.height = `${size}px`;
        div.style.background = color;
        div.title = `${ev.title} (${ev.category})`;

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'iranEvent',
            data: ev,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Hotspots (always HTML - level colors and BREAKING badges)
    if (this.state.layers.hotspots) {
      this.hotspots.forEach((spot) => {
        const pos = projection([spot.lon, spot.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = 'hotspot';
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        setTrustedHtml(div, trustedHtml(`
          <div class="hotspot-marker ${escapeHtml(spot.level || 'low')}"></div>
        `, "legacy direct innerHTML migration"));

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const relatedNews = this.getRelatedNews(spot);
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'hotspot',
            data: spot,
            relatedNews,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
          this.popup.loadHotspotGdeltContext(spot);
          this.onHotspotClick?.(spot);
        });

        this.appendOverlay(div);
      });
    }

    // Military bases (always HTML - nation colors matter)
    if (this.state.layers.bases) {
      this.getMilitaryBasesForRender().forEach((base) => {
        const pos = projection([base.lon, base.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        const isHighlighted = this.highlightedAssets.base.has(base.id);
        div.className = `base-marker ${base.type}${isHighlighted ? ' asset-highlight asset-highlight-base' : ''}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const label = document.createElement('div');
        label.className = 'base-label';
        label.textContent = base.name;
        div.appendChild(label);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'base',
            data: base,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Earthquakes (magnitude-based sizing) - part of NATURAL layer
    if (this.state.layers.natural) {
      console.log('[Map] Rendering earthquakes. Total:', this.earthquakes.length, 'Layer enabled:', this.state.layers.natural);
      const filteredQuakes = this.state.timeRange === 'all'
        ? this.earthquakes
        : this.earthquakes.filter((eq) => eq.occurredAt >= Date.now() - this.getTimeRangeMs());
      const quakesForRender = this.isMobile
        ? filteredQuakes.filter((eq) => eq.magnitude >= MapComponent.MOBILE_MIN_EARTHQUAKE_MAGNITUDE)
        : filteredQuakes;
      console.log('[Map] After time/mobile filter:', quakesForRender.length, 'earthquakes. TimeRange:', this.state.timeRange);
      let rendered = 0;
      quakesForRender.forEach((eq) => {
        const pos = projection([eq.location?.longitude ?? 0, eq.location?.latitude ?? 0]);
        if (!pos) {
          console.log('[Map] Earthquake position null for:', eq.place, eq.location?.longitude, eq.location?.latitude);
          return;
        }
        rendered++;

        const size = Math.max(8, eq.magnitude * 3);
        const div = document.createElement('div');
        div.className = 'earthquake-marker';
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.style.width = `${size}px`;
        div.style.height = `${size}px`;
        div.title = `M${eq.magnitude.toFixed(1)} - ${eq.place}`;

        const label = document.createElement('div');
        label.className = 'earthquake-label';
        label.textContent = `M${eq.magnitude.toFixed(1)}`;
        div.appendChild(label);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'earthquake',
            data: eq,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
      console.log('[Map] Actually rendered', rendered, 'earthquake markers');
    }

    // Economic Centers (always HTML - emoji icons for type distinction)
    if (this.state.layers.economic) {
      ECONOMIC_CENTERS.forEach((center) => {
        const pos = projection([center.lon, center.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `economic-marker ${center.type}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'economic-icon';
        icon.textContent = center.type === 'exchange' ? '📈' : center.type === 'central-bank' ? '🏛' : '💰';
        div.appendChild(icon);
        div.title = center.name;

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'economic',
            data: center,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Weather Alerts (severity icons)
    if (this.state.layers.weather) {
      this.weatherAlerts.forEach((alert) => {
        if (!alert.centroid) return;
        const pos = projection(alert.centroid);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `weather-marker ${alert.severity.toLowerCase()}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.style.borderColor = getSeverityColor(alert.severity);

        const icon = document.createElement('div');
        icon.className = 'weather-icon';
        icon.textContent = '⚠';
        div.appendChild(icon);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'weather',
            data: alert,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    if (this.state.layers.radiationWatch) {
      this.radiationObservations.forEach((observation) => {
        const pos = projection([observation.lon, observation.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        const color = observation.severity === 'spike' ? '#ff3030' : '#ffaa00';
        div.className = `radiation-watch-marker radiation-watch-marker-${observation.severity}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.style.width = '14px';
        div.style.height = '14px';
        div.style.borderRadius = '50%';
        div.style.background = color;
        div.style.border = '2px solid rgba(255,255,255,0.75)';
        div.style.boxShadow = `0 0 10px ${color}88`;
        div.title = `${observation.location}: ${observation.value.toFixed(1)} ${observation.unit}`;

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'radiation',
            data: observation,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Internet Outages (severity colors)
    if (this.state.layers.outages) {
      this.outages.forEach((outage) => {
        const pos = projection([outage.lon, outage.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `outage-marker ${outage.severity}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'outage-icon';
        icon.textContent = '📡';
        div.appendChild(icon);

        const label = document.createElement('div');
        label.className = 'outage-label';
        label.textContent = outage.country;
        div.appendChild(label);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'outage',
            data: outage,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Cable advisories & repair ships
    if (this.state.layers.cables) {
      this.cableAdvisories.forEach((advisory) => {
        const pos = projection([advisory.lon, advisory.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `cable-advisory-marker ${advisory.severity}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'cable-advisory-icon';
        icon.textContent = advisory.severity === 'fault' ? '⚡' : '⚠';
        div.appendChild(icon);

        const label = document.createElement('div');
        label.className = 'cable-advisory-label';
        label.textContent = this.getCableName(advisory.cableId);
        div.appendChild(label);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'cable-advisory',
            data: advisory,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });

      this.repairShips.forEach((ship) => {
        const pos = projection([ship.lon, ship.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `repair-ship-marker ${ship.status}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'repair-ship-icon';
        icon.textContent = '🚢';
        div.appendChild(icon);

        const label = document.createElement('div');
        label.className = 'repair-ship-label';
        label.textContent = ship.name;
        div.appendChild(label);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'repair-ship',
            data: ship,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // AI Data Centers (always HTML - 🖥️ icons, filter to ≥10k GPUs)
    const MIN_GPU_COUNT = 10000;
    if (this.state.layers.datacenters) {
      AI_DATA_CENTERS.filter(dc => (dc.chipCount || 0) >= MIN_GPU_COUNT).forEach((dc) => {
        const pos = projection([dc.lon, dc.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        const isHighlighted = this.highlightedAssets.datacenter.has(dc.id);
        div.className = `datacenter-marker ${dc.status}${isHighlighted ? ' asset-highlight asset-highlight-datacenter' : ''}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'datacenter-icon';
        icon.textContent = '🖥️';
        div.appendChild(icon);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'datacenter',
            data: dc,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Spaceports (🚀 icon)
    if (this.state.layers.spaceports) {
      SPACEPORTS.forEach((port) => {
        const pos = projection([port.lon, port.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `spaceport-marker ${port.status}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'spaceport-icon';
        icon.textContent = '🚀';
        div.appendChild(icon);

        const label = document.createElement('div');
        label.className = 'spaceport-label';
        label.textContent = port.name;
        div.appendChild(label);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'spaceport',
            data: port,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Critical Minerals (💎 icon)
    if (this.state.layers.minerals) {
      CRITICAL_MINERALS.forEach((mine) => {
        const pos = projection([mine.lon, mine.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `mineral-marker ${mine.status}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'mineral-icon';
        // Select icon based on mineral type
        icon.textContent = mine.mineral === 'Lithium' ? '🔋' : mine.mineral === 'Rare Earths' ? '🧲' : '💎';
        div.appendChild(icon);

        const label = document.createElement('div');
        label.className = 'mineral-label';
        label.textContent = `${mine.mineral} - ${mine.name}`;
        div.appendChild(label);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'mineral',
            data: mine,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // === TECH VARIANT LAYERS ===

    // Startup Hubs (🚀 icon by tier)
    if (this.state.layers.startupHubs) {
      STARTUP_HUBS.forEach((hub) => {
        const pos = projection([hub.lon, hub.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `startup-hub-marker ${hub.tier}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'startup-hub-icon';
        icon.textContent = hub.tier === 'mega' ? '🦄' : hub.tier === 'major' ? '🚀' : '💡';
        div.appendChild(icon);

        if (this.state.zoom >= 2 || hub.tier === 'mega') {
          const label = document.createElement('div');
          label.className = 'startup-hub-label';
          label.textContent = hub.name;
          div.appendChild(label);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'startupHub',
            data: hub,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Cloud Regions (☁️ icons by provider)
    if (this.state.layers.cloudRegions) {
      CLOUD_REGIONS.forEach((region) => {
        const pos = projection([region.lon, region.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `cloud-region-marker ${region.provider}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'cloud-region-icon';
        // Provider-specific icons
        const icons: Record<string, string> = { aws: '🟠', gcp: '🔵', azure: '🟣', cloudflare: '🟡' };
        icon.textContent = icons[region.provider] || '☁️';
        div.appendChild(icon);

        if (this.state.zoom >= 3) {
          const label = document.createElement('div');
          label.className = 'cloud-region-label';
          label.textContent = region.provider.toUpperCase();
          div.appendChild(label);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'cloudRegion',
            data: region,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Tech HQs (🏢 icons by company type) - with clustering by city
    if (this.state.layers.techHQs) {
      // Cluster radius depends on zoom - tighter clustering when zoomed out
      const clusterRadius = this.state.zoom >= 4 ? 15 : this.state.zoom >= 3 ? 25 : 40;
      // Group by city to prevent clustering companies from different cities
      const clusters = this.clusterMarkers(TECH_HQS, projection, clusterRadius, hq => hq.city);

      clusters.forEach((cluster) => {
        if (cluster.items.length === 0) return;
        const div = document.createElement('div');
        const isCluster = cluster.items.length > 1;
        const primaryItem = cluster.items[0]!; // Use first item for styling

        div.className = `tech-hq-marker ${primaryItem.type} ${isCluster ? 'cluster' : ''}`;
        div.style.left = `${cluster.pos[0]}px`;
        div.style.top = `${cluster.pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'tech-hq-icon';

        if (isCluster) {
          // Show count for clusters
          const unicornCount = cluster.items.filter(h => h.type === 'unicorn').length;
          const faangCount = cluster.items.filter(h => h.type === 'faang').length;
          icon.textContent = faangCount > 0 ? '🏛️' : unicornCount > 0 ? '🦄' : '🏢';

          const badge = document.createElement('div');
          badge.className = 'cluster-badge';
          badge.textContent = String(cluster.items.length);
          div.appendChild(badge);

          div.title = cluster.items.map(h => h.company).join(', ');
        } else {
          icon.textContent = primaryItem.type === 'faang' ? '🏛️' : primaryItem.type === 'unicorn' ? '🦄' : '🏢';
        }
        div.appendChild(icon);

        // Show label at higher zoom or for single FAANG markers
        if (!isCluster && (this.state.zoom >= 3 || primaryItem.type === 'faang')) {
          const label = document.createElement('div');
          label.className = 'tech-hq-label';
          label.textContent = primaryItem.company;
          div.appendChild(label);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          if (isCluster) {
            // Show cluster popup with list of companies
            this.popup.show({
              type: 'techHQCluster',
              data: { items: cluster.items, city: primaryItem.city, country: primaryItem.country },
              x: e.clientX - rect.left,
              y: e.clientY - rect.top,
            });
          } else {
            this.popup.show({
              type: 'techHQ',
              data: primaryItem,
              x: e.clientX - rect.left,
              y: e.clientY - rect.top,
            });
          }
        });

        this.appendOverlay(div);
      });
    }

    // Accelerators (🎯 icons)
    if (this.state.layers.accelerators) {
      ACCELERATORS.forEach((acc) => {
        const pos = projection([acc.lon, acc.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `accelerator-marker ${acc.type}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'accelerator-icon';
        icon.textContent = acc.type === 'accelerator' ? '🎯' : acc.type === 'incubator' ? '🔬' : '🎨';
        div.appendChild(icon);

        if (this.state.zoom >= 3) {
          const label = document.createElement('div');
          label.className = 'accelerator-label';
          label.textContent = acc.name;
          div.appendChild(label);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'accelerator',
            data: acc,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Tech Events / Conferences (📅 icons) - with clustering
    if (this.state.layers.techEvents && this.techEvents.length > 0) {
      const mapWidth = this.container.clientWidth;
      const mapHeight = this.container.clientHeight;

      // Map events to have lon property for clustering, filter visible
      const visibleEvents = this.techEvents
        .map(e => ({ ...e, lon: e.lng }))
        .filter(e => {
          const pos = projection([e.lon, e.lat]);
          return pos && pos[0] >= 0 && pos[0] <= mapWidth && pos[1] >= 0 && pos[1] <= mapHeight;
        });

      const clusterRadius = this.state.zoom >= 4 ? 15 : this.state.zoom >= 3 ? 25 : 40;
      // Group by location to prevent clustering events from different cities
      const clusters = this.clusterMarkers(visibleEvents, projection, clusterRadius, e => e.location);

      clusters.forEach((cluster) => {
        if (cluster.items.length === 0) return;
        const div = document.createElement('div');
        const isCluster = cluster.items.length > 1;
        const primaryEvent = cluster.items[0]!;
        const hasUpcomingSoon = cluster.items.some(e => e.daysUntil <= 14);

        div.className = `tech-event-marker ${hasUpcomingSoon ? 'upcoming-soon' : ''} ${isCluster ? 'cluster' : ''}`;
        div.style.left = `${cluster.pos[0]}px`;
        div.style.top = `${cluster.pos[1]}px`;

        if (isCluster) {
          const badge = document.createElement('div');
          badge.className = 'cluster-badge';
          badge.textContent = String(cluster.items.length);
          div.appendChild(badge);
          div.title = cluster.items.map(e => e.title).join(', ');
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          if (isCluster) {
            this.popup.show({
              type: 'techEventCluster',
              data: { items: cluster.items, location: primaryEvent.location, country: primaryEvent.country },
              x: e.clientX - rect.left,
              y: e.clientY - rect.top,
            });
          } else {
            this.popup.show({
              type: 'techEvent',
              data: primaryEvent,
              x: e.clientX - rect.left,
              y: e.clientY - rect.top,
            });
          }
        });

        this.appendOverlay(div);
      });
    }

    // Stock Exchanges (🏛️ icon by tier)
    if (this.state.layers.stockExchanges) {
      STOCK_EXCHANGES.forEach((exchange) => {
        const pos = projection([exchange.lon, exchange.lat]);
        if (!pos || !Number.isFinite(pos[0]) || !Number.isFinite(pos[1])) return;

        const icon = exchange.tier === 'mega' ? '🏛️' : exchange.tier === 'major' ? '📊' : '📈';
        const div = document.createElement('div');
        div.className = `map-marker exchange-marker tier-${exchange.tier}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.style.zIndex = exchange.tier === 'mega' ? '50' : '40';
        div.textContent = icon;
        div.title = `${exchange.shortName} (${exchange.city})`;

        if ((this.state.zoom >= 2 && exchange.tier === 'mega') || this.state.zoom >= 3) {
          const label = document.createElement('span');
          label.className = 'marker-label';
          label.textContent = exchange.shortName;
          div.appendChild(label);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'stockExchange',
            data: exchange,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Financial Centers (💰 icon by type)
    if (this.state.layers.financialCenters) {
      FINANCIAL_CENTERS.forEach((center) => {
        const pos = projection([center.lon, center.lat]);
        if (!pos || !Number.isFinite(pos[0]) || !Number.isFinite(pos[1])) return;

        const icon = center.type === 'global' ? '💰' : center.type === 'regional' ? '🏦' : '🏝️';
        const div = document.createElement('div');
        div.className = `map-marker financial-center-marker type-${center.type}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.style.zIndex = center.type === 'global' ? '45' : '35';
        div.textContent = icon;
        div.title = `${center.name} Financial Center`;

        if ((this.state.zoom >= 2 && center.type === 'global') || this.state.zoom >= 3) {
          const label = document.createElement('span');
          label.className = 'marker-label';
          label.textContent = center.name;
          div.appendChild(label);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'financialCenter',
            data: center,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Central Banks (🏛️ icon by type)
    if (this.state.layers.centralBanks) {
      CENTRAL_BANKS.forEach((bank) => {
        const pos = projection([bank.lon, bank.lat]);
        if (!pos || !Number.isFinite(pos[0]) || !Number.isFinite(pos[1])) return;

        const icon = bank.type === 'supranational' ? '🌐' : bank.type === 'major' ? '🏛️' : '🏦';
        const div = document.createElement('div');
        div.className = `map-marker central-bank-marker type-${bank.type}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.style.zIndex = bank.type === 'supranational' ? '48' : bank.type === 'major' ? '42' : '38';
        div.textContent = icon;
        div.title = `${bank.shortName} - ${bank.name}`;

        if ((this.state.zoom >= 2 && (bank.type === 'major' || bank.type === 'supranational')) || this.state.zoom >= 3) {
          const label = document.createElement('span');
          label.className = 'marker-label';
          label.textContent = bank.shortName;
          div.appendChild(label);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'centralBank',
            data: bank,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Commodity Hubs (⛽ icon by type)
    if (this.state.layers.commodityHubs) {
      COMMODITY_HUBS.forEach((hub) => {
        const pos = projection([hub.lon, hub.lat]);
        if (!pos || !Number.isFinite(pos[0]) || !Number.isFinite(pos[1])) return;

        const icon = hub.type === 'exchange' ? '📦' : hub.type === 'port' ? '🚢' : '⛽';
        const div = document.createElement('div');
        div.className = `map-marker commodity-hub-marker type-${hub.type}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.style.zIndex = '38';
        div.textContent = icon;
        div.title = `${hub.name} (${hub.city})`;

        if (this.state.zoom >= 3) {
          const label = document.createElement('span');
          label.className = 'marker-label';
          label.textContent = hub.name;
          div.appendChild(label);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'commodityHub',
            data: hub,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Tech Hub Activity Markers (shows activity heatmap for tech hubs with news activity)
    if (SITE_VARIANT === 'tech' && this.techActivities.length > 0) {
      this.techActivities.forEach((activity) => {
        const pos = projection([activity.lon, activity.lat]);
        if (!pos) return;

        // Only show markers for hubs with actual activity
        if (activity.newsCount === 0) return;

        const div = document.createElement('div');
        div.className = `tech-activity-marker ${activity.activityLevel}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.style.zIndex = activity.activityLevel === 'high' ? '60' : activity.activityLevel === 'elevated' ? '50' : '40';
        div.title = `${activity.city}: ${activity.newsCount} stories`;

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          this.onTechHubClick?.(activity);
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'techActivity',
            data: activity,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);

        // Add label for high/elevated activity hubs at sufficient zoom
        if ((activity.activityLevel === 'high' || (activity.activityLevel === 'elevated' && this.state.zoom >= 2)) && this.state.zoom >= 1.5) {
          const label = document.createElement('div');
          label.className = 'tech-activity-label';
          label.textContent = activity.city;
          label.style.left = `${pos[0]}px`;
          label.style.top = `${pos[1] + 14}px`;
          this.appendOverlay(label);
        }
      });
    }

    // Geo Hub Activity Markers (shows activity heatmap for geopolitical hubs - full variant)
    if (SITE_VARIANT === 'full' && this.geoActivities.length > 0) {
      this.geoActivities.forEach((activity) => {
        const pos = projection([activity.lon, activity.lat]);
        if (!pos) return;

        // Only show markers for hubs with actual activity
        if (activity.newsCount === 0) return;

        const div = document.createElement('div');
        div.className = `geo-activity-marker ${activity.activityLevel}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;
        div.style.zIndex = activity.activityLevel === 'high' ? '60' : activity.activityLevel === 'elevated' ? '50' : '40';
        div.title = `${activity.name}: ${activity.newsCount} stories`;

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          this.onGeoHubClick?.(activity);
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'geoActivity',
            data: activity,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Protests / Social Unrest Events (severity colors + icons) - with clustering
    // Filter to show only significant events on map (all events still used for CII analysis)
    if (this.state.layers.protests) {
      const significantProtests = this.protests.filter((event) => {
        // Only show riots and high severity (red markers)
        // All protests still counted in CII analysis
        return event.eventType === 'riot' || event.severity === 'high';
      });

      const clusterRadius = this.state.zoom >= 4 ? 12 : this.state.zoom >= 3 ? 20 : 35;
      const clusters = this.clusterMarkers(significantProtests, projection, clusterRadius, p => p.country);

      clusters.forEach((cluster) => {
        if (cluster.items.length === 0) return;
        const div = document.createElement('div');
        const isCluster = cluster.items.length > 1;
        const primaryEvent = cluster.items[0]!;
        const hasRiot = cluster.items.some(e => e.eventType === 'riot');
        const hasHighSeverity = cluster.items.some(e => e.severity === 'high');

        div.className = `protest-marker ${hasHighSeverity ? 'high' : primaryEvent.severity} ${hasRiot ? 'riot' : primaryEvent.eventType} ${isCluster ? 'cluster' : ''}`;
        div.style.left = `${cluster.pos[0]}px`;
        div.style.top = `${cluster.pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'protest-icon';
        icon.textContent = hasRiot ? '🔥' : primaryEvent.eventType === 'strike' ? '✊' : '📢';
        div.appendChild(icon);

        if (isCluster) {
          const badge = document.createElement('div');
          badge.className = 'cluster-badge';
          badge.textContent = String(cluster.items.length);
          div.appendChild(badge);
          div.title = `${primaryEvent.country}: ${cluster.items.length} ${t('popups.events')}`;
        } else {
          div.title = `${primaryEvent.city || primaryEvent.country} - ${primaryEvent.eventType} (${primaryEvent.severity})`;
          if (primaryEvent.validated) {
            div.classList.add('validated');
          }
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          if (isCluster) {
            this.popup.show({
              type: 'protestCluster',
              data: { items: cluster.items, country: primaryEvent.country },
              x: e.clientX - rect.left,
              y: e.clientY - rect.top,
            });
          } else {
            this.popup.show({
              type: 'protest',
              data: primaryEvent,
              x: e.clientX - rect.left,
              y: e.clientY - rect.top,
            });
          }
        });

        this.appendOverlay(div);
      });
    }

    // Flight Delays (delay severity colors + ✈️ icons)
    if (this.state.layers.flights) {
      this.flightDelays.forEach((delay) => {
        const pos = projection([delay.lon, delay.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `flight-delay-marker ${delay.severity}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'flight-delay-icon';
        // #3707: 'unknown' = no telemetry. Use ❔ glyph (consistent with MapPopup)
        // so users don't see the healthy ✈️ for uncovered airports.
        icon.textContent = delay.severity === 'unknown' ? '❔'
          : delay.delayType === 'ground_stop' ? '🛑'
          : delay.severity === 'severe' ? '✈️'
          : '🛫';
        div.appendChild(icon);

        if (this.state.zoom >= 3) {
          const label = document.createElement('div');
          label.className = 'flight-delay-label';
          label.textContent = `${delay.iata} ${delay.avgDelayMinutes > 0 ? `+${delay.avgDelayMinutes}m` : ''}`;
          div.appendChild(label);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'flight',
            data: delay,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Aircraft positions (simplified dots in SVG fallback, limited to 200)
    if (this.state.layers.flights) {
      this.aircraftPositions.slice(0, 200).forEach((ac) => {
        const pt = projection([ac.lon, ac.lat]);
        if (!pt) return;

        const div = document.createElement('div');
        div.className = 'aircraft-marker';
        div.style.position = 'absolute';
        div.style.left = `${pt[0]}px`;
        div.style.top = `${pt[1]}px`;
        div.style.transform = `rotate(${ac.trackDeg}deg)`;
        div.style.fontSize = '12px';
        div.style.color = ac.onGround ? '#888' : '#a064ff';
        div.style.lineHeight = '1';
        div.style.pointerEvents = 'auto';
        div.style.cursor = 'pointer';
        div.textContent = '\u25B2'; // ▲

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'aircraft',
            data: ac,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Military Tracking (flights and vessels)
    if (this.state.layers.military) {
      // Render individual flights
      this.militaryFlights.forEach((flight) => {
        const pos = projection([flight.lon, flight.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `military-flight-marker ${flight.operator} ${flight.aircraftType}${flight.isInteresting ? ' interesting' : ''}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        // Crosshair icon - rotates with heading
        const icon = document.createElement('div');
        icon.className = `military-flight-icon ${flight.aircraftType}`;
        icon.style.transform = `rotate(${flight.heading}deg)`;
        // CSS handles the crosshair rendering
        div.appendChild(icon);

        // Show callsign at higher zoom levels
        if (this.state.zoom >= 3) {
          const label = document.createElement('div');
          label.className = 'military-flight-label';
          label.textContent = flight.callsign;
          div.appendChild(label);
        }

        // Show altitude indicator
        if (flight.altitude > 0) {
          const alt = document.createElement('div');
          alt.className = 'military-flight-altitude';
          alt.textContent = `FL${Math.round(flight.altitude / 100)}`;
          div.appendChild(alt);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'militaryFlight',
            data: flight,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);

        // Render flight track if available
        if (flight.track && flight.track.length > 1 && this.state.zoom >= 2) {
          const trackLine = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
          const points = flight.track
            .map((p) => {
              const pt = projection([p[1], p[0]]);
              return pt ? `${pt[0]},${pt[1]}` : null;
            })
            .filter(Boolean)
            .join(' ');

          if (points) {
            trackLine.setAttribute('points', points);
            trackLine.setAttribute('class', `military-flight-track ${flight.operator}`);
            trackLine.setAttribute('fill', 'none');
            trackLine.setAttribute('stroke-width', '1.5');
            trackLine.setAttribute('stroke-dasharray', '4,2');
            this.dynamicLayerGroup?.select('.overlays-svg').append(() => trackLine);
          }
        }
      });

      // Render flight clusters
      this.militaryFlightClusters.forEach((cluster) => {
        const pos = projection([cluster.lon, cluster.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `military-cluster-marker flight-cluster ${cluster.activityType || 'unknown'}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const count = document.createElement('div');
        count.className = 'cluster-count';
        count.textContent = String(cluster.flightCount);
        div.appendChild(count);

        const label = document.createElement('div');
        label.className = 'cluster-label';
        label.textContent = cluster.name;
        div.appendChild(label);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'militaryFlightCluster',
            data: cluster,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });

      // Military Vessels (warships, carriers, submarines)
      // Render individual vessels
      this.militaryVessels.forEach((vessel) => {
        const pos = projection([vessel.lon, vessel.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `military-vessel-marker ${vessel.operator} ${vessel.vesselType}${vessel.isDark ? ' dark-vessel' : ''}${vessel.isInteresting ? ' interesting' : ''}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = `military-vessel-icon ${vessel.vesselType}`;
        icon.style.transform = `rotate(${vessel.heading}deg)`;
        // CSS handles the diamond/anchor rendering
        div.appendChild(icon);

        // Dark vessel warning indicator
        if (vessel.isDark) {
          const darkIndicator = document.createElement('div');
          darkIndicator.className = 'dark-vessel-indicator';
          darkIndicator.textContent = '⚠️';
          darkIndicator.title = 'AIS Signal Lost';
          div.appendChild(darkIndicator);
        }

        // Show vessel name at higher zoom
        if (this.state.zoom >= 3) {
          const label = document.createElement('div');
          label.className = 'military-vessel-label';
          label.textContent = vessel.name;
          div.appendChild(label);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'militaryVessel',
            data: vessel,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);

        // Render vessel track if available
        if (vessel.track && vessel.track.length > 1 && this.state.zoom >= 2) {
          const trackLine = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
          const points = vessel.track
            .map((p) => {
              const pt = projection([p[1], p[0]]);
              return pt ? `${pt[0]},${pt[1]}` : null;
            })
            .filter(Boolean)
            .join(' ');

          if (points) {
            trackLine.setAttribute('points', points);
            trackLine.setAttribute('class', `military-vessel-track ${vessel.operator}`);
            trackLine.setAttribute('fill', 'none');
            trackLine.setAttribute('stroke-width', '2');
            this.dynamicLayerGroup?.select('.overlays-svg').append(() => trackLine);
          }
        }
      });

      // Render vessel clusters
      this.militaryVesselClusters.forEach((cluster) => {
        const pos = projection([cluster.lon, cluster.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `military-cluster-marker vessel-cluster ${cluster.activityType || 'unknown'}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const count = document.createElement('div');
        count.className = 'cluster-count';
        count.textContent = String(cluster.vesselCount);
        div.appendChild(count);

        const label = document.createElement('div');
        label.className = 'cluster-label';
        label.textContent = cluster.name;
        div.appendChild(label);

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'militaryVesselCluster',
            data: cluster,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Natural Events (NASA EONET) - part of NATURAL layer
    if (this.state.layers.natural) {
      this.naturalEvents.forEach((event) => {
        const pos = projection([event.lon, event.lat]);
        if (!pos) return;

        const div = document.createElement('div');
        div.className = `nat-event-marker ${event.category}`;
        div.style.left = `${pos[0]}px`;
        div.style.top = `${pos[1]}px`;

        const icon = document.createElement('div');
        icon.className = 'nat-event-icon';
        icon.textContent = getNaturalEventIcon(event.category);
        div.appendChild(icon);

        if (this.state.zoom >= 2) {
          const label = document.createElement('div');
          label.className = 'nat-event-label';
          label.textContent = event.title.length > 25 ? event.title.slice(0, 25) + '…' : event.title;
          div.appendChild(label);
        }

        if (event.magnitude) {
          const mag = document.createElement('div');
          mag.className = 'nat-event-magnitude';
          mag.textContent = `${event.magnitude}${event.magnitudeUnit ? ` ${event.magnitudeUnit}` : ''}`;
          div.appendChild(mag);
        }

        div.addEventListener('click', (e) => {
          e.stopPropagation();
          const rect = this.container.getBoundingClientRect();
          this.popup.show({
            type: 'natEvent',
            data: event,
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          });
        });

        this.appendOverlay(div);
      });
    }

    // Satellite Fires (NASA FIRMS) - separate fires layer
    if (this.state.layers.fires) {
      this.firmsFireData.forEach((fire) => {
        const pos = projection([fire.lon, fire.lat]);
        if (!pos) return;

        const color = fire.brightness > 400 ? getCSSColor('--semantic-critical') : fire.brightness > 350 ? getCSSColor('--semantic-high') : getCSSColor('--semantic-elevated');
        const size = Math.max(4, Math.min(10, (fire.frp || 1) * 0.5));

        const dot = document.createElement('div');
        dot.className = 'fire-dot';
        dot.style.left = `${pos[0]}px`;
        dot.style.top = `${pos[1]}px`;
        dot.style.width = `${size}px`;
        dot.style.height = `${size}px`;
        dot.style.backgroundColor = color;
        dot.title = `${fire.region} — ${Math.round(fire.brightness)}K, ${fire.frp}MW`;

        this.appendOverlay(dot);
      });
    }

    // Webcam markers (colored circles, gated by zoom >= 2)
    if (this.state.layers.webcams && this.webcamData.length > 0 && this.state.zoom >= 2) {
      const CATEGORY_COLORS: Record<string, string> = {
        traffic: '#ffd700', city: '#00d4ff', landscape: '#45b7d1',
        nature: '#96ceb4', beach: '#f4a460', water: '#4169e1', other: '#888888',
      };
      this.webcamData.forEach((cam) => {
        const pos = projection([cam.lng, cam.lat]);
        if (!pos || !Number.isFinite(pos[0]) || !Number.isFinite(pos[1])) return;
        const isCluster = 'count' in cam;
        const radius = isCluster ? Math.min(4 + Math.sqrt((cam as WebcamCluster).count), 12) : 3;
        const size = radius * 2;
        const color = isCluster ? '#00d4ff' : (CATEGORY_COLORS[(cam as WebcamEntry).category] ?? '#888888');
        const dot = document.createElement('div');
        dot.className = 'webcam-dot';
        dot.style.left = `${pos[0]}px`;
        dot.style.top = `${pos[1]}px`;
        dot.style.width = `${size}px`;
        dot.style.height = `${size}px`;
        dot.style.position = 'absolute';
        dot.style.borderRadius = '50%';
        dot.style.backgroundColor = color;
        dot.style.opacity = '0.75';
        dot.style.cursor = 'pointer';
        dot.title = isCluster ? `${(cam as WebcamCluster).count} webcams` : ((cam as WebcamEntry).title || 'Webcam');
        dot.addEventListener('click', (e) => {
          e.stopPropagation();
          if (isCluster) {
            this.showWebcamClusterPopup(cam as WebcamCluster, e.clientX, e.clientY);
          } else {
            this.showWebcamTooltip(cam as WebcamEntry, e.clientX, e.clientY);
          }
        });
        this.appendOverlay(dot);
      });
    }

    } finally {
      this.overlayAppendTarget = previousTarget;
      this.overlays.appendChild(fragment);
    }
  }

  private renderConflictEventMarkers(projection: d3.GeoProjection): void {
    const visibleEvents = this.state.timeRange === 'all'
      ? this.conflictEvents
      : this.conflictEvents.filter((event) => event.occurredAt >= Date.now() - this.getTimeRangeMs());
    const clusters = this.clusterMarkers(
      visibleEvents
        .map((event) => ({
          event,
          lat: event.location?.latitude ?? Number.NaN,
          lon: event.location?.longitude ?? Number.NaN,
        }))
        .filter((item) => Number.isFinite(item.lat) && Number.isFinite(item.lon)),
      projection,
      this.state.zoom >= 4 ? 10 : this.state.zoom >= 3 ? 16 : 28,
      (item) => item.event.country,
    );

    clusters.forEach((cluster) => {
      if (cluster.items.length === 0) return;
      const primary = cluster.items[0]!.event;
      const fatalities = cluster.items.reduce((sum, item) => sum + (item.event.fatalities || 0), 0);
      const isCluster = cluster.items.length > 1;
      const div = document.createElement('div');
      div.className = `conflict-event-marker${fatalities > 0 ? ' fatal' : ''}${isCluster ? ' cluster' : ''}`;
      div.style.left = `${cluster.pos[0]}px`;
      div.style.top = `${cluster.pos[1]}px`;
      div.title = isCluster
        ? `${primary.country}: ${cluster.items.length} conflict events${fatalities > 0 ? `, ${fatalities} fatalities` : ''}`
        : `${primary.country}${primary.admin1 ? `, ${primary.admin1}` : ''}: ${primary.eventType}${primary.fatalities > 0 ? `, ${primary.fatalities} fatalities` : ''}`;

      if (isCluster) {
        const badge = document.createElement('span');
        badge.className = 'conflict-event-count';
        badge.textContent = String(cluster.items.length);
        div.appendChild(badge);
      }

      this.appendOverlay(div);
    });
  }

  private makeWebcamTooltipShell(): { tooltip: HTMLDivElement; closeBtn: HTMLButtonElement } {
    this.container.querySelector('.webcam-tooltip')?.remove();
    const tooltip = document.createElement('div');
    tooltip.className = 'webcam-tooltip';
    tooltip.style.cssText = [
      'position:absolute',
      'background:rgba(10,12,16,0.95)',
      'border:1px solid rgba(60,120,60,0.6)',
      'padding:8px 12px',
      'border-radius:3px',
      'font-size:11px',
      'font-family:var(--font-mono)',
      'color:#d4d4d4',
      'max-width:240px',
      'z-index:1000',
      'pointer-events:auto',
      'line-height:1.5',
    ].join(';');
    const closeBtn = document.createElement('button');
    closeBtn.style.cssText = 'position:absolute;top:4px;right:4px;background:none;border:none;color:#888;cursor:pointer;font-size:14px;line-height:1;padding:2px 4px;';
    closeBtn.setAttribute('aria-label', 'Close');
    closeBtn.textContent = '×';
    closeBtn.addEventListener('click', () => tooltip.remove());
    tooltip.appendChild(closeBtn);
    return { tooltip, closeBtn };
  }

  private placeWebcamTooltip(tooltip: HTMLElement, clientX: number, clientY: number): void {
    const rect = this.container.getBoundingClientRect();
    this.container.appendChild(tooltip);
    const x = Math.min(clientX - rect.left + 10, rect.width - 260);
    const y = Math.max(clientY - rect.top - 20, 4);
    tooltip.style.left = `${x}px`;
    tooltip.style.top = `${y}px`;
    let hideTimer: ReturnType<typeof setTimeout> | null = setTimeout(() => tooltip.remove(), 8000);
    tooltip.addEventListener('mouseenter', () => { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } });
    tooltip.addEventListener('mouseleave', () => { hideTimer = setTimeout(() => tooltip.remove(), 2000); });
  }

  private showWebcamTooltip(cam: WebcamEntry, clientX: number, clientY: number): void {
    const { tooltip } = this.makeWebcamTooltipShell();

    const title = document.createElement('div');
    title.style.cssText = 'font-weight:bold;color:#00d4ff;padding-right:18px;';
    title.textContent = `\u{1F4F7} ${cam.title || cam.category || 'Webcam'}`;
    tooltip.appendChild(title);

    const meta = document.createElement('div');
    meta.style.cssText = 'opacity:0.7;font-size:10px;margin-top:2px;';
    meta.textContent = [cam.country, cam.category].filter(Boolean).join(' \u00B7 ');
    if (meta.textContent) tooltip.appendChild(meta);

    const previewDiv = document.createElement('div');
    previewDiv.style.marginTop = '6px';
    const loadingSpan = document.createElement('span');
    loadingSpan.style.cssText = 'opacity:0.5;font-size:10px;';
    loadingSpan.textContent = 'Loading preview...';
    previewDiv.appendChild(loadingSpan);
    tooltip.appendChild(previewDiv);

    if (cam.webcamId) {
      const link = document.createElement('a');
      link.href = `https://www.windy.com/webcams/${cam.webcamId}`;
      link.target = '_blank';
      link.rel = 'noopener';
      link.style.cssText = 'display:block;margin-top:4px;color:#00d4ff;font-size:11px;text-decoration:none;';
      link.textContent = 'Open on Windy \u2197';
      tooltip.appendChild(link);
    }

    this.placeWebcamTooltip(tooltip, clientX, clientY);

    if (cam.webcamId) {
      import('@/services/webcams').then(({ fetchWebcamImage }) => {
        fetchWebcamImage(cam.webcamId).then(img => {
          if (!tooltip.isConnected) return;
          previewDiv.replaceChildren();
          if (img.thumbnailUrl) {
            const imgEl = document.createElement('img');
            imgEl.src = img.thumbnailUrl;
            imgEl.style.cssText = 'width:200px;border-radius:4px;margin-bottom:4px;';
            imgEl.loading = 'lazy';
            previewDiv.appendChild(imgEl);
          } else {
            const span = document.createElement('span');
            span.style.cssText = 'opacity:0.5;font-size:10px;';
            span.textContent = 'Preview unavailable';
            previewDiv.appendChild(span);
          }

          const pinBtn = document.createElement('button');
          pinBtn.className = 'webcam-pin-btn';
          const wcId = cam.webcamId;
          if (isPinned(wcId)) {
            pinBtn.classList.add('webcam-pin-btn--pinned');
            pinBtn.textContent = '\u{1F4CC} Pinned';
            pinBtn.disabled = true;
          } else {
            pinBtn.textContent = '\u{1F4CC} Pin';
            pinBtn.addEventListener('click', (e) => {
              e.stopPropagation();
              pinWebcam({
                webcamId: wcId,
                title: cam.title || img?.title || '',
                lat: cam.lat,
                lng: cam.lng,
                category: cam.category || 'other',
                country: cam.country || '',
                playerUrl: img?.playerUrl || '',
              });
              pinBtn.classList.add('webcam-pin-btn--pinned');
              pinBtn.textContent = '\u{1F4CC} Pinned';
              pinBtn.disabled = true;
            });
          }
          tooltip.appendChild(pinBtn);
        });
      });
    } else {
      previewDiv.remove();
    }
  }

  private showWebcamClusterPopup(cam: WebcamCluster, clientX: number, clientY: number): void {
    const { tooltip } = this.makeWebcamTooltipShell();

    const header = document.createElement('div');
    header.style.cssText = 'font-weight:bold;color:#00d4ff;padding-right:18px;';
    header.textContent = `\u{1F4F7} ${cam.count} webcams — loading...`;
    tooltip.appendChild(header);

    this.placeWebcamTooltip(tooltip, clientX, clientY);

    const currentZoom = this.state.zoom ?? 3;
    import('@/services/webcams').then(({ fetchWebcams, getClusterCellSize }) => {
      const margin = Math.max(0.5, getClusterCellSize(currentZoom));
      fetchWebcams(10, {
        w: cam.lng - margin, s: cam.lat - margin,
        e: cam.lng + margin, n: cam.lat + margin,
      }).then(result => {
        if (!tooltip.isConnected) return;
        const webcams = result.webcams.slice(0, 20);
        header.textContent = `\u{1F4F7} ${webcams.length} webcams`;

        const list = document.createElement('div');
        list.style.cssText = 'max-height:200px;overflow-y:auto;margin-top:6px;';
        for (const webcam of webcams) {
          const item = document.createElement('div');
          item.style.cssText = 'padding:3px 2px;cursor:pointer;color:#aaa;border-bottom:1px solid rgba(255,255,255,0.08);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
          const nameSpan = document.createElement('span');
          nameSpan.textContent = webcam.title || webcam.category || 'Webcam';
          item.appendChild(nameSpan);
          if (webcam.country) {
            const cc = document.createElement('span');
            cc.style.cssText = 'float:right;opacity:0.4;font-size:10px;margin-left:6px;';
            cc.textContent = webcam.country;
            item.appendChild(cc);
          }
          item.addEventListener('mouseenter', () => { item.style.color = '#00d4ff'; });
          item.addEventListener('mouseleave', () => { item.style.color = '#aaa'; });
          item.addEventListener('click', (e) => {
            e.stopPropagation();
            this.showWebcamTooltip(webcam, e.clientX, e.clientY);
          });
          list.appendChild(item);
        }
        tooltip.appendChild(list);
      }).catch(() => {
        if (!tooltip.isConnected) return;
        header.textContent = '\u{1F4F7} Failed to load webcam list';
      });
    });
  }

  private renderWaterways(projection: d3.GeoProjection): void {
    STRATEGIC_WATERWAYS.forEach((waterway) => {
      const pos = projection([waterway.lon, waterway.lat]);
      if (!pos) return;

      const div = document.createElement('div');
      div.className = 'waterway-marker';
      div.style.left = `${pos[0]}px`;
      div.style.top = `${pos[1]}px`;
      div.title = waterway.name;

      const diamond = document.createElement('div');
      diamond.className = 'waterway-diamond';
      div.appendChild(diamond);

      div.addEventListener('click', (e) => {
        e.stopPropagation();
        const rect = this.container.getBoundingClientRect();
        this.popup.show({
          type: 'waterway',
          data: waterway,
          x: e.clientX - rect.left,
          y: e.clientY - rect.top,
        });
      });

      this.appendOverlay(div);
    });
  }

  private renderAisDisruptions(projection: d3.GeoProjection): void {
    this.aisDisruptions.forEach((event) => {
      const pos = projection([event.lon, event.lat]);
      if (!pos) return;

      const div = document.createElement('div');
      div.className = `ais-disruption-marker ${event.severity} ${event.type}`;
      div.style.left = `${pos[0]}px`;
      div.style.top = `${pos[1]}px`;

      const icon = document.createElement('div');
      icon.className = 'ais-disruption-icon';
      icon.textContent = event.type === 'gap_spike' ? '🛰️' : '🚢';
      div.appendChild(icon);

      const label = document.createElement('div');
      label.className = 'ais-disruption-label';
      label.textContent = event.name;
      div.appendChild(label);

      div.addEventListener('click', (e) => {
        e.stopPropagation();
        const rect = this.container.getBoundingClientRect();
        this.popup.show({
          type: 'ais',
          data: event,
          x: e.clientX - rect.left,
          y: e.clientY - rect.top,
        });
      });

      this.appendOverlay(div);
    });
  }

  private renderAisDensity(projection: d3.GeoProjection): void {
    if (!this.dynamicLayerGroup) return;
    const densityGroup = this.dynamicLayerGroup.append('g').attr('class', 'ais-density');

    this.aisDensity.forEach((zone) => {
      const pos = projection([zone.lon, zone.lat]);
      if (!pos) return;

      const intensity = Math.min(Math.max(zone.intensity, 0.15), 1);
      const radius = 4 + intensity * 8;  // Small dots (4-12px)
      const isCongested = zone.deltaPct >= 15;
      const color = isCongested ? getCSSColor('--semantic-elevated') : getCSSColor('--semantic-info');
      const fillOpacity = 0.15 + intensity * 0.25;  // More visible individual dots

      densityGroup
        .append('circle')
        .attr('class', 'ais-density-spot')
        .attr('cx', pos[0])
        .attr('cy', pos[1])
        .attr('r', radius)
        .attr('fill', color)
        .attr('fill-opacity', fillOpacity)
        .attr('stroke', 'none');
    });
  }

  private renderPorts(projection: d3.GeoProjection): void {
    PORTS.forEach((port) => {
      const pos = projection([port.lon, port.lat]);
      if (!pos) return;

      const div = document.createElement('div');
      div.className = `port-marker port-${port.type}`;
      div.style.left = `${pos[0]}px`;
      div.style.top = `${pos[1]}px`;

      const icon = document.createElement('div');
      icon.className = 'port-icon';
      icon.textContent = port.type === 'naval' ? '⚓' : port.type === 'oil' || port.type === 'lng' ? '🛢️' : '🏭';
      div.appendChild(icon);

      const label = document.createElement('div');
      label.className = 'port-label';
      label.textContent = port.name;
      div.appendChild(label);

      div.addEventListener('click', (e) => {
        e.stopPropagation();
        const rect = this.container.getBoundingClientRect();
        this.popup.show({
          type: 'port',
          data: port,
          x: e.clientX - rect.left,
          y: e.clientY - rect.top,
        });
      });

      this.appendOverlay(div);
    });
  }

  private async loadAptGroups(): Promise<void> {
    const { APT_GROUPS } = await import('@/config/apt-groups');
    this.aptGroups = APT_GROUPS;
    this.aptGroupsLoaded = true;
    this.render();
  }

  private renderAPTMarkers(projection: d3.GeoProjection): void {
    this.aptGroups.forEach((apt) => {
      const pos = projection([apt.lon, apt.lat]);
      if (!pos) return;

      const div = document.createElement('div');
      div.className = 'apt-marker';
      div.style.left = `${pos[0]}px`;
      div.style.top = `${pos[1]}px`;
      setTrustedHtml(div, trustedHtml(`
        <div class="apt-icon">⚠</div>
        <div class="apt-label">${escapeHtml(apt.name)}</div>
      `, "legacy direct innerHTML migration"));

      div.addEventListener('click', (e) => {
        e.stopPropagation();
        const rect = this.container.getBoundingClientRect();
        this.popup.show({
          type: 'apt',
          data: apt,
          x: e.clientX - rect.left,
          y: e.clientY - rect.top,
        });
      });

      this.appendOverlay(div);
    });
  }

  private getRelatedNews(hotspot: Hotspot): NewsItem[] {
    const conflictTopics = ['gaza', 'ukraine', 'ukrainian', 'russia', 'russian', 'israel', 'israeli', 'iran', 'iranian', 'china', 'chinese', 'taiwan', 'taiwanese', 'korea', 'korean', 'syria', 'syrian'];

    return this.news
      .map((item) => {
        const tokens = tokenizeForMatch(item.title);
        const matchedKeywords = findMatchingKeywords(tokens, hotspot.keywords);

        if (matchedKeywords.length === 0) return null;

        const conflictMatches = conflictTopics.filter(t =>
          matchKeyword(tokens, t) && !hotspot.keywords.some(k => k.toLowerCase().includes(t))
        );

        if (conflictMatches.length > 0) {
          const strongLocalMatch = matchedKeywords.some(kw =>
            kw.toLowerCase() === hotspot.name.toLowerCase() ||
            hotspot.agencies?.some(a => matchKeyword(tokens, a))
          );
          if (!strongLocalMatch) return null;
        }

        const score = matchedKeywords.length;
        return { item, score };
      })
      .filter((x): x is { item: NewsItem; score: number } => x !== null)
      .sort((a, b) => b.score - a.score)
      .slice(0, 5)
      .map(x => x.item);
  }

  public updateHotspotActivity(news: NewsItem[]): void {
    this.news = news; // Store for related news lookup

    this.hotspots.forEach((spot) => {
      let score = 0;
      let hasBreaking = false;
      let matchedCount = 0;

      news.forEach((item) => {
        const tokens = tokenizeForMatch(item.title);
        const matches = spot.keywords.filter((kw) => matchKeyword(tokens, kw));

        if (matches.length > 0) {
          matchedCount++;
          // Base score per match
          score += matches.length * 2;

          // Breaking news is critical
          if (item.isAlert) {
            score += 5;
            hasBreaking = true;
          }

          // Recent news (last 6 hours) weighted higher
          if (item.pubDate) {
            const hoursAgo = (Date.now() - item.pubDate.getTime()) / (1000 * 60 * 60);
            if (hoursAgo < 1) score += 3; // Last hour
            else if (hoursAgo < 6) score += 2; // Last 6 hours
            else if (hoursAgo < 24) score += 1; // Last day
          }
        }
      });

      spot.hasBreaking = hasBreaking;

      // Dynamic level calculation - sensitive to real activity
      // HIGH: Breaking news OR 4+ matching articles OR score >= 10
      // ELEVATED: 2+ matching articles OR score >= 4
      // LOW: Default when no significant activity
      if (hasBreaking || matchedCount >= 4 || score >= 10) {
        spot.level = 'high';
        spot.status = hasBreaking ? 'BREAKING NEWS' : 'High activity';
      } else if (matchedCount >= 2 || score >= 4) {
        spot.level = 'elevated';
        spot.status = 'Elevated activity';
      } else if (matchedCount >= 1) {
        spot.level = 'low';
        spot.status = 'Recent mentions';
      } else {
        spot.level = 'low';
        spot.status = 'Monitoring';
      }

      // Update dynamic escalation score
      const velocity = matchedCount > 0 ? score / matchedCount : 0;
      updateHotspotEscalation(spot.id, matchedCount, hasBreaking, velocity);
    });

    this.render();
  }

  public flashLocation(lat: number, lon: number, durationMs = 2000): void {
    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    if (!width || !height) return;

    const projection = this.getProjection(width, height);
    const pos = projection([lon, lat]);
    if (!pos) return;

    const flash = document.createElement('div');
    flash.className = 'map-flash';
    flash.style.left = `${pos[0]}px`;
    flash.style.top = `${pos[1]}px`;
    flash.style.setProperty('--flash-duration', `${durationMs}ms`);
    this.appendOverlay(flash);

    window.setTimeout(() => {
      flash.remove();
    }, durationMs);
  }

  public initEscalationGetters(): void {
    setCIIGetter((code) => getCachedCountryScoreValue(code) ?? getCountryScore(code));
    setGeoAlertGetter(getAlertsNearLocation);
  }

  public updateMilitaryForEscalation(flights: MilitaryFlight[], vessels: MilitaryVessel[]): void {
    setMilitaryData(flights, vessels);
  }

  public getHotspotDynamicScore(hotspotId: string) {
    return getHotspotEscalation(hotspotId);
  }

  public setView(view: MapView, zoom?: number): void {
    this.state.view = view;

    // Region-specific zoom and pan settings
    // Pan: +x = west, -x = east, +y = north, -y = south
    const viewSettings: Record<MapView, { zoom: number; pan: { x: number; y: number } }> = {
      global: { zoom: 1, pan: { x: 0, y: 0 } },
      america: { zoom: 1.8, pan: { x: 180, y: 30 } },
      mena: { zoom: 3.5, pan: { x: -100, y: 50 } },
      eu: { zoom: 2.4, pan: { x: -30, y: 100 } },
      asia: { zoom: 2.0, pan: { x: -320, y: 40 } },
      latam: { zoom: 2.0, pan: { x: 120, y: -100 } },
      africa: { zoom: 2.2, pan: { x: -40, y: -30 } },
      oceania: { zoom: 2.2, pan: { x: -420, y: -100 } },
    };

    const settings = viewSettings[view];
    this.state.zoom = zoom ?? settings.zoom;
    this.state.pan = settings.pan;
    this.applyTransform();
    this.render();
  }

  private static readonly ASYNC_DATA_LAYERS: Set<keyof MapLayers> = new Set([
    'natural', 'weather', 'outages', 'ais', 'protests', 'flights', 'military', 'techEvents',
  ]);

  public toggleLayer(layer: keyof MapLayers, source: 'user' | 'programmatic' = 'user'): void {
    console.log(`[Map.toggleLayer] ${layer}: ${this.state.layers[layer]} -> ${!this.state.layers[layer]}`);
    this.state.layers[layer] = !this.state.layers[layer];
    if (this.state.layers[layer]) {
      const thresholds = MapComponent.LAYER_ZOOM_THRESHOLDS[layer];
      if (thresholds && this.state.zoom < thresholds.minZoom) {
        this.layerZoomOverrides[layer] = true;
      } else {
        delete this.layerZoomOverrides[layer];
      }
    } else {
      delete this.layerZoomOverrides[layer];
    }

    const btn = this.container.querySelector(`[data-layer="${layer}"]`);
    const isEnabled = this.state.layers[layer];
    const isAsyncLayer = MapComponent.ASYNC_DATA_LAYERS.has(layer);

    if (isEnabled && isAsyncLayer) {
      // Async layers: start in loading state, will be set to active when data arrives
      btn?.classList.remove('active');
      btn?.classList.add('loading');
    } else {
      // Static layers or disabling: toggle active immediately
      btn?.classList.toggle('active', isEnabled);
      btn?.classList.remove('loading');
    }

    this.onLayerChange?.(layer, this.state.layers[layer], source);
    // Defer render to next frame to avoid blocking the click handler
    this.scheduleRender();
  }

  public setOnLayerChange(callback: (layer: keyof MapLayers, enabled: boolean, source: 'user' | 'programmatic') => void): void {
    this.onLayerChange = callback;
  }

  public hideLayerToggle(layer: keyof MapLayers): void {
    const btn = this.container.querySelector(`.layer-toggle[data-layer="${layer}"]`);
    if (btn) {
      (btn as HTMLElement).style.display = 'none';
    }
  }

  public setChokepointData(data: GetChokepointStatusResponse | null): void {
    this.popup.setChokepointData(data);
  }

  public setScenarioState(_state: ScenarioVisualState | null): void {
    // SVG renderer: scenario fill deferred (no iso2 data binding on country elements)
  }

  public setLayerLoading(layer: keyof MapLayers, loading: boolean): void {
    const btn = this.container.querySelector(`.layer-toggle[data-layer="${layer}"]`);
    if (btn) {
      btn.classList.toggle('loading', loading);
    }
  }

  public setLayerReady(layer: keyof MapLayers, hasData: boolean): void {
    const btn = this.container.querySelector(`.layer-toggle[data-layer="${layer}"]`);
    if (!btn) return;

    btn.classList.remove('loading');
    if (this.state.layers[layer] && hasData) {
      btn.classList.add('active');
    } else {
      btn.classList.remove('active');
    }
  }

  public onStateChanged(callback: (state: MapState) => void): void {
    this.onStateChange = callback;
  }

  public zoomIn(): void {
    this.state.zoom = Math.min(this.state.zoom + 0.5, 10);
    this.applyTransform();
    // The on-screen +/- controls are excluded by shouldIgnoreInteractionStart, so a
    // mobile user zooming only via buttons would never arm label thinning otherwise.
    this.resumeMobileLabelVisibility();
  }

  public zoomOut(): void {
    this.state.zoom = Math.max(this.state.zoom - 0.5, 1);
    this.applyTransform();
    this.resumeMobileLabelVisibility();
  }

  public reset(): void {
    this.state.zoom = 1;
    this.state.pan = { x: 0, y: 0 };
    if (this.state.view !== 'global') {
      this.state.view = 'global';
      this.render();
    } else {
      this.applyTransform();
    }
    this.resumeMobileLabelVisibility();
  }

  public triggerHotspotClick(id: string): void {
    const hotspot = this.hotspots.find(h => h.id === id);
    if (!hotspot) return;

    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    const pos = projection([hotspot.lon, hotspot.lat]);
    if (!pos) return;

    const relatedNews = this.getRelatedNews(hotspot);
    this.popup.show({
      type: 'hotspot',
      data: hotspot,
      relatedNews,
      x: pos[0],
      y: pos[1],
    });
    this.popup.loadHotspotGdeltContext(hotspot);
    this.onHotspotClick?.(hotspot);
  }

  public triggerConflictClick(id: string): void {
    const conflict = CONFLICT_ZONES.find(c => c.id === id);
    if (!conflict) return;

    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    const pos = projection(conflict.center as [number, number]);
    if (!pos) return;

    this.popup.show({
      type: 'conflict',
      data: conflict,
      x: pos[0],
      y: pos[1],
    });
    this.popup.loadConflictHistory(conflict);
  }

  public triggerBaseClick(id: string): void {
    const base = getCachedMilitaryBases().find(b => b.id === id);
    if (!base) {
      void preloadMilitaryBases()
        .then(() => {
          if (!this.destroyed) this.triggerBaseClick(id);
        })
        .catch((error) => {
          console.warn('[Map] Military base config unavailable:', error);
        });
      return;
    }

    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    const pos = projection([base.lon, base.lat]);
    if (!pos) return;

    this.popup.show({
      type: 'base',
      data: base,
      x: pos[0],
      y: pos[1],
    });
  }

  public triggerPipelineClick(id: string): void {
    const pipeline = PIPELINES.find(p => p.id === id);
    if (!pipeline || pipeline.points.length === 0) return;

    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    const midPoint = pipeline.points[Math.floor(pipeline.points.length / 2)] as [number, number];
    const pos = projection(midPoint);
    if (!pos) return;

    this.popup.show({
      type: 'pipeline',
      data: pipeline,
      x: pos[0],
      y: pos[1],
    });
  }

  public triggerCableClick(id: string): void {
    const cable = UNDERSEA_CABLES.find(c => c.id === id);
    if (!cable || cable.points.length === 0) return;

    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    const midPoint = cable.points[Math.floor(cable.points.length / 2)] as [number, number];
    const pos = projection(midPoint);
    if (!pos) return;

    this.popup.show({
      type: 'cable',
      data: cable,
      x: pos[0],
      y: pos[1],
    });
  }

  public triggerDatacenterClick(id: string): void {
    const dc = AI_DATA_CENTERS.find(d => d.id === id);
    if (!dc) return;

    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    const pos = projection([dc.lon, dc.lat]);
    if (!pos) return;

    this.popup.show({
      type: 'datacenter',
      data: dc,
      x: pos[0],
      y: pos[1],
    });
  }

  public triggerNuclearClick(id: string): void {
    const facility = NUCLEAR_FACILITIES.find(n => n.id === id);
    if (!facility) return;

    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    const pos = projection([facility.lon, facility.lat]);
    if (!pos) return;

    this.popup.show({
      type: 'nuclear',
      data: facility,
      x: pos[0],
      y: pos[1],
    });
  }

  public triggerIrradiatorClick(id: string): void {
    const irradiator = GAMMA_IRRADIATORS.find(i => i.id === id);
    if (!irradiator) return;

    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    const pos = projection([irradiator.lon, irradiator.lat]);
    if (!pos) return;

    this.popup.show({
      type: 'irradiator',
      data: irradiator,
      x: pos[0],
      y: pos[1],
    });
  }

  public enableLayer(layer: keyof MapLayers): void {
    if (!this.state.layers[layer]) {
      this.state.layers[layer] = true;
      const thresholds = MapComponent.LAYER_ZOOM_THRESHOLDS[layer];
      if (thresholds && this.state.zoom < thresholds.minZoom) {
        this.layerZoomOverrides[layer] = true;
      } else {
        delete this.layerZoomOverrides[layer];
      }
      const btn = document.querySelector(`[data-layer="${layer}"]`);
      btn?.classList.add('active');
      this.onLayerChange?.(layer, true, 'programmatic');
      this.render();
    }
  }

  public highlightAssets(assets: RelatedAsset[] | null): void {
    (Object.keys(this.highlightedAssets) as AssetType[]).forEach((type) => {
      this.highlightedAssets[type].clear();
    });

    if (assets) {
      assets.forEach((asset) => {
        if (asset?.type && this.highlightedAssets[asset.type]) {
          this.highlightedAssets[asset.type].add(asset.id);
        }
      });
    }

    this.render();
  }

  private clampPan(width: number, height: number): void {
    const zoom = this.state.zoom;

    // Allow generous panning - maps should be explorable
    // Scale limits with zoom to allow reaching edges at higher zoom
    const maxPanX = (width / 2) * Math.max(1, zoom * 0.8);
    const maxPanY = (height / 2) * Math.max(1, zoom * 0.8);

    this.state.pan.x = Math.max(-maxPanX, Math.min(maxPanX, this.state.pan.x));
    this.state.pan.y = Math.max(-maxPanY, Math.min(maxPanY, this.state.pan.y));
  }

  private applyTransform(): void {
    const { width, height } = this.getKnownContainerSize();
    this.clampPan(width, height);
    const zoom = this.state.zoom;

    // With transform-origin: 0 0, we need to offset to keep center in view
    // Formula: translate first to re-center, then scale
    const centerOffsetX = (width / 2) * (1 - zoom);
    const centerOffsetY = (height / 2) * (1 - zoom);
    const tx = centerOffsetX + this.state.pan.x * zoom;
    const ty = centerOffsetY + this.state.pan.y * zoom;

    this.wrapper.style.transform = `translate(${tx}px, ${ty}px) scale(${zoom})`;

    // Set CSS variable for counter-scaling labels/markers
    // Labels: max 1.5x scale, so counter-scale = min(1.5, zoom) / zoom
    // Markers: fixed size, so counter-scale = 1 / zoom
    const labelScale = Math.min(1.5, zoom) / zoom;
    const markerScale = 1 / zoom;
    this.wrapper.style.setProperty('--label-scale', String(labelScale));
    this.wrapper.style.setProperty('--marker-scale', String(markerScale));
    this.wrapper.style.setProperty('--zoom', String(zoom));

    // Smart label hiding based on zoom level and overlap
    if (this.shouldUpdateLabelVisibility()) this.updateLabelVisibility(zoom);
    this.updateZoomLayerVisibility();
    this.emitStateChange();
  }

  private shouldUpdateLabelVisibility(): boolean {
    return !this.isMobile || this.mobileLabelVisibilityArmed;
  }

  private resumeMobileLabelVisibility(): void {
    if (!this.isMobile || this.mobileLabelVisibilityArmed) return;
    this.mobileLabelVisibilityArmed = true;
    this.updateLabelVisibility(this.state.zoom);
  }

  private updateZoomLayerVisibility(): void {
    const zoom = this.state.zoom;
    (Object.keys(MapComponent.LAYER_ZOOM_THRESHOLDS) as (keyof MapLayers)[]).forEach((layer) => {
      const thresholds = MapComponent.LAYER_ZOOM_THRESHOLDS[layer];
      if (!thresholds) return;

      const enabled = this.state.layers[layer];
      const override = Boolean(this.layerZoomOverrides[layer]);
      const isVisible = enabled && (override || zoom >= thresholds.minZoom);
      const labelZoom = thresholds.showLabels ?? thresholds.minZoom;
      const labelsVisible = enabled && zoom >= labelZoom;
      const hiddenAttr = `data-layer-hidden-${layer}`;
      const labelsHiddenAttr = `data-labels-hidden-${layer}`;

      if (isVisible) {
        this.wrapper.removeAttribute(hiddenAttr);
      } else {
        this.wrapper.setAttribute(hiddenAttr, 'true');
      }

      if (labelsVisible) {
        this.wrapper.removeAttribute(labelsHiddenAttr);
      } else {
        this.wrapper.setAttribute(labelsHiddenAttr, 'true');
      }

      const btn = document.querySelector(`[data-layer="${layer}"]`);
      const autoHidden = enabled && !override && zoom < thresholds.minZoom;
      btn?.classList.toggle('auto-hidden', autoHidden);
    });
  }

  private emitStateChange(): void {
    this.onStateChange?.(this.getState());
  }

  private updateLabelVisibility(zoom: number): void {
    this.pendingLabelVisibilityZoom = zoom;
    if (this.labelVisibilityScheduled) return;
    this.labelVisibilityScheduled = true;

    measure(() => {
      const measuredZoom = this.pendingLabelVisibilityZoom;
      const labelRects = this.measureLabelVisibility();
      mutate(() => {
        this.labelVisibilityScheduled = false;
        if (this.destroyed) return;
        this.applyLabelVisibility(labelRects, measuredZoom);
        if (this.pendingLabelVisibilityZoom !== measuredZoom) this.updateLabelVisibility(this.pendingLabelVisibilityZoom);
      });
    });
  }

  private measureLabelVisibility(): { el: HTMLElement; rect: DOMRect; priority: number }[] {
    const labels = this.overlays.querySelectorAll('.hotspot-label, .earthquake-label, .weather-label, .apt-label');
    const labelRects: { el: HTMLElement; rect: DOMRect; priority: number }[] = [];

    labels.forEach((label) => {
      const el = label as HTMLElement;
      const parent = el.closest('.hotspot, .earthquake-marker, .weather-marker, .apt-marker');

      let priority = 1;
      if (parent?.classList.contains('hotspot')) {
        const marker = parent.querySelector('.hotspot-marker');
        if (marker?.classList.contains('high')) priority = 5;
        else if (marker?.classList.contains('elevated')) priority = 3;
        else priority = 2;
      } else if (parent?.classList.contains('earthquake-marker')) {
        priority = 4;
      } else if (parent?.classList.contains('weather-marker')) {
        if (parent.classList.contains('extreme')) priority = 5;
        else if (parent.classList.contains('severe')) priority = 4;
        else priority = 2;
      }

      labelRects.push({ el, rect: el.getBoundingClientRect(), priority });
    });

    return labelRects;
  }

  private applyLabelVisibility(labelRects: { el: HTMLElement; rect: DOMRect; priority: number }[], zoom: number): void {
    labelRects.sort((a, b) => b.priority - a.priority);

    const visibleRects: DOMRect[] = [];
    const minDistance = 30 / zoom;

    labelRects.forEach(({ el, rect, priority }) => {
      const overlaps = visibleRects.some((vr) => {
        const dx = Math.abs((rect.left + rect.width / 2) - (vr.left + vr.width / 2));
        const dy = Math.abs((rect.top + rect.height / 2) - (vr.top + vr.height / 2));
        return dx < (rect.width + vr.width) / 2 + minDistance &&
          dy < (rect.height + vr.height) / 2 + minDistance;
      });

      if (overlaps && zoom < 2) {
        el.style.opacity = priority >= 4 ? '0.7' : '0';
      } else {
        el.style.opacity = '1';
        visibleRects.push(rect);
      }
    });
  }

  public onHotspotClicked(callback: (hotspot: Hotspot) => void): void {
    this.onHotspotClick = callback;
  }

  public onTimeRangeChanged(callback: (range: TimeRange) => void): void {
    this.onTimeRangeChange = callback;
  }

  public setOnCountryClick(cb: (country: CountryClickPayload) => void): void {
    this.onCountryClick = cb;
  }

  public fitCountry(code: string): void {
    const bbox = getCountryBbox(code);
    if (!bbox) return;
    const [minLon, minLat, maxLon, maxLat] = bbox;
    const midLon = (minLon + maxLon) / 2;
    const midLat = (minLat + maxLat) / 2;
    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    const topLeft = projection([minLon, maxLat]);
    const bottomRight = projection([maxLon, minLat]);
    if (!topLeft || !bottomRight) {
      this.state.zoom = 4;
      this.setCenter(midLat, midLon);
      return;
    }
    const pxWidth = Math.abs(bottomRight[0] - topLeft[0]);
    const pxHeight = Math.abs(bottomRight[1] - topLeft[1]);
    const padFactor = 0.8;
    const zoomX = pxWidth > 0 ? (width * padFactor) / pxWidth : 4;
    const zoomY = pxHeight > 0 ? (height * padFactor) / pxHeight : 4;
    this.state.zoom = Math.max(1, Math.min(8, Math.min(zoomX, zoomY)));
    this.setCenter(midLat, midLon);
  }

  public getState(): MapState {
    return { ...this.state };
  }

  public getCenter(): { lat: number; lon: number } | null {
    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    if (!projection.invert) return null;
    const zoom = this.state.zoom;
    const centerX = width / (2 * zoom) - this.state.pan.x;
    const centerY = height / (2 * zoom) - this.state.pan.y;
    const coords = projection.invert([centerX, centerY]);
    if (!coords) return null;
    return { lon: coords[0], lat: coords[1] };
  }

  public getTimeRange(): TimeRange {
    return this.state.timeRange;
  }

  public setZoom(zoom: number): void {
    this.state.zoom = Math.max(1, Math.min(10, zoom));
    this.applyTransform();
    // Ensure base layer is intact after zoom change
    this.ensureBaseLayerIntact();
  }

  private ensureBaseLayerIntact(): void {
    // Query DOM directly instead of relying on cached d3 selection
    const svgNode = this.svg.node();
    const domBaseGroup = svgNode?.querySelector('.map-base');
    const selectionNode = this.baseLayerGroup?.node();

    // Check for stale selection (d3 reference doesn't match DOM)
    if (domBaseGroup && selectionNode !== domBaseGroup) {
      console.warn('[Map] Stale base layer selection detected, forcing full rebuild');
      this.baseRendered = false;
      this.render();
      return;
    }

    // Check for missing countries
    const countryCount = domBaseGroup?.querySelectorAll('.country').length ?? 0;
    if (countryCount === 0 && this.countryFeatures && this.countryFeatures.length > 0) {
      console.warn('[Map] Base layer missing countries, triggering recovery render');
      this.baseRendered = false;
      this.render();
    }
  }

  public setCenter(lat: number, lon: number): void {
    console.log('[Map] setCenter called:', { lat, lon });
    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const projection = this.getProjection(width, height);
    const pos = projection([lon, lat]);
    console.log('[Map] projected pos:', pos, 'container:', { width, height }, 'zoom:', this.state.zoom);
    if (!pos) return;
    // Pan formula: after applyTransform() computes tx = centerOffset + pan*zoom,
    // and transform is translate(tx,ty) scale(zoom), to center on pos:
    // pos*zoom + tx = width/2 → tx = width/2 - pos*zoom
    // Solving: (width/2)(1-zoom) + pan*zoom = width/2 - pos*zoom
    // → pan = width/2 - pos (independent of zoom)
    this.state.pan = {
      x: width / 2 - pos[0],
      y: height / 2 - pos[1],
    };
    this.applyTransform();
    // Ensure base layer is intact after pan
    this.ensureBaseLayerIntact();
  }

  public setLayers(layers: MapLayers): void {
    const prevCyber = this.state.layers.cyberThreats;
    this.state.layers = { ...layers };
    if (this.state.layers.cyberThreats && !prevCyber && !this.aptGroupsLoaded) this.loadAptGroups();
    this.syncLayerButtons();
    this.render();
  }

  public setEarthquakes(earthquakes: Earthquake[]): void {
    console.log('[Map] setEarthquakes called with', earthquakes.length, 'earthquakes');
    if (earthquakes.length > 0 || this.earthquakes.length === 0) {
      this.earthquakes = earthquakes;
    } else {
      console.log('[Map] Keeping existing', this.earthquakes.length, 'earthquakes (new data was empty)');
    }
    this.render();
  }

  public setWeatherAlerts(alerts: WeatherAlert[]): void {
    this.weatherAlerts = alerts;
    this.render();
  }

  public setRadiationObservations(observations: RadiationObservation[]): void {
    this.radiationObservations = observations;
    this.render();
  }

  public setOutages(outages: InternetOutage[]): void {
    this.outages = outages;
    this.render();
  }

  public setAisData(disruptions: AisDisruptionEvent[], density: AisDensityZone[]): void {
    this.aisDisruptions = disruptions;
    this.aisDensity = density;
    this.render();
  }

  public setCableActivity(advisories: CableAdvisory[], repairShips: RepairShip[]): void {
    this.cableAdvisories = advisories;
    this.repairShips = repairShips;
    this.popup.setCableActivity(advisories, repairShips);
    this.render();
  }

  public setCableHealth(healthMap: Record<string, CableHealthRecord>): void {
    this.healthByCableId = healthMap;
    this.render();
  }

  public setConflictEvents(events: AcledConflictEvent[]): void {
    this.conflictEvents = events;
    this.render();
  }

  public setProtests(events: SocialUnrestEvent[]): void {
    this.protests = events;
    this.render();
  }

  public setFlightDelays(delays: AirportDelayAlert[]): void {
    this.flightDelays = delays;
    this.render();
  }

  public setAircraftPositions(positions: PositionSample[]): void {
    this.aircraftPositions = positions;
    this.render();
  }

  public setMilitaryFlights(flights: MilitaryFlight[], clusters: MilitaryFlightCluster[] = []): void {
    this.militaryFlights = flights;
    this.militaryFlightClusters = clusters;
    this.render();
  }

  public setMilitaryVessels(vessels: MilitaryVessel[], clusters: MilitaryVesselCluster[] = []): void {
    this.militaryVessels = vessels;
    this.militaryVesselClusters = clusters;
    this.render();
  }

  public setNaturalEvents(events: NaturalEvent[]): void {
    this.naturalEvents = events;
    this.render();
  }

  public setFires(fires: Array<{ lat: number; lon: number; brightness: number; frp: number; confidence: number; region: string; acq_date: string; daynight: string }>): void {
    this.firmsFireData = fires;
    this.render();
  }

  public setWebcams(markers: Array<WebcamEntry | WebcamCluster>): void {
    this.webcamData = markers;
    this.render();
  }

  public setTechEvents(events: TechEventMarker[]): void {
    this.techEvents = events;
    this.render();
  }

  public setCyberThreats(_threats: CyberThreat[]): void {
    // SVG/mobile fallback intentionally does not render this layer to stay lightweight.
  }

  public setIranEvents(events: IranEvent[]): void {
    this.iranEvents = events;
    this.render();
  }

  public setNewsLocations(_data: Array<{ lat: number; lon: number; title: string; threatLevel: string; timestamp?: Date }>): void {
    // SVG fallback: news locations rendered as simple circles
    // For now, skip on SVG map to keep mobile lightweight
  }

  public setTechActivity(activities: TechHubActivity[]): void {
    this.techActivities = activities;
    this.render();
  }

  public setOnTechHubClick(handler: (hub: TechHubActivity) => void): void {
    this.onTechHubClick = handler;
  }

  public setGeoActivity(activities: GeoHubActivity[]): void {
    this.geoActivities = activities;
    this.render();
  }

  public setOnGeoHubClick(handler: (hub: GeoHubActivity) => void): void {
    this.onGeoHubClick = handler;
  }

  private getCableAdvisory(cableId: string): CableAdvisory | undefined {
    const advisories = this.cableAdvisories.filter((advisory) => advisory.cableId === cableId);
    return advisories.reduce<CableAdvisory | undefined>((latest, advisory) => {
      if (!latest) return advisory;
      return advisory.reported.getTime() > latest.reported.getTime() ? advisory : latest;
    }, undefined);
  }

  private getCableName(cableId: string): string {
    return UNDERSEA_CABLES.find((cable) => cable.id === cableId)?.name || cableId;
  }

  public getHotspotLevels(): Record<string, string> {
    const levels: Record<string, string> = {};
    this.hotspots.forEach(spot => {
      levels[spot.name] = spot.level || 'low';
    });
    return levels;
  }

  public setHotspotLevels(levels: Record<string, string>): void {
    this.hotspots.forEach(spot => {
      if (levels[spot.name]) {
        spot.level = levels[spot.name] as 'high' | 'elevated' | 'low';
      }
    });
    this.render();
  }
}
