const { useState, useEffect, useRef } = React;

// Coordonnées de tuile (slippy map) pour une position lon/lat donnée — sert à générer
// une vraie vignette d'aperçu (façon Google Maps) pour le bouton de bascule de fond de carte.
function lonLatToTile(lon, lat, z) {
  const n = Math.pow(2, z);
  const x = Math.floor(((lon + 180) / 360) * n);
  const latRad = (lat * Math.PI) / 180;
  const y = Math.floor(((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) * n);
  return { x, y, z };
}

// Couleur du carroyage DFCI selon le fond de carte actif : le jaune se distingue bien sur
// la photo satellite mais se confond avec les teintes claires du fond plan OSM.
const GRID_COLOR = { satellite: "#fbbf24", plan: "#854d0e" };

// Couleurs des courbes de niveau (façon carte de randonnée) — brun/terracotta, distinct du
// jaune/brun du carroyage DFCI. Ligne maîtresse (multiples de la valeur "majeure") plus
// foncée/épaisse et étiquetée ; lignes intermédiaires plus fines et plus discrètes.
// Fond plan : brun/terracotta (bon contraste sur OSM clair). Fond satellite : version bien
// plus claire (crème/sable) — le brun se fond dans la photo aérienne (végétation/roche sombres).
const CONTOUR_COLOR = {
  plan: { major: "#8a4a1f", minor: "#c2793a" },
  satellite: { major: "#fde68a", minor: "#fef3c7" },
};

// Source d'élévation (Mapterhorn, encodage "terrarium") utilisée pour générer les courbes de
// niveau côté client (bibliothèque maplibre-contour, cf. index.html) — même jeu de tuiles
// que le relief 3D/hillshade, pas de service supplémentaire à charger.
const contourDemSource = new mlcontour.DemSource({
  url: "https://tiles.mapterhorn.com/{z}/{x}/{y}.webp",
  encoding: "terrarium",
  maxzoom: 13,
  worker: true,
});
contourDemSource.setupMaplibre(maplibregl);

// Aimantation des points de mission sur les chemins/routes environnants (cf. fetchPathsData
// dans lib.js) — rayon d'accroche et point de référence fixe pour la projection en mètres
// locaux (peu importe lequel, tant qu'il est cohérent entre le chargement des données et
// chaque calcul de distance).
const SNAP_METERS = 35;
const SNAP_REF = CONFIG.CAMP_POSITION;

// Illustration de fond du bouton "Relief 3D" — maillage filaire (wireframe) d'un terrain
// montagneux, façon visualisation de modèle numérique de terrain (lignes claires sur fond
// sombre) : plusieurs lignes de crête empilées, de plus en plus proches/marquées/opaques
// vers l'avant, pour suggérer la profondeur sans dépendance externe (SVG encodé en data URI).
const TERRAIN_BG = "data:image/svg+xml;utf8," + encodeURIComponent(
  '<svg xmlns="http://www.w3.org/2000/svg" width="104" height="52" viewBox="0 0 104 52">' +
  '<rect width="104" height="52" fill="#05070a"/>' +
  '<polyline points="0,10 15,8.2 30,9.4 45,7.6 60,9.1 75,7.9 90,9.7 104,10" fill="none" stroke="#e8edf0" stroke-width="0.5" opacity="0.35"/>' +
  '<polyline points="0,16 15,12 30,16.5 45,11 60,17 75,13 90,18 104,16" fill="none" stroke="#e8edf0" stroke-width="0.5" opacity="0.45"/>' +
  '<polyline points="0,23 15,19.5 30,25.1 45,16.7 60,26.5 75,16 90,24.4 104,23" fill="none" stroke="#e8edf0" stroke-width="0.55" opacity="0.55"/>' +
  '<polyline points="0,30 15,21 30,33.6 45,23.7 60,37.2 75,21.9 90,34.5 104,30" fill="none" stroke="#e8edf0" stroke-width="0.6" opacity="0.7"/>' +
  '<polyline points="0,38 15,30.3 30,44.6 45,27 60,41.3 75,29.2 90,45.7 104,38" fill="none" stroke="#e8edf0" stroke-width="0.65" opacity="0.85"/>' +
  '<polyline points="0,44 15,36 30,50.4 45,39.2 60,51.5 75,36.8 90,48.8 104,44" fill="none" stroke="#ffffff" stroke-width="0.7" opacity="1"/>' +
  '</svg>'
);

// Illustration de fond du bouton "Courbes niveau" — faisceau de lignes sinueuses parallèles
// (même forme, simplement translatée verticalement d'une ligne à l'autre) : garantit
// géométriquement qu'aucune ligne ne se croise, tout en gardant un tracé organique façon
// courbes de niveau. Même couleur que les courbes affichées sur la carte (CONTOUR_COLOR.minor).
const CONTOUR_BG = "data:image/svg+xml;utf8," + encodeURIComponent(
  '<svg xmlns="http://www.w3.org/2000/svg" width="104" height="52" viewBox="0 0 104 52">' +
  '<rect width="104" height="52" fill="#141a1d"/>' +
  '<path d="M-4,0 Q26,-4 39,0 Q52,4 65,1.5 Q78,-1 108,7" fill="none" stroke="#c2793a" stroke-width="0.9" opacity="0.8"/>' +
  '<path d="M-4,6.5 Q26,2.5 39,6.5 Q52,10.5 65,8 Q78,5.5 108,13.5" fill="none" stroke="#c2793a" stroke-width="0.9" opacity="0.8"/>' +
  '<path d="M-4,13 Q26,9 39,13 Q52,17 65,14.5 Q78,12 108,20" fill="none" stroke="#c2793a" stroke-width="1.5"/>' +
  '<path d="M-4,19.5 Q26,15.5 39,19.5 Q52,23.5 65,21 Q78,18.5 108,26.5" fill="none" stroke="#c2793a" stroke-width="0.9" opacity="0.8"/>' +
  '<path d="M-4,26 Q26,22 39,26 Q52,30 65,27.5 Q78,25 108,33" fill="none" stroke="#c2793a" stroke-width="0.9" opacity="0.8"/>' +
  '<path d="M-4,32.5 Q26,28.5 39,32.5 Q52,36.5 65,34 Q78,31.5 108,39.5" fill="none" stroke="#c2793a" stroke-width="1.5"/>' +
  '<path d="M-4,39 Q26,35 39,39 Q52,43 65,40.5 Q78,38 108,46" fill="none" stroke="#c2793a" stroke-width="0.9" opacity="0.8"/>' +
  '<path d="M-4,45.5 Q26,41.5 39,45.5 Q52,49.5 65,47 Q78,44.5 108,52.5" fill="none" stroke="#c2793a" stroke-width="0.9" opacity="0.8"/>' +
  '</svg>'
);

// Carte MapLibre : fond OSM + relief 3D (Mapterhorn) + couche DFCI (Overpass) + marqueurs missions/feux/camp.
// Remplace entièrement la carte en image statique du prototype pc-dfci.jsx — la logique métier
// (missions, feux, triangulation) est portée depuis ce prototype, adaptée aux vraies coordonnées lon/lat.
function MapView({
  missions, fires, selected, onSelect,
  placing, onMapClickPlaceFire,
  onMissionMoved, onFireMoved,
  firePicker,
}) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);
  const markersRef = useRef(new Map()); // id -> { marker, labelMarker, kind }
  const [dfciStatus, setDfciStatus] = useState("Chargement des données DFCI…");
  const [mapReady, setMapReady] = useState(false);
  const basemapSatelliteRef = useRef(false);
  const terrainOnRef = useRef(false);
  const pathsLocalRef = useRef(null); // segments (tous chemins/routes) projetés en mètres locaux, ou null si pas encore chargés/indisponibles

  // Aimante (lon, lat) sur le chemin/route le plus proche s'il y en a un à moins de
  // SNAP_METERS ; sinon renvoie le point tel quel. Utilisé uniquement au relâchement du
  // glissement (dragend) — MapLibre recalcule la position d'un marker glissé à partir du
  // déplacement cumulé depuis le point de départ, donc un setLngLat pendant l'événement
  // "drag" lui-même serait immédiatement écrasé au prochain mouvement de souris.
  function snapLonLat(lon, lat) {
    const lines = pathsLocalRef.current;
    if (!lines || lines.length === 0) return { lon, lat };
    const pxy = metersFrom(SNAP_REF, { lon, lat });
    const snapped = nearestPointOnProjectedLines(pxy, lines, SNAP_METERS);
    if (!snapped) return { lon, lat };
    return lonLatFrom(SNAP_REF, snapped);
  }

  // Couleur du carroyage selon le fond de carte actif — appelée à la création des layers
  // (loadDfciGrid) et à chaque bascule de fond de carte (BasemapToggleControl).
  function applyGridColor() {
    const map = mapRef.current;
    if (!map) return;
    const color = basemapSatelliteRef.current ? GRID_COLOR.satellite : GRID_COLOR.plan;
    if (map.getLayer("dfci-grid-line")) map.setPaintProperty("dfci-grid-line", "line-color", color);
    if (map.getLayer("dfci-grid-label")) {
      map.setPaintProperty("dfci-grid-label", "text-color", color);
      map.setPaintProperty("dfci-grid-label", "text-halo-color", color);
    }
  }

  // Couleur des courbes de niveau selon le fond de carte actif — même principe que
  // applyGridColor() : le brun/terracotta se distingue bien sur le fond plan OSM mais se
  // fond dans la photo satellite (végétation/roche sombres), d'où une teinte claire dédiée.
  function applyContourColor() {
    const map = mapRef.current;
    if (!map) return;
    const c = basemapSatelliteRef.current ? CONTOUR_COLOR.satellite : CONTOUR_COLOR.plan;
    if (map.getLayer("contour-line")) {
      map.setPaintProperty("contour-line", "line-color", ["match", ["get", "level"], 1, c.major, c.minor]);
    }
    if (map.getLayer("contour-label")) {
      map.setPaintProperty("contour-label", "text-color", c.major);
      map.setPaintProperty("contour-label", "text-halo-color", c.major);
    }
  }

  // Refs "live" pour que les listeners MapLibre (attachés une seule fois) lisent toujours les
  // dernières valeurs sans avoir à ré-attacher les événements à chaque rendu.
  const placingRef = useRef(placing);
  placingRef.current = placing;
  const onMapClickRef = useRef(onMapClickPlaceFire);
  onMapClickRef.current = onMapClickPlaceFire;

  // ---- Initialisation de la carte (une seule fois) ----
  useEffect(() => {
    const map = new maplibregl.Map({
      container: containerRef.current,
      zoom: CONFIG.MAP_DEFAULT_ZOOM,
      center: CONFIG.MAP_CENTER,
      pitch: 0,
      bearing: 0,
      maxZoom: 18,
      minZoom: CONFIG.MAP_MIN_ZOOM,
      maxBounds: CONFIG.MAP_MAX_BOUNDS,
      maxPitch: 85,
      // Attribution requise (OSM/Esri/Mapterhorn) mais repliée par défaut (icône seule, clic
      // pour dérouler) — cf. AttributionControl ci-dessous, qui remplace celle ajoutée
      // automatiquement (repliée seulement sur petit écran par défaut).
      attributionControl: false,
      style: {
        version: 8,
        glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
        sources: {
          osm: {
            type: "raster",
            tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
            tileSize: 256,
            attribution: "&copy; OpenStreetMap Contributors",
            maxzoom: 19,
          },
          satellite: {
            type: "raster",
            tiles: ["https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"],
            tileSize: 256,
            attribution: "Tiles &copy; Esri",
            maxzoom: 19,
          },
          terrainSource: { type: "raster-dem", url: "https://tiles.mapterhorn.com/tilejson.json" },
          hillshadeSource: { type: "raster-dem", url: "https://tiles.mapterhorn.com/tilejson.json" },
        },
        layers: [
          { id: "osm", type: "raster", source: "osm" },
          { id: "satellite", type: "raster", source: "satellite", layout: { visibility: "none" } },
          {
            id: "hills", type: "hillshade", source: "hillshadeSource",
            layout: { visibility: "visible" }, paint: { "hillshade-shadow-color": "#473B24" },
          },
        ],
        // Pas de relief 3D activé au chargement (vue zénithale par défaut) — la source reste
        // déclarée pour que le bouton "Disable/Enable terrain" (TerrainControl) et le bouton
        // "2D / 3D" puissent l'activer à la demande.
        sky: {},
      },
    });
    mapRef.current = map;

    map.addControl(new maplibregl.NavigationControl({ visualizePitch: true, showZoom: true, showCompass: true }));
    map.addControl(new maplibregl.AttributionControl({ compact: true }));

    // ---- Bascule fond de carte : plan OSM / photo satellite (Esri World Imagery) ----
    // Vignette façon Google Maps : le bouton affiche un aperçu réel (tuile) du fond
    // qu'on activerait en cliquant — pas celui actuellement affiché.
    class BasemapToggleControl {
      onAdd(m) {
        this._map = m;
        this._container = document.createElement("div");
        this._container.className = "maplibregl-ctrl";
        this._btn = document.createElement("div");
        this._btn.className = "pc-toolbtn";
        this._btn.style.cssText = "width:104px;height:52px;border-radius:8px;overflow:hidden;cursor:pointer;position:relative;box-shadow:0 1px 4px rgba(0,0,0,0.45);border:2px solid #fff;background-size:cover;background-position:center;background-color:#333;";
        const label = document.createElement("div");
        label.style.cssText = "position:absolute;left:0;right:0;bottom:0;padding:4px 0 5px;background:linear-gradient(to top, rgba(0,0,0,0.65), transparent);color:#fff;font-size:10.5px;font-weight:600;display:flex;align-items:center;justify-content:center;gap:3px;";
        const iconEl = document.createElement("span");
        iconEl.style.cssText = "display:flex;";
        ReactDOM.createRoot(iconEl).render(React.createElement(Icons.Layers, { size: 11, color: "#fff" }));
        this._text = document.createElement("span");
        this._text.className = "pc-toolbtn-text";
        label.appendChild(iconEl);
        label.appendChild(this._text);
        this._btn.appendChild(label);
        this._btn.onclick = () => {
          basemapSatelliteRef.current = !basemapSatelliteRef.current;
          this._map.setLayoutProperty("osm", "visibility", basemapSatelliteRef.current ? "none" : "visible");
          this._map.setLayoutProperty("satellite", "visibility", basemapSatelliteRef.current ? "visible" : "none");
          applyGridColor();
          applyContourColor();
          this._update();
        };
        this._container.appendChild(this._btn);
        this._update();
        return this._container;
      }
      _update() {
        // Le bouton décrit toujours la destination (pas le fond actif) : vignette ET texte
        // montrent ce qu'on obtient en cliquant, comme sur Google Maps.
        const center = this._map.getCenter();
        const tile = lonLatToTile(center.lng, center.lat, 12);
        const url = basemapSatelliteRef.current
          ? `https://tile.openstreetmap.org/${tile.z}/${tile.x}/${tile.y}.png`
          : `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/${tile.z}/${tile.y}/${tile.x}`;
        this._btn.style.backgroundImage = `url(${url})`;
        this._text.textContent = basemapSatelliteRef.current ? "Plan" : "Satellite";
      }
      onRemove() { this._container.parentNode.removeChild(this._container); }
    }
    map.addControl(new BasemapToggleControl(), "bottom-left");

    // ---- Bascule carroyage DFCI (grille officielle 2 km, source data.gouv.fr) ----
    // Même gabarit que le bouton de fond de carte (72px), avec un motif de grille en fond
    // pour identifier la fonction au premier coup d'œil.
    class GridToggleControl {
      onAdd(m) {
        this._map = m;
        this._on = false;
        this._container = document.createElement("div");
        this._container.className = "maplibregl-ctrl";
        this._btn = document.createElement("div");
        this._btn.className = "pc-toolbtn";
        this._btn.style.cssText = "width:104px;height:52px;border-radius:8px;overflow:hidden;cursor:pointer;position:relative;box-shadow:0 1px 4px rgba(0,0,0,0.45);border:2px solid #fff;background-color:#1c2226;background-image:linear-gradient(to right, #fbbf2499 1px, transparent 1px),linear-gradient(to bottom, #fbbf2499 1px, transparent 1px);background-size:14px 14px;transition:border-color 0.15s;";
        const label = document.createElement("div");
        label.style.cssText = "position:absolute;left:0;right:0;bottom:0;padding:4px 0 5px;background:linear-gradient(to top, rgba(0,0,0,0.65), transparent);color:#fff;font-size:10.5px;font-weight:600;display:flex;align-items:center;justify-content:center;gap:3px;";
        const iconEl = document.createElement("span");
        iconEl.style.cssText = "display:flex;";
        ReactDOM.createRoot(iconEl).render(React.createElement(Icons.LayoutDashboard, { size: 11, color: "#fff" }));
        const text = document.createElement("span");
        text.className = "pc-toolbtn-text";
        text.textContent = "Grille DFCI";
        label.appendChild(iconEl);
        label.appendChild(text);
        this._btn.appendChild(label);
        this._btn.onclick = () => {
          this._on = !this._on;
          const vis = this._on ? "visible" : "none";
          if (this._map.getLayer("dfci-grid-line")) this._map.setLayoutProperty("dfci-grid-line", "visibility", vis);
          if (this._map.getLayer("dfci-grid-label")) this._map.setLayoutProperty("dfci-grid-label", "visibility", vis);
          this._btn.style.borderColor = this._on ? "#fbbf24" : "#fff";
        };
        this._container.appendChild(this._btn);
        return this._container;
      }
      onRemove() { this._container.parentNode.removeChild(this._container); }
    }
    map.addControl(new GridToggleControl(), "bottom-left");

    // ---- Bascule relief 3D (terrain DEM Mapterhorn) — remplace le bouton natif MapLibre
    // (icône montagne sous la boussole/le zoom) par le même gabarit que les deux boutons
    // ci-dessus, pour une barre d'outils visuellement cohérente en bas à gauche.
    class TerrainToggleControl {
      onAdd(m) {
        this._map = m;
        this._container = document.createElement("div");
        this._container.className = "maplibregl-ctrl";
        this._btn = document.createElement("div");
        this._btn.className = "pc-toolbtn";
        this._btn.style.cssText = "width:104px;height:52px;border-radius:8px;overflow:hidden;cursor:pointer;position:relative;box-shadow:0 1px 4px rgba(0,0,0,0.45);border:2px solid #fff;background-color:#141a1d;background-image:url('" + TERRAIN_BG + "');background-size:cover;background-position:center;transition:border-color 0.15s;";
        const label = document.createElement("div");
        label.style.cssText = "position:absolute;left:0;right:0;bottom:0;padding:4px 0 5px;background:linear-gradient(to top, rgba(0,0,0,0.65), transparent);color:#fff;font-size:10.5px;font-weight:600;display:flex;align-items:center;justify-content:center;gap:3px;";
        const iconEl = document.createElement("span");
        iconEl.style.cssText = "display:flex;";
        ReactDOM.createRoot(iconEl).render(React.createElement(Icons.Mountain, { size: 11, color: "#fff" }));
        const text = document.createElement("span");
        text.className = "pc-toolbtn-text";
        text.textContent = "Relief 3D";
        label.appendChild(iconEl);
        label.appendChild(text);
        this._btn.appendChild(label);
        this._btn.onclick = () => {
          terrainOnRef.current = !terrainOnRef.current;
          this._map.setTerrain(terrainOnRef.current ? { source: "terrainSource", exaggeration: 1.8 } : null);
          this._btn.style.borderColor = terrainOnRef.current ? "#4ade80" : "#fff";
        };
        this._container.appendChild(this._btn);
        return this._container;
      }
      onRemove() { this._container.parentNode.removeChild(this._container); }
    }
    map.addControl(new TerrainToggleControl(), "bottom-left");

    // ---- Bascule courbes de niveau (façon carte de randonnée) — même gabarit que les
    // boutons ci-dessus, fond illustrant des courbes concentriques (silhouette de colline).
    class ContourToggleControl {
      onAdd(m) {
        this._map = m;
        this._on = false;
        this._container = document.createElement("div");
        this._container.className = "maplibregl-ctrl";
        this._btn = document.createElement("div");
        this._btn.className = "pc-toolbtn";
        this._btn.style.cssText = "width:104px;height:52px;border-radius:8px;overflow:hidden;cursor:pointer;position:relative;box-shadow:0 1px 4px rgba(0,0,0,0.45);border:2px solid #fff;background-color:#141a1d;background-image:url('" + CONTOUR_BG + "');background-size:cover;background-position:center;transition:border-color 0.15s;";
        const label = document.createElement("div");
        label.style.cssText = "position:absolute;left:0;right:0;bottom:0;padding:4px 0 5px;background:linear-gradient(to top, rgba(0,0,0,0.65), transparent);color:#fff;font-size:10.5px;font-weight:600;display:flex;align-items:center;justify-content:center;gap:3px;";
        const iconEl = document.createElement("span");
        iconEl.style.cssText = "display:flex;";
        ReactDOM.createRoot(iconEl).render(React.createElement(Icons.Waves, { size: 11, color: "#fff" }));
        const text = document.createElement("span");
        text.className = "pc-toolbtn-text";
        text.textContent = "Courbes niveau";
        label.appendChild(iconEl);
        label.appendChild(text);
        this._btn.appendChild(label);
        this._btn.onclick = () => {
          this._on = !this._on;
          const vis = this._on ? "visible" : "none";
          if (this._map.getLayer("contour-line")) this._map.setLayoutProperty("contour-line", "visibility", vis);
          if (this._map.getLayer("contour-label")) this._map.setLayoutProperty("contour-label", "visibility", vis);
          this._btn.style.borderColor = this._on ? CONTOUR_COLOR.plan.minor : "#fff";
        };
        this._container.appendChild(this._btn);
        return this._container;
      }
      onRemove() { this._container.parentNode.removeChild(this._container); }
    }
    map.addControl(new ContourToggleControl(), "bottom-left");

    map.on("click", (e) => {
      if (!placingRef.current) return;
      onMapClickRef.current({ lon: e.lngLat.lng, lat: e.lngLat.lat });
    });

    map.on("load", () => {
      // Sources vides pour l'aperçu de triangulation (lignes de visée + point estimé)
      map.addSource("triangulation-lines", { type: "geojson", data: { type: "FeatureCollection", features: [] } });
      map.addLayer({
        id: "triangulation-lines-layer", type: "line", source: "triangulation-lines",
        paint: { "line-color": "#fb923c", "line-width": 2, "line-dasharray": [2, 1.5] },
      });

      // ---- Courbes de niveau (façon carte de randonnée), générées côté client à partir du
      // relief 3D — masquées par défaut, bascule via le bouton "Courbes niveau" (bottom-left).
      map.addSource("contours", {
        type: "vector",
        tiles: [
          contourDemSource.contourProtocolUrl({
            multiplier: 1,
            // Une courbe tous les 10 m (façon carte de randonnée type IGN), ligne maîtresse
            // (plus épaisse, étiquetée) tous les 50 m pour rester lisible.
            thresholds: {
              10: [10, 50],
            },
            contourLayer: "contours",
            elevationKey: "ele",
            levelKey: "level",
          }),
        ],
        maxzoom: 15,
      });
      map.addLayer({
        id: "contour-line", type: "line", source: "contours", "source-layer": "contours",
        layout: { visibility: "none" },
        paint: {
          "line-color": ["match", ["get", "level"], 1, CONTOUR_COLOR.plan.major, CONTOUR_COLOR.plan.minor],
          "line-width": ["match", ["get", "level"], 1, 1.6, 0.7],
          "line-opacity": ["match", ["get", "level"], 1, 0.85, 0.55],
        },
      });
      map.addLayer({
        id: "contour-label", type: "symbol", source: "contours", "source-layer": "contours",
        filter: [">", ["get", "level"], 0],
        layout: {
          visibility: "none",
          "symbol-placement": "line",
          "text-field": ["concat", ["number-format", ["get", "ele"], {}], " m"],
          "text-size": 10,
          "text-font": ["Noto Sans Regular"],
          // Décalage perpendiculaire à la ligne : évite que le chiffre ne se retrouve pile sur
          // une piste/route qui croise la courbe (illisible en superposition directe).
          "text-offset": [0, -0.9],
        },
        // Halo de la même couleur que le texte : simule un effet gras (pas de police "Bold"
        // disponible sur la source de glyphes utilisée), les chiffres étaient trop fins sinon.
        paint: { "text-color": CONTOUR_COLOR.plan.major, "text-halo-color": CONTOUR_COLOR.plan.major, "text-halo-width": 0.7 },
      });
      applyContourColor();

      loadDfci();
      loadDfciGrid();
      loadMissionRoutes();
      setMapReady(true);
    });

    // Aimantation des points de mission : chargement à part, en arrière-plan, sans bloquer le
    // reste de la carte — un échec ou une lenteur laisse simplement l'aimantation désactivée.
    fetchPathsData().then((lines) => {
      if (lines) pathsLocalRef.current = projectLinesToLocal(lines, SNAP_REF);
    });

    return () => map.remove();
    // eslint-disable-next-line
  }, []);

  // ---- Carroyage DFCI officiel (maille 2 km subdivisée en 5 zones de 1 km, geonov.fr/
  // data.gouv.fr, découpé sur le massif) ----
  async function loadDfciGrid() {
    const map = mapRef.current;
    try {
      const [r, rLabels] = await Promise.all([
        fetch("js/data/dfci-grid.geojson"),
        fetch("js/data/dfci-grid-labels.geojson"),
      ]);
      if (!r.ok) throw new Error("HTTP " + r.status);
      const data = await r.json();
      const labelData = rLabels.ok ? await rLabels.json() : { type: "FeatureCollection", features: [] };

      map.addSource("dfci-grid", { type: "geojson", data });
      map.addLayer({
        id: "dfci-grid-line", type: "line", source: "dfci-grid",
        layout: { visibility: "none" },
        paint: { "line-color": GRID_COLOR.plan, "line-width": 0.75, "line-opacity": 0.85 },
      });
      // Étiquette : référence DFCI complète de chaque zone (ex. "KD40F4.3") — code de la
      // maille 2 km + numéro de zone (1-4 dans les coins, 5 au centre), la référence utilisée
      // pour la coordination radio. Points dédiés (centre du carré 1 km d'origine) plutôt que
      // le centroïde du polygone affiché : les zones 1-4 ont un coin échancré là où la zone 5
      // les chevauche, ce qui décentre et perturbe le placement automatique sur le polygone.
      map.addSource("dfci-grid-labels", { type: "geojson", data: labelData });
      map.addLayer({
        id: "dfci-grid-label", type: "symbol", source: "dfci-grid-labels",
        layout: {
          visibility: "none",
          "text-field": ["concat", ["get", "cell"], ".", ["get", "zone"]],
          "text-size": 13,
          "text-font": ["Noto Sans Regular"],
          "text-allow-overlap": true, "text-ignore-placement": true,
        },
        // Halo de la même couleur que le texte : simule un effet gras (pas de police "Bold"
        // disponible sur la source de glyphes utilisée), sans ajouter de contour contrasté.
        paint: { "text-color": GRID_COLOR.plan, "text-halo-color": GRID_COLOR.plan, "text-halo-width": 0.6 },
      });
      applyGridColor();
    } catch (err) {
      // Non bloquant : la grille est un complément, pas une donnée critique pour le reste de l'outil.
      console.error("Chargement du carroyage DFCI impossible :", err);
    }
  }

  // Les pistes DFCI (rouge, chargées via Overpass) doivent toujours rester visibles par-
  // dessus la surbrillance d'itinéraire de mission — appelée à la fin des deux chargements
  // (loadDfci/loadMissionRoutes), quel que soit celui qui termine en premier : celui qui
  // termine en second constate que l'autre calque existe déjà et corrige l'ordre ; celui qui
  // termine en premier n'a rien à faire (l'autre s'en chargera à son tour).
  function ensureMissionRoutesBelowPistes(map) {
    if (map.getLayer("mission-route-glow") && map.getLayer("dfci-pistes-line")) {
      map.moveLayer("mission-route-glow", "dfci-pistes-line");
      map.moveLayer("mission-route-line", "dfci-pistes-line");
    }
  }

  // ---- Itinéraires précis des missions (tracés fournis par le coordinateur, un fichier par
  // mission — cf. js/data/mission-routes.geojson) : masqués par défaut, seule la mission
  // sélectionnée voit son tracé mis en surbrillance (cf. l'effet plus bas qui pilote le
  // filtre selon `selected`). Chaque tracé est identifié par son "route" = mission.label
  // exact (ex. "Patrouille Sugiton"), pour qu'un nouveau tracé n'ait qu'à reprendre le même
  // libellé que la mission pour être automatiquement relié — aucun autre code à toucher.
  async function loadMissionRoutes() {
    const map = mapRef.current;
    try {
      const r = await fetch("js/data/mission-routes.geojson");
      if (!r.ok) throw new Error("HTTP " + r.status);
      const data = await r.json();
      map.addSource("mission-routes", { type: "geojson", data });
      const routeColor = ["match", ["get", "type"],
        "vigie", MISSION_TYPES.vigie.color,
        "patrouille", MISSION_TYPES.patrouille.color,
        "barriere", MISSION_TYPES.barriere.color,
        "#ffffff",
      ];
      // Halo large et flou derrière un trait plein plus fin : lisible même sur un fond de
      // carte chargé, sans masquer le tracé exact en dessous.
      map.addLayer({
        id: "mission-route-glow", type: "line", source: "mission-routes",
        filter: ["==", ["get", "route"], ""],
        paint: { "line-color": routeColor, "line-width": 11, "line-opacity": 0.35, "line-blur": 2.5 },
      });
      map.addLayer({
        id: "mission-route-line", type: "line", source: "mission-routes",
        filter: ["==", ["get", "route"], ""],
        paint: { "line-color": routeColor, "line-width": 3.5 },
      });
      ensureMissionRoutesBelowPistes(map);
    } catch (err) {
      // Non bloquant : les itinéraires sont un complément, pas une donnée critique.
      console.error("Chargement des itinéraires de mission impossible :", err);
    }
  }

  // ---- Chargement des données DFCI (pistes, citernes, vigies) via Overpass ----
  async function loadDfci() {
    const map = mapRef.current;
    try {
      const { lines, points } = await fetchDfciData();

      map.addSource("dfci-pistes", { type: "geojson", data: lines });
      map.addLayer({
        id: "dfci-pistes-line", type: "line", source: "dfci-pistes",
        paint: { "line-color": "#ef4444", "line-width": 2, "line-dasharray": [2, 1] },
      });
      // Nom des pistes (codes DFCI type "CQ"/"HC") en majuscule, le long du tracé
      map.addLayer({
        id: "dfci-pistes-label", type: "symbol", source: "dfci-pistes",
        filter: ["any", ["has", "ref:FR:DFCI"], ["has", "name"]],
        layout: {
          "symbol-placement": "line", "symbol-spacing": 220,
          "text-field": ["coalesce", ["get", "ref:FR:DFCI"], ["get", "name"]],
          "text-transform": "uppercase", "text-size": 14,
          "text-font": ["Noto Sans Regular"],
          "text-offset": [0, -0.9],
        },
        // Léger halo de la même couleur que le texte : simule un effet gras (pas de police "Bold"
        // disponible sur la source de glyphes utilisée) sans ajouter de contour contrasté.
        paint: { "text-color": "#ef4444", "text-halo-color": "#ef4444", "text-halo-width": 0.6 },
      });
      ensureMissionRoutesBelowPistes(map);

      map.addSource("dfci-points", { type: "geojson", data: points });
      map.addLayer({
        id: "dfci-vigies-osm", type: "circle", source: "dfci-points",
        filter: ["==", ["get", "emergency"], "fire_lookout"],
        paint: { "circle-radius": 7, "circle-color": "#c084fc22", "circle-stroke-width": 1.5, "circle-stroke-color": "#c084fc" },
      });

      // Citernes / bassins : marqueur carré bleu + numéro (ref DFCI) affiché au-dessus, en couleur
      points.features
        .filter((f) => ["water_tank", "fire_water_pond"].includes(f.properties.emergency))
        .forEach((f) => {
          const p = f.properties;
          const label = p["ref:FR:DFCI"] || p.name || "";
          const coords = f.geometry.coordinates;

          const sq = document.createElement("div");
          sq.style.cssText = "width:15px;height:15px;background:#4d9fef;border:1.5px solid #0f1417;box-shadow:0 1px 2px rgba(0,0,0,0.6);cursor:pointer;z-index:1;";
          sq.addEventListener("click", (ev) => {
            ev.stopPropagation();
            const capacity = p.capacity ? Math.round(p.capacity / 1000) + " m³" : "contenance inconnue";
            new maplibregl.Popup().setLngLat(coords)
              .setHTML(`<div class="dfci-popup"><b>${escapeHtml(p.name || label || "Citerne")}</b><br>${capacity}</div>`)
              .addTo(map);
          });
          new maplibregl.Marker({ element: sq, anchor: "center" }).setLngLat(coords).addTo(map);

          if (label) {
            const lbl = document.createElement("div");
            lbl.style.cssText = "pointer-events:none;white-space:nowrap;color:#4d9fef;font-weight:700;font-size:17px;z-index:1;";
            lbl.textContent = label;
            new maplibregl.Marker({ element: lbl, anchor: "bottom", offset: [0, -10] }).setLngLat(coords).addTo(map);
          }
        });

      map.on("click", "dfci-pistes-line", (e) => {
        const p = e.features[0].properties;
        new maplibregl.Popup().setLngLat(e.lngLat)
          .setHTML(`<div class="dfci-popup"><b>${escapeHtml(p.name || p["ref:FR:DFCI"] || "Piste DFCI")}</b></div>`)
          .addTo(map);
      });
      ["dfci-vigies-osm", "dfci-pistes-line"].forEach((id) => {
        map.on("mouseenter", id, () => (map.getCanvas().style.cursor = "pointer"));
        map.on("mouseleave", id, () => (map.getCanvas().style.cursor = ""));
      });

      setDfciStatus(`DFCI chargé : ${lines.features.length} pistes, ${points.features.length} points.`);
      setTimeout(() => setDfciStatus(null), 4000);
    } catch (err) {
      setDfciStatus("Erreur de chargement DFCI (serveur Overpass surchargé ?) : " + err.message);
    }
  }

  // ---- Marqueur fixe du camp ----
  useEffect(() => {
    if (!mapReady) return;
    const map = mapRef.current;
    const el = document.createElement("div");
    el.style.cssText = "width:30px;height:30px;background:#f2a93b;border:2px solid #0f1417;border-radius:6px;box-shadow:0 1px 3px rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;color:#0a0d0f;z-index:2;";
    ReactDOM.createRoot(el).render(React.createElement(Icons.TentFill, { size: 18, color: "#0a0d0f" }));
    const marker = new maplibregl.Marker({ element: el, anchor: "center" })
      .setLngLat([CONFIG.CAMP_POSITION.lon, CONFIG.CAMP_POSITION.lat])
      .setPopup(new maplibregl.Popup().setHTML("<b>Camp / PC</b><br>Position à confirmer"))
      .addTo(map);
    return () => marker.remove();
  }, [mapReady]);

  // ---- Repères fixes (CONFIG.LANDMARKS) — ex. sommet réel d'un poste de vigie. Discrets et
  // non déplaçables (contrairement aux marqueurs de mission) : juste une petite icône +
  // un texte coloré, façon étiquette de sommet/lieu-dit sur un fond de carte classique.
  useEffect(() => {
    if (!mapReady) return;
    const map = mapRef.current;
    const markers = CONFIG.LANDMARKS.map((lm) => {
      const iconEl = document.createElement("div");
      iconEl.style.cssText = "display:flex;filter:drop-shadow(0 1px 1.5px rgba(0,0,0,0.8));";
      ReactDOM.createRoot(iconEl).render(React.createElement(Icons.Watchtower, { size: 15, color: "#ffffff" }));
      const iconMarker = new maplibregl.Marker({ element: iconEl, anchor: "center" })
        .setLngLat([lm.lon, lm.lat]).addTo(map);

      const labelEl = document.createElement("div");
      labelEl.style.cssText = "white-space:nowrap;font-size:11px;font-weight:700;color:#ffffff;text-shadow:0 1px 2px rgba(0,0,0,0.9), 0 0 4px rgba(0,0,0,0.8);";
      labelEl.textContent = lm.label;
      const labelMarker = new maplibregl.Marker({ element: labelEl, anchor: "left", offset: [9, 0] })
        .setLngLat([lm.lon, lm.lat]).addTo(map);

      return [iconMarker, labelMarker];
    });
    return () => markers.forEach(([iconMarker, labelMarker]) => { iconMarker.remove(); labelMarker.remove(); });
  }, [mapReady]);

  // ---- Synchronisation des marqueurs missions + feux ----
  useEffect(() => {
    if (!mapReady) return;
    const map = mapRef.current;
    const live = new Set();

    missions.forEach((m) => live.add("mission:" + m.id));
    fires.forEach((f) => live.add("fire:" + f.id));

    // Supprime les marqueurs dont l'entité a disparu
    for (const [key, entry] of markersRef.current) {
      if (!live.has(key)) {
        entry.marker.remove();
        if (entry.labelMarker) entry.labelMarker.remove();
        markersRef.current.delete(key);
      }
    }

    missions.forEach((m) => syncMissionMarker(map, m));
    fires.forEach((f) => syncFireMarker(map, f));

    // Recalcule le sens des étiquettes (gauche/droite) selon la position à l'écran, après un léger
    // délai pour laisser le temps aux marqueurs d'être posés.
    updateAllLabelFlips(map);
    // eslint-disable-next-line
  }, [mapReady, missions, fires, selected]);

  // ---- Surbrillance de l'itinéraire de la mission sélectionnée (cf. loadMissionRoutes) ----
  useEffect(() => {
    if (!mapReady) return;
    const map = mapRef.current;
    if (!map.getLayer("mission-route-line")) return;
    const sel = selected && selected.kind === "mission" ? missions.find((m) => m.id === selected.id) : null;
    const label = sel ? sel.label : "";
    map.setFilter("mission-route-glow", ["==", ["get", "route"], label]);
    map.setFilter("mission-route-line", ["==", ["get", "route"], label]);
  }, [mapReady, selected, missions]);

  useEffect(() => {
    if (!mapReady) return;
    const map = mapRef.current;
    const onMoveEnd = () => updateAllLabelFlips(map);
    map.on("moveend", onMoveEnd);
    return () => map.off("moveend", onMoveEnd);
    // eslint-disable-next-line
  }, [mapReady, missions]);

  // Largeur/hauteur approximatives d'une étiquette de mission — servent à la fois à détecter
  // la proximité du bord droit de la carte et un chevauchement probable avec un point voisin
  // (mission ou feu) situé juste à droite.
  const LABEL_W = 140;
  const LABEL_H = 34;

  function updateAllLabelFlips(map) {
    const width = map.getContainer().clientWidth;

    // Position écran de tous les points (missions + feux) — sert à repérer les voisins
    // proches d'une mission donnée, pas seulement le bord de la carte.
    const positions = [];
    for (const [, entry] of markersRef.current) {
      if (entry.kind === "preview") continue;
      const px = map.project(entry.marker.getLngLat());
      positions.push({ entry, x: px.x, y: px.y });
    }

    for (const p of positions) {
      const { entry } = p;
      if (!entry.labelMarker) continue; // seules les missions ont une étiquette à repositionner

      // Par défaut l'étiquette part à droite du point, sauf trop près du bord droit de la
      // carte — ou si un autre point se trouve juste à droite, là où elle passerait sinon :
      // on la fait alors basculer à gauche pour ne pas le recouvrir (cf. capture d'écran où
      // l'étiquette "Vigie Luminy" chevauchait le point "Patrouille Escampon" voisin).
      let flip = p.x > width - LABEL_W;
      if (!flip) {
        for (const other of positions) {
          if (other === p) continue;
          const dx = other.x - p.x;
          const dy = Math.abs(other.y - p.y);
          if (dx > 0 && dx < LABEL_W && dy < LABEL_H) { flip = true; break; }
        }
      }

      const wantAnchor = flip ? "right" : "left";
      if (entry.labelAnchor !== wantAnchor) {
        entry.labelMarker.remove();
        entry.labelEl.style.transform = ""; // reset, maplibre gère le placement via anchor
        entry.labelMarker = new maplibregl.Marker({
          element: entry.labelEl, draggable: false, anchor: wantAnchor, offset: flip ? [-10, 0] : [10, 0],
        }).setLngLat(entry.marker.getLngLat()).addTo(map);
        entry.labelAnchor = wantAnchor;
        // Le point et son écouteur "drag" (bindMissionDrag, attaché une fois à la création)
        // lisent entry.labelMarker dynamiquement — seule l'étiquette (recréée ci-dessus) a
        // besoin qu'on lui réattache ses propres écouteurs.
        if (entry.kind === "mission") bindLabelDrag(entry);
      }
    }
  }

  // Glisser le point OU l'étiquette déplace la mission — les deux marqueurs restent
  // synchronisés pendant le geste, pas seulement à la fin. Les callbacks lisent
  // entry.marker/entry.labelMarker au moment du drag (pas de closure figée sur l'objet
  // Marker initial), donc restent valides même après qu'updateAllLabelFlips ait recréé le
  // Marker de l'étiquette.
  //
  // Glissement "maison" (pointerdown/move/up), plutôt que le draggable natif de MapLibre :
  // celui-ci recalcule la position à partir du déplacement cumulé de la souris depuis le
  // début du geste, donc un setLngLat fait pendant son propre événement "drag" serait écrasé
  // au mouvement suivant — impossible d'aimanter le point pendant qu'on le tient tout en
  // gardant la main dessus. En pilotant nous-mêmes le pointeur, chaque mouvement recalcule la
  // position réelle du curseur sur la carte, l'aimante si un chemin est assez proche, et rend
  // la main dès qu'on s'en éloigne à nouveau — exactement le comportement "magnétique" voulu.
  function bindMissionDrag(entry) {
    attachSnapDrag(entry.marker.getElement(), entry);
    bindLabelDrag(entry);
  }
  function bindLabelDrag(entry) {
    attachSnapDrag(entry.labelMarker.getElement(), entry);
  }
  function attachSnapDrag(el, entry) {
    el.addEventListener("pointerdown", (e) => {
      if (e.button != null && e.button !== 0) return;
      e.preventDefault();
      e.stopPropagation();
      const map = mapRef.current;
      el.style.cursor = "grabbing";
      map.dragPan.disable();
      if (el.setPointerCapture) { try { el.setPointerCapture(e.pointerId); } catch (err) {} }

      // Poignée, pas téléportation : on suit le DÉPLACEMENT du curseur depuis le début du
      // geste (delta), appliqué à la position de départ du point — l'étiquette (ou le point)
      // reste sous la main exactement où on l'a saisie, elle ne fait pas sauter le point sous
      // la souris. L'aimantation s'applique ensuite à cette position candidate (recalculée
      // depuis le début à chaque mouvement, jamais cumulée sur l'état déjà aimanté).
      const rect0 = map.getContainer().getBoundingClientRect();
      const startCursor = map.unproject([e.clientX - rect0.left, e.clientY - rect0.top]);
      const startMarker = entry.marker.getLngLat();
      const startCursorXY = metersFrom(SNAP_REF, { lon: startCursor.lng, lat: startCursor.lat });
      const startMarkerXY = metersFrom(SNAP_REF, { lon: startMarker.lng, lat: startMarker.lat });

      function onMove(ev) {
        const rect = map.getContainer().getBoundingClientRect();
        const cur = map.unproject([ev.clientX - rect.left, ev.clientY - rect.top]);
        const curXY = metersFrom(SNAP_REF, { lon: cur.lng, lat: cur.lat });
        const candidateXY = {
          x: startMarkerXY.x + (curXY.x - startCursorXY.x),
          y: startMarkerXY.y + (curXY.y - startCursorXY.y),
        };
        const candidateLL = lonLatFrom(SNAP_REF, candidateXY);
        const snapped = snapLonLat(candidateLL.lon, candidateLL.lat);
        entry.marker.setLngLat([snapped.lon, snapped.lat]);
        if (entry.labelMarker) entry.labelMarker.setLngLat([snapped.lon, snapped.lat]);
      }
      function onUp() {
        window.removeEventListener("pointermove", onMove);
        window.removeEventListener("pointerup", onUp);
        el.style.cursor = "grab";
        map.dragPan.enable();
        const ll = entry.marker.getLngLat();
        onMissionMoved(entry.id, ll.lng, ll.lat);
      }
      window.addEventListener("pointermove", onMove);
      window.addEventListener("pointerup", onUp);
    });
  }

  function syncMissionMarker(map, m) {
    const key = "mission:" + m.id;
    const mt = MISSION_TYPES[m.type];
    const isSelected = selected && selected.kind === "mission" && selected.id === m.id;
    let entry = markersRef.current.get(key);

    if (!entry) {
      const el = document.createElement("div");
      el.style.cssText = "width:16px;height:16px;cursor:grab;touch-action:none;z-index:3;";
      const inner = document.createElement("div");
      inner.style.cssText = "width:100%;height:100%;transform:rotate(45deg);border-radius:2px;box-sizing:border-box;box-shadow:0 1px 2px rgba(0,0,0,0.6);";
      el.appendChild(inner);
      el.addEventListener("click", (ev) => { ev.stopPropagation(); onSelect({ kind: "mission", id: m.id }); });

      const labelEl = document.createElement("div");
      labelEl.style.cssText = "cursor:grab;touch-action:none;white-space:nowrap;display:flex;flex-direction:column;background:rgba(10,13,15,0.82);border-radius:5px;padding:4px 9px;line-height:1.25;z-index:3;";
      labelEl.addEventListener("click", (ev) => { ev.stopPropagation(); onSelect({ kind: "mission", id: m.id }); });
      const labelMarker = new maplibregl.Marker({ element: labelEl, draggable: false, anchor: "left", offset: [10, 0] })
        .setLngLat([m.x, m.y]).addTo(map);

      const marker = new maplibregl.Marker({ element: el, draggable: false, anchor: "center" })
        .setLngLat([m.x, m.y]).addTo(map);

      entry = { marker, labelMarker, labelEl, labelAnchor: "left", kind: "mission", id: m.id };
      markersRef.current.set(key, entry);
      bindMissionDrag(entry);
    } else {
      entry.marker.setLngLat([m.x, m.y]);
      entry.labelMarker.setLngLat([m.x, m.y]);
    }

    const inner = entry.marker.getElement().firstChild;
    inner.style.background = mt.color;
    inner.style.border = `2px solid ${isSelected ? "#fff" : STATUS[m.status].color}`;

    entry.labelEl.style.border = `1px solid ${mt.color}66`;
    entry.labelEl.innerHTML =
      `<span style="color:#e8edf0;font-weight:700;font-size:13.5px;">${escapeHtml(m.label)}</span>` +
      `<span style="color:${STATUS[m.status].color};font-size:12px;">${STATUS[m.status].label}</span>`;
  }

  function syncFireMarker(map, f) {
    const key = "fire:" + f.id;
    const isSelected = selected && selected.kind === "fire" && selected.id === f.id;
    let entry = markersRef.current.get(key);
    // Toujours rouge sur la carte, quel que soit le statut (contrairement au badge de statut
    // dans FirePanel) : repérer un feu/fumée signalé en un coup d'œil doit rester possible
    // sans avoir à se souvenir de quelle couleur correspond à quelle étape.
    const color = "#ef4444";

    if (!entry) {
      const el = document.createElement("div");
      el.style.cssText = "width:26px;height:26px;border-radius:50%;cursor:grab;touch-action:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;box-shadow:0 1px 2px rgba(0,0,0,0.6);z-index:3;";
      ReactDOM.createRoot(el).render(React.createElement(Icons.Flame, { size: 15, color: "#fff" }));
      el.addEventListener("click", (ev) => { ev.stopPropagation(); onSelect({ kind: "fire", id: f.id }); });

      const marker = new maplibregl.Marker({ element: el, draggable: true, anchor: "center" })
        .setLngLat([f.x, f.y]).addTo(map);
      marker.on("dragend", () => {
        const ll = marker.getLngLat();
        onFireMoved(f.id, ll.lng, ll.lat);
      });
      entry = { marker, labelMarker: null, kind: "fire" };
      markersRef.current.set(key, entry);
    } else {
      entry.marker.setLngLat([f.x, f.y]);
    }

    const el = entry.marker.getElement();
    el.style.background = color;
    el.style.border = `2px solid ${isSelected ? "#fff" : color}`;
    el.style.color = color;
    el.style.animation = f.status !== "eteint" ? "dfci-pulse 1.6s infinite" : "none";
  }

  // ---- Aperçu de triangulation (lignes de visée + point estimé) ----
  useEffect(() => {
    if (!mapReady) return;
    const map = mapRef.current;
    const src = map.getSource("triangulation-lines");
    if (!src) return;

    // retire l'ancien marqueur d'aperçu s'il existe
    if (markersRef.current.has("preview")) {
      markersRef.current.get("preview").marker.remove();
      markersRef.current.delete("preview");
    }

    if (!firePicker || firePicker.mode !== "triangulate") {
      src.setData({ type: "FeatureCollection", features: [] });
      return;
    }
    const A = missions.find((v) => v.id === firePicker.vigieA);
    const B = missions.find((v) => v.id === firePicker.vigieB);
    const features = [];
    if (A && firePicker.azA != null) {
      const end = sightlineRayEnd({ lon: A.x, lat: A.y }, firePicker.azA, 4000);
      features.push({ type: "Feature", geometry: { type: "LineString", coordinates: [[A.x, A.y], [end.lon, end.lat]] } });
    }
    if (B && firePicker.azB != null) {
      const end = sightlineRayEnd({ lon: B.x, lat: B.y }, firePicker.azB, 4000);
      features.push({ type: "Feature", geometry: { type: "LineString", coordinates: [[B.x, B.y], [end.lon, end.lat]] } });
    }
    src.setData({ type: "FeatureCollection", features });

    if (A && B && A.id !== B.id && firePicker.azA != null && firePicker.azB != null) {
      const r = intersectSightlines({ lon: A.x, lat: A.y }, firePicker.azA, { lon: B.x, lat: B.y }, firePicker.azB);
      if (!r.error) {
        const el = document.createElement("div");
        el.style.cssText = "width:18px;height:18px;border-radius:50%;border:2px dashed #fb923c;pointer-events:none;";
        const marker = new maplibregl.Marker({ element: el, anchor: "center" }).setLngLat([r.lon, r.lat]).addTo(map);
        markersRef.current.set("preview", { marker, kind: "preview" });
      }
    }
    // eslint-disable-next-line
  }, [mapReady, firePicker, missions]);

  return React.createElement(
    "div",
    { style: { position: "relative", width: "100%", height: "100%", borderRadius: 10, overflow: "hidden", border: "1px solid var(--border)" } },
    React.createElement("div", { ref: containerRef, style: { position: "absolute", inset: 0, cursor: placing ? "crosshair" : "" } }),
    placing && React.createElement("div", {
      style: { position: "absolute", top: 10, left: 10, zIndex: 5, background: "rgba(15,20,23,0.9)", color: "#fbbf24", fontSize: 12.5, padding: "7px 12px", borderRadius: 6, display: "flex", alignItems: "center", gap: 6 },
    }, React.createElement(Icons.Info, { size: 14 }), " Clique sur la carte pour placer le feu"),
    // Décalé à droite des boutons Relief 3D/Grille DFCI/Satellite (empilés en bas à gauche,
    // ~104px de large) pour ne pas les recouvrir pendant le chargement.
    dfciStatus && React.createElement("div", {
      className: "dfci-status-banner",
      style: { position: "absolute", bottom: 10, left: 128, zIndex: 5, background: "rgba(15,20,23,0.85)", color: "#e8edf0", fontSize: 11.5, padding: "7px 11px", borderRadius: 6, maxWidth: 280 },
    }, dfciStatus)
  );
}

function escapeHtml(s) {
  return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
