const { useState: useState2, useRef: useRef2, useEffect: useEffect2 } = React;
const {
  Flame, Radio, ShieldAlert, MapPin, Clock, Plus, X, Check,
  LogIn, LogOut, Info, ChevronRight, Crosshair,
  MousePointerClick, ArrowLeft, ArrowDown, ArrowUp, Users, Search, Flag,
  Menu, LayoutDashboard, SunMoon, Settings, Download,
} = Icons;

// Palette pour distinguer les groupes de l'effectif d'un coup d'œil. Volontairement sans le
// vert (#34d399, "Présent") ni l'orange (#fb923c, "Sorti"/"Hors du camp") : un groupe ne doit
// jamais pouvoir se confondre avec la couleur de statut d'une personne inscrite seule, sans
// groupe. L'attribution (voir buildGroupColorMap) est par ordre alphabétique plutôt que par
// hachage, pour garantir qu'aucun groupe ne partage sa couleur avec un autre.
const GROUP_COLOR_PALETTE = ["#4d9fef", "#c084fc", "#f87171", "#22d3ee", "#ec4899", "#818cf8", "#facc15", "#fb7185", "#38bdf8", "#a78bfa", "#2dd4bf", "#f472b6"];
// `extraNames` : autres étiquettes de regroupement à colorer de façon unique en plus des
// groupes d'inscription — sert aux noms de mission utilisés pour regrouper "Hors du camp"
// (cf. EffectifTab), pour qu'une mission n'hérite jamais de la couleur d'un groupe existant.
function buildGroupColorMap(roster, extraNames) {
  const regNames = Array.from(new Set(roster.map((p) => p.group).filter(Boolean)));
  const extra = (extraNames || []).filter((n) => !regNames.includes(n));
  const names = [...regNames, ...extra].sort((a, b) => a.localeCompare(b));
  return new Map(names.map((name, i) => [name, GROUP_COLOR_PALETTE[i % GROUP_COLOR_PALETTE.length]]));
}

// Motifs de sortie proposés en boutons dans le formulaire entrée/sortie — "Autre" ouvre un
// champ texte libre juste en dessous.
const MOTIF_PRESETS = ["Mission", "Courses", "Médical", "Logistique", "Dépose Gare", "Sortie définitive", "Autre"];
// Groupe par défaut toujours proposé à l'inscription, pour les personnes qui ne font partie
// d'aucun groupe constitué (ex. staff isolé, visiteur) — couleur dédiée, cf. groupButtons.
const INVITES_GROUP = "Invités";

const inputStyle = {
  width: "100%", padding: "8px 10px", borderRadius: 7, border: "1px solid var(--border)",
  background: "var(--bg3)", color: "var(--text)", fontSize: 12.5, marginTop: 4, marginBottom: 10, boxSizing: "border-box",
};
const selectStyle = {
  padding: "7px 8px", borderRadius: 7, border: "1px solid var(--border)", background: "var(--bg3)",
  color: "var(--text)", fontSize: 12,
};

// Écran de connexion (Firebase Auth, email/mot de passe) — affiché tant qu'aucun utilisateur
// n'est authentifié. Les comptes du staff sont créés à la main par le coordinateur dans la
// console Firebase (Authentication > Users), pas d'auto-inscription depuis cet écran.
function LoginScreen({ onLogin }) {
  const [email, setEmail] = useState2("");
  const [password, setPassword] = useState2("");
  const [error, setError] = useState2("");
  const [loading, setLoading] = useState2(false);

  function submit(e) {
    e.preventDefault();
    if (!email.trim() || !password || loading) return;
    setLoading(true);
    setError("");
    onLogin(email.trim(), password).catch((err) => {
      const badCreds = ["auth/invalid-credential", "auth/wrong-password", "auth/user-not-found", "auth/invalid-email"];
      setError(badCreds.includes(err.code) ? "Email ou mot de passe incorrect." : "Connexion impossible : " + err.message);
      setLoading(false);
    });
  }

  return (
    <div style={{ height: "100vh", display: "flex", alignItems: "center", justifyContent: "center", background: "#0f1417", fontFamily: "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif" }}>
      <form onSubmit={submit} style={{ width: 320, background: "#161d22", border: "1px solid #2a343a", borderRadius: 12, padding: 24, boxSizing: "border-box" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
          <ShieldAlert size={22} color="#4d9fef" />
          <span style={{ fontSize: 17, fontWeight: 700, color: "#e8edf0" }}>VigiCamp</span>
        </div>
        <div style={{ fontSize: 12, color: "#8b9aa3", marginBottom: 20 }}>Connexion de l'équipe de coordination</div>

        <label style={{ fontSize: 11.5, color: "#8b9aa3", fontWeight: 600 }}>Email</label>
        <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} autoFocus autoComplete="username"
          style={{ width: "100%", marginTop: 4, marginBottom: 12, padding: "9px 10px", borderRadius: 7, border: "1px solid #2a343a", background: "#0f1417", color: "#e8edf0", fontSize: 13, boxSizing: "border-box" }} />

        <label style={{ fontSize: 11.5, color: "#8b9aa3", fontWeight: 600 }}>Mot de passe</label>
        <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password"
          style={{ width: "100%", marginTop: 4, marginBottom: 16, padding: "9px 10px", borderRadius: 7, border: "1px solid #2a343a", background: "#0f1417", color: "#e8edf0", fontSize: 13, boxSizing: "border-box" }} />

        {error && <div style={{ color: "#f87171", fontSize: 11.5, marginBottom: 12 }}>{error}</div>}

        <button type="submit" disabled={loading} style={{
          width: "100%", padding: "10px", borderRadius: 7, border: "none", fontWeight: 700, fontSize: 13,
          background: loading ? "#2a343a" : "#4d9fef", color: loading ? "#8b9aa3" : "#0a0d0f", cursor: loading ? "not-allowed" : "pointer",
        }}>{loading ? "Connexion…" : "Se connecter"}</button>
      </form>
    </div>
  );
}

function Header({ redDay, now, menuOpen, setMenuOpen, lightMode, setLightMode, setTab }) {
  return (
    <div className="app-header" style={{
      display: "flex", alignItems: "center", justifyContent: "space-between",
      padding: "14px 20px", borderBottom: "1px solid var(--border)", background: "var(--bg1)",
    }}>
      <div className="app-header-brand" style={{ display: "flex", alignItems: "center", gap: 10 }}>
        <button onClick={() => setMenuOpen(true)} style={{ background: "none", border: "none", color: "var(--text)", cursor: "pointer", display: "flex", padding: 2 }}>
          <Menu size={20} />
        </button>
        <ShieldAlert size={22} color={redDay ? "#f87171" : "#4d9fef"} />
        <div>
          <div className="app-header-title" style={{ fontWeight: 700, fontSize: 15, letterSpacing: 0.3 }}>VigiCamp — DFCI Calanques</div>
          <div className="app-header-subtitle" style={{ fontSize: 12, color: "var(--text-muted)" }}>Camp Nature Environnement · Luminy</div>
        </div>
      </div>
      <div className="app-header-right" style={{ display: "flex", alignItems: "center", gap: 16 }}>
        <div style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 13, color: "var(--text-muted)" }}>
          {new Date(now).toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}
        </div>
        <div className="app-header-redday" style={{
          display: "flex", alignItems: "center", gap: 8,
          padding: "7px 14px", borderRadius: 8, fontWeight: 600, fontSize: 13,
          border: `1px solid ${redDay ? "#f87171" : "#34d399"}`,
          background: redDay ? "#f87171" : "transparent",
          color: redDay ? "#1a0f0f" : "#34d399",
        }}>
          {redDay ? "Journée rouge" : "Journée verte"}
        </div>
      </div>
      {menuOpen && <SideMenu onClose={() => setMenuOpen(false)} lightMode={lightMode} setLightMode={setLightMode} setTab={setTab} />}
    </div>
  );
}

function SideMenu({ onClose, lightMode, setLightMode, setTab }) {
  const items = [
    { icon: LayoutDashboard, label: "Tableau de bord" },
    { icon: SunMoon, label: "Thème Clair/Sombre", onClick: () => setLightMode(!lightMode), active: lightMode },
    { icon: Settings, label: "Paramètres", onClick: () => { setTab("settings"); onClose(); } },
    { icon: LogOut, label: "Se déconnecter", onClick: () => { window.fb.signOut(window.fb.auth); onClose(); } },
  ];
  return (
    <>
      <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: 100 }} />
      <div style={{
        position: "fixed", top: 0, left: 0, bottom: 0, width: 250, maxWidth: "80vw",
        background: "var(--bg2)", borderRight: "1px solid var(--border)", zIndex: 101,
        boxShadow: "4px 0 16px rgba(0,0,0,0.4)", display: "flex", flexDirection: "column",
      }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "14px 16px", borderBottom: "1px solid var(--border)" }}>
          <span style={{ fontWeight: 700, fontSize: 13.5, color: "var(--text)" }}>Menu</span>
          <button onClick={onClose} style={{ background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", display: "flex" }}>
            <X size={18} />
          </button>
        </div>
        <div style={{ display: "flex", flexDirection: "column", padding: 8 }}>
          {items.map((it) => {
            const IconC = it.icon;
            return (
              <button key={it.label} onClick={it.onClick} style={{
                display: "flex", alignItems: "center", gap: 12, padding: "12px 12px", borderRadius: 8,
                color: "var(--text)", fontSize: 13.5, fontWeight: 600, cursor: it.onClick ? "pointer" : "default",
                background: it.active ? "#4d9fef22" : "none", border: "none", width: "100%", textAlign: "left",
              }}>
                <IconC size={17} color="#4d9fef" />
                {it.label}
                {it.active !== undefined && <span style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-muted)" }}>{it.active ? "Clair" : "Sombre"}</span>}
              </button>
            );
          })}
        </div>
      </div>
    </>
  );
}

function SettingsPage({ redDay, setRedDay, setTab }) {
  return (
    <div style={{ padding: "16px", maxWidth: 480 }}>
      <button onClick={() => setTab("carte")} style={{
        display: "flex", alignItems: "center", gap: 6, background: "none", border: "none",
        color: "var(--text-muted)", cursor: "pointer", fontSize: 13, fontWeight: 600, padding: 0, marginBottom: 16,
      }}>
        <ArrowLeft size={15} /> Retour
      </button>
      <div style={{ fontSize: 18, fontWeight: 700, marginBottom: 16 }}>Paramètres</div>

      <div style={{ background: "var(--bg2)", border: "1px solid var(--border)", borderRadius: 10, padding: 16 }}>
        <div style={{ fontSize: 13, fontWeight: 700, marginBottom: 4 }}>Couleur du jour</div>
        <div style={{ fontSize: 11.5, color: "var(--text-muted)", marginBottom: 12 }}>
          Affichée en haut de l'écran ; en journée rouge, le motif de sortie reste obligatoire comme en journée verte.
        </div>
        <div style={{ display: "flex", gap: 10 }}>
          <button onClick={() => setRedDay(false)} style={{
            flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 6, cursor: "pointer",
            padding: "14px 10px", borderRadius: 9, fontWeight: 700, fontSize: 13,
            border: `2px solid ${!redDay ? "#34d399" : "var(--border)"}`,
            background: !redDay ? "#34d39922" : "var(--bg3)",
            color: !redDay ? "#34d399" : "var(--text-muted)",
          }}>
            Journée verte
          </button>
          <button onClick={() => setRedDay(true)} style={{
            flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 6, cursor: "pointer",
            padding: "14px 10px", borderRadius: 9, fontWeight: 700, fontSize: 13,
            border: `2px solid ${redDay ? "#f87171" : "var(--border)"}`,
            background: redDay ? "#f8717122" : "var(--bg3)",
            color: redDay ? "#f87171" : "var(--text-muted)",
          }}>
            Journée rouge
          </button>
        </div>
      </div>
    </div>
  );
}

function Tabs({ tab, setTab }) {
  const items = [
    { id: "carte", label: "Carte & missions", icon: MapPin },
    { id: "effectif", label: "Effectif & Mouvements", icon: Users },
    { id: "journal", label: "Journal", icon: Clock },
  ];
  return (
    <div className="app-tabs" style={{ display: "flex", gap: 4, padding: "10px 16px 0" }}>
      {items.map((it) => {
        const IconC = it.icon;
        const active = tab === it.id;
        return (
          <button key={it.id} onClick={() => setTab(it.id)} className="app-tab-btn" style={{
            display: "flex", alignItems: "center", gap: 7, cursor: "pointer",
            padding: "8px 14px", borderRadius: "8px 8px 0 0", fontSize: 13, fontWeight: 600,
            border: "none", borderBottom: active ? "2px solid #4d9fef" : "2px solid transparent",
            background: active ? "var(--bg2)" : "transparent", color: active ? "var(--text)" : "var(--text-muted)",
          }}>
            <IconC size={15} /> <span className="app-tab-label">{it.label}</span>
          </button>
        );
      })}
    </div>
  );
}

function Toolbar({
  placing, setPlacing, openNamePicker, namePicker, setNamePicker, presets, addPreset, confirmPlacement,
  firePicker, setFirePicker, vigies, placeFireDirect, confirmTriangulatedFire, roster, entries,
}) {
  const btnBase = { display: "flex", alignItems: "center", gap: 6, cursor: "pointer", padding: "8px 12px", borderRadius: 8, fontSize: 12.5, fontWeight: 600, border: "1px solid var(--border)" };
  return (
    <div style={{ marginBottom: 10 }}>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 8 }}>
        {Object.entries(MISSION_TYPES).map(([key, mt]) => (
          <button key={key} onClick={() => openNamePicker(key)} style={{ ...btnBase, background: "var(--bg2)", color: mt.color, borderColor: mt.color + "55" }}>
            <Plus size={14} /> {mt.label}
          </button>
        ))}
        <button onClick={() => setFirePicker(firePicker ? null : { mode: "choose", vigieA: "", azA: null, vigieB: "", azB: null })}
          style={{ ...btnBase, background: firePicker ? "#fb923c22" : "var(--bg2)", color: "#fb923c", borderColor: "#fb923c55" }}>
          <Flame size={14} /> Feu / fumée
        </button>
        {placing && (
          <button onClick={() => setPlacing(null)} style={{ ...btnBase, background: "#2a1414", color: "#f87171", borderColor: "#f8717155" }}>
            <X size={14} /> Annuler le placement
          </button>
        )}
      </div>
      {namePicker && (
        <NamePicker type={namePicker.type} presets={presets[namePicker.type]} roster={roster} entries={entries}
          onPick={(label, crew) => confirmPlacement(namePicker.type, label, crew)}
          onAdd={(name) => addPreset(namePicker.type, name)}
          onCancel={() => setNamePicker(null)} />
      )}
      {firePicker && (
        <FirePicker firePicker={firePicker} setFirePicker={setFirePicker} vigies={vigies}
          onDirect={placeFireDirect} onConfirm={confirmTriangulatedFire} onCancel={() => setFirePicker(null)} />
      )}
    </div>
  );
}

function NamePicker({ type, presets, roster, entries, onPick, onAdd, onCancel }) {
  const [value, setValue] = useState2("");
  const [crewQuery, setCrewQuery] = useState2("");
  const [selectedCrew, setSelectedCrew] = useState2([]);
  const mt = MISSION_TYPES[type];

  const present = roster.filter((p) => personStatus(entries, p.id).status === "present");
  const q = normalizeText(crewQuery.trim());
  const available = present
    .filter((p) => !selectedCrew.some((c) => c.personId === p.id))
    .filter((p) => !q || normalizeText(p.name).split(" ").some((w) => w.startsWith(q)) || normalizeText(p.name).startsWith(q));

  const canCreate = value.trim().length > 0 && selectedCrew.length > 0;

  function toggleAdd(p) { setSelectedCrew((c) => [...c, { personId: p.id, name: p.name }]); }
  function toggleRemove(personId) { setSelectedCrew((c) => c.filter((x) => x.personId !== personId)); }
  function create() {
    if (!canCreate) return;
    onAdd(value.trim());
    onPick(value.trim(), selectedCrew);
  }

  return (
    <div style={{ background: "var(--bg2)", border: `1px solid ${mt.color}55`, borderRadius: 10, padding: 12, marginBottom: 8 }}>
      <div style={{ fontSize: 12.5, fontWeight: 700, color: mt.color, marginBottom: 8 }}>
        Nom de la {mt.label.toLowerCase()} à créer — elle apparaîtra au camp, en mouvement
      </div>
      {presets.length > 0 && (
        <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 10 }}>
          {presets.map((p) => {
            // Le type est déjà annoncé juste au-dessus ("Nom de la vigie à créer…") et par le
            // bouton actif dans la barre d'outils — on n'affiche que le nom propre du preset
            // (ex. "Luminy" plutôt que "Vigie Luminy"), la valeur réellement créée/transmise
            // (onPick) garde elle le libellé complet pour rester cohérente avec le reste de
            // l'app (journal, liste des missions en cours, étiquette sur la carte…).
            const shortLabel = p.startsWith(mt.label + " ") ? p.slice(mt.label.length + 1) : p;
            return (
              <button key={p} onClick={() => setValue(p)} style={{
                padding: "5px 8px", borderRadius: 6, background: value === p ? mt.color : "var(--bg3)", border: `1px solid ${mt.color}55`, fontSize: 12,
                color: value === p ? "#0a0d0f" : "var(--text)", cursor: "pointer", fontWeight: value === p ? 700 : 400,
              }}>{shortLabel}</button>
            );
          })}
        </div>
      )}
      {presets.length === 0 && <div style={{ fontSize: 11.5, color: "var(--text-faint)", marginBottom: 10 }}>Aucun nom disponible pour ce type de mission pour l'instant.</div>}

      <div style={{ marginBottom: 10, padding: 10, borderRadius: 8, background: "var(--bg4)", border: `1px solid ${selectedCrew.length > 0 ? "#34d39955" : "#f8717155"}` }}>
        <div style={{ fontSize: 11.5, color: "var(--text-muted)", fontWeight: 600, marginBottom: 6, display: "flex", alignItems: "center", gap: 5 }}>
          <Users size={12} /> Personnes qui partent avec cette mission <span style={{ color: "#f87171" }}>(obligatoire)</span>
        </div>
        {selectedCrew.length > 0 && (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 5, marginBottom: 8 }}>
            {selectedCrew.map((c) => (
              <div key={c.personId} style={{ display: "flex", alignItems: "center", gap: 4, padding: "4px 7px", borderRadius: 6, background: "#34d39922", border: "1px solid #34d39955", fontSize: 11.5, color: "var(--text)" }}>
                {c.name}
                <button onClick={() => toggleRemove(c.personId)} style={{ background: "none", border: "none", color: "var(--text-faint)", cursor: "pointer", display: "flex" }}><X size={11} /></button>
              </div>
            ))}
          </div>
        )}
        <input value={crewQuery} onChange={(e) => setCrewQuery(e.target.value)} placeholder="Chercher une personne présente (prénom)…"
          autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
          style={{ width: "100%", padding: "6px 8px", borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg3)", color: "var(--text)", fontSize: 12, boxSizing: "border-box", marginBottom: q ? 6 : 0 }} />
        {q.length > 0 && (
          <div style={{ display: "flex", flexDirection: "column", gap: 3, maxHeight: 140, overflowY: "auto" }}>
            {available.length === 0 && <div style={{ fontSize: 11, color: "var(--text-faint)", padding: "4px 2px" }}>Aucun résultat.</div>}
            {available.slice(0, 8).map((p) => (
              <button key={p.id} type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => { toggleAdd(p); setCrewQuery(""); }}
                style={{ textAlign: "left", padding: "6px 8px", borderRadius: 6, background: "var(--bg3)", border: "1px solid var(--border)", fontSize: 12, color: "var(--text)", cursor: "pointer" }}>+ {p.name}</button>
            ))}
          </div>
        )}
        {selectedCrew.length === 0 && <div style={{ fontSize: 11, color: "#f87171", marginTop: 6 }}>Sélectionne au moins une personne — une mission ne peut pas partir sans personne assignée.</div>}
      </div>

      <div style={{ display: "flex", gap: 8 }}>
        <button onClick={create} disabled={!canCreate} style={{
          flex: 1, padding: "9px", borderRadius: 7, border: "none", fontWeight: 700, fontSize: 12.5,
          cursor: canCreate ? "pointer" : "not-allowed", background: canCreate ? mt.color : "var(--border)", color: canCreate ? "#0a0d0f" : "var(--text-faint)",
        }}>Créer la mission</button>
        <button onClick={onCancel} style={{ padding: "9px 12px", borderRadius: 7, border: "1px solid var(--border)", background: "transparent", color: "var(--text-muted)", fontSize: 12, cursor: "pointer" }}>Annuler</button>
      </div>
    </div>
  );
}

function AzimuthControl({ value, onChange, size }) {
  const ref = useRef2(null);
  const s = size || 64;
  function handleClick(e) {
    const rect = ref.current.getBoundingClientRect();
    const cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2;
    const dx = e.clientX - cx, dy = e.clientY - cy;
    let deg = (Math.atan2(dx, -dy) * 180) / Math.PI;
    if (deg < 0) deg += 360;
    onChange(Math.round(deg));
  }
  const has = value != null;
  const rad = ((value || 0) * Math.PI) / 180;
  const nx = 50 + 38 * Math.sin(rad), ny = 50 - 38 * Math.cos(rad);
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
      <svg ref={ref} onClick={handleClick} width={s} height={s} viewBox="0 0 100 100"
        style={{ cursor: "crosshair", background: "var(--bg3)", borderRadius: "50%", border: "1px solid var(--border)", flexShrink: 0 }}>
        <circle cx="50" cy="50" r="46" fill="none" stroke="var(--border)" strokeWidth="1.5" />
        <text x="50" y="13" fill="var(--text-faint)" fontSize="9" textAnchor="middle">N</text>
        {has && <line x1="50" y1="50" x2={nx} y2={ny} stroke="#f2a93b" strokeWidth="3" strokeLinecap="round" />}
        {has && <circle cx={nx} cy={ny} r="4" fill="#f2a93b" />}
      </svg>
      <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
        <input type="number" min="0" max="359" value={value ?? ""} placeholder="—"
          onChange={(e) => onChange(e.target.value === "" ? null : ((Number(e.target.value) % 360) + 360) % 360)}
          style={{ width: 54, padding: "6px 8px", borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg3)", color: "var(--text)", fontSize: 12.5 }} />
        <span style={{ fontSize: 12, color: "var(--text-muted)" }}>°</span>
      </div>
    </div>
  );
}

function FirePicker({ firePicker, setFirePicker, vigies, onDirect, onConfirm, onCancel }) {
  const canTriangulate = vigies.length >= 2;

  if (firePicker.mode === "choose") {
    return (
      <div style={{ background: "var(--bg2)", border: "1px solid #fb923c55", borderRadius: 10, padding: 12, marginBottom: 8 }}>
        <div style={{ fontSize: 12.5, fontWeight: 700, color: "#fb923c", marginBottom: 10 }}>Comment situer ce feu / cette fumée ?</div>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
          <button onClick={onDirect} style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer", padding: "10px 14px", borderRadius: 8, border: "1px solid #fb923c55", background: "var(--bg3)", color: "var(--text)", fontSize: 12.5, fontWeight: 600 }}>
            <MousePointerClick size={15} color="#fb923c" /> Placer directement sur la carte
          </button>
          <button onClick={() => canTriangulate && setFirePicker({ ...firePicker, mode: "triangulate", error: null })} disabled={!canTriangulate}
            title={!canTriangulate ? "Il faut au moins 2 vigies placées sur la carte" : ""}
            style={{
              display: "flex", alignItems: "center", gap: 8, padding: "10px 14px", borderRadius: 8,
              border: `1px solid ${canTriangulate ? "#fb923c55" : "var(--border)"}`, background: "var(--bg3)",
              color: canTriangulate ? "var(--text)" : "#4a5560", fontSize: 12.5, fontWeight: 600, cursor: canTriangulate ? "pointer" : "not-allowed",
            }}>
            <Crosshair size={15} color={canTriangulate ? "#fb923c" : "#4a5560"} /> Trianguler avec deux vigies
          </button>
          <button onClick={onCancel} style={{ padding: "10px 12px", borderRadius: 8, border: "1px solid var(--border)", background: "transparent", color: "var(--text-muted)", fontSize: 12, cursor: "pointer" }}>Annuler</button>
        </div>
      </div>
    );
  }

  const A = vigies.find((v) => v.id === firePicker.vigieA);
  const B = vigies.find((v) => v.id === firePicker.vigieB);
  let preview = null;
  if (A && B && A.id !== B.id && firePicker.azA != null && firePicker.azB != null) {
    preview = intersectSightlines({ lon: A.x, lat: A.y }, firePicker.azA, { lon: B.x, lat: B.y }, firePicker.azB);
  }

  return (
    <div style={{ background: "var(--bg2)", border: "1px solid #fb923c55", borderRadius: 10, padding: 12, marginBottom: 8 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
        <button onClick={() => setFirePicker({ ...firePicker, mode: "choose" })} style={{ background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", display: "flex" }}><ArrowLeft size={15} /></button>
        <div style={{ fontSize: 12.5, fontWeight: 700, color: "#fb923c" }}>Trianguler à partir de deux vigies</div>
      </div>

      {[["A", "vigieA", "azA"], ["B", "vigieB", "azB"]].map(([letter, vKey, aKey]) => (
        <div key={letter} style={{ marginBottom: 10, paddingBottom: 10, borderBottom: letter === "A" ? "1px solid var(--border)" : "none" }}>
          <div style={{ fontSize: 11.5, color: "var(--text-muted)", fontWeight: 600, marginBottom: 6 }}>Vigie {letter}</div>
          <select value={firePicker[vKey]} onChange={(e) => setFirePicker({ ...firePicker, [vKey]: e.target.value, error: null })} style={{ ...selectStyle, marginBottom: 8, width: "100%" }}>
            <option value="">Choisir une vigie…</option>
            {vigies.map((v) => <option key={v.id} value={v.id}>{v.label}</option>)}
          </select>
          <AzimuthControl value={firePicker[aKey]} onChange={(az) => setFirePicker({ ...firePicker, [aKey]: az, error: null })} />
        </div>
      ))}

      {A && B && A.id === B.id && <div style={{ fontSize: 11.5, color: "#f87171", marginBottom: 8 }}>Choisis deux vigies différentes.</div>}
      {preview && preview.error && <div style={{ fontSize: 11.5, color: "#f87171", marginBottom: 8 }}>{preview.error}</div>}
      {preview && !preview.error && <div style={{ fontSize: 11.5, color: "#34d399", marginBottom: 8 }}>Point estimé — vérifie qu'il correspond bien au repère visé sur la carte avant de confirmer.</div>}

      <div style={{ display: "flex", gap: 8 }}>
        <button onClick={() => preview && !preview.error && onConfirm(preview, A.label, firePicker.azA, B.label, firePicker.azB)} disabled={!preview || preview.error}
          style={{
            flex: 1, padding: "9px", borderRadius: 7, border: "none", fontWeight: 700, fontSize: 12.5,
            cursor: preview && !preview.error ? "pointer" : "not-allowed",
            background: preview && !preview.error ? "#fb923c" : "var(--border)", color: preview && !preview.error ? "#0a0d0f" : "var(--text-faint)",
          }}>Confirmer ce point</button>
        <button onClick={onCancel} style={{ padding: "9px 12px", borderRadius: 7, border: "1px solid var(--border)", background: "transparent", color: "var(--text-muted)", fontSize: 12, cursor: "pointer" }}>Annuler</button>
      </div>
    </div>
  );
}

function Panel({ children, title, accent, onClose }) {
  return (
    <div style={{ background: "var(--bg2)", border: `1px solid ${accent}55`, borderRadius: 10, padding: 14 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
        <div style={{ fontWeight: 700, fontSize: 13.5, color: accent }}>{title}</div>
        {onClose && <button onClick={onClose} style={{ background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer" }}><X size={16} /></button>}
      </div>
      {children}
    </div>
  );
}

function MissionPanel({ mission, now, onPoint, onStatus, onReturnMission, onClose }) {
  const mt = MISSION_TYPES[mission.type];
  return (
    <Panel title={mission.label} accent={mt.color} onClose={onClose}>
      <div style={{ fontSize: 12, color: "var(--text-muted)", marginBottom: 10 }}>
        Dernier contact : {fmtTime(mission.lastContact)} ({fmtElapsed(mission.lastContact, now)})
      </div>
      <div style={{ fontSize: 11, color: "var(--text-faint)", marginBottom: 10, display: "flex", alignItems: "center", gap: 5 }}>
        <MousePointerClick size={12} /> Glisse le marqueur sur la carte pour ajuster sa position
      </div>
      <div style={{ display: "flex", gap: 6, marginBottom: 10, flexWrap: "wrap" }}>
        {Object.entries(STATUS).map(([k, s]) => (
          <button key={k} onClick={() => onStatus(k)} style={{
            padding: "6px 10px", borderRadius: 7, fontSize: 11.5, fontWeight: 600, cursor: "pointer",
            border: `1px solid ${s.color}`, background: mission.status === k ? s.color : "transparent", color: mission.status === k ? "#0f1417" : s.color,
          }}>{s.label}</button>
        ))}
      </div>
      {mission.crew && mission.crew.length > 0 && (
        <div style={{ marginBottom: 10 }}>
          <div style={{ fontSize: 11.5, color: "var(--text-muted)", marginBottom: 5, fontWeight: 600, display: "flex", alignItems: "center", gap: 5 }}>
            <Users size={12} /> Équipe sur place ({mission.crew.length})
          </div>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>
            {mission.crew.map((c) => (
              <div key={c.personId} style={{ padding: "3px 8px", borderRadius: 6, background: "var(--bg3)", border: `1px solid ${mt.color}55`, fontSize: 11.5, color: "var(--text)" }}>{c.name}</div>
            ))}
          </div>
        </div>
      )}
      <button onClick={onPoint} style={{
        width: "100%", padding: "9px", borderRadius: 7, border: "none", background: "#4d9fef", color: "#0a0d0f", fontWeight: 700, fontSize: 12.5, cursor: "pointer", marginBottom: 10,
        display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
      }}><Radio size={14} /> Pointage radio (maintenant)</button>
      <div style={{ fontSize: 11.5, color: "var(--text-muted)", marginBottom: 6, fontWeight: 600 }}>Historique</div>
      <div style={{ maxHeight: 56, overflowY: "auto", display: "flex", flexDirection: "column", gap: 4 }}>
        {mission.log.map((l, i) => (
          <div key={i} style={{ fontSize: 11.5, color: "var(--text)", display: "flex", gap: 8 }}>
            <span style={{ color: "var(--text-faint)", fontFamily: "ui-monospace, monospace" }}>{fmtTime(l.time)}</span>{l.note}
          </div>
        ))}
      </div>
      <button onClick={onReturnMission} style={{ marginTop: 10, width: "100%", background: "#34d399", border: "none", color: "#0a0d0f", borderRadius: 7, padding: "9px 10px", fontSize: 12.5, fontWeight: 700, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: 6 }}>
        <LogIn size={14} /> Faire rentrer la mission
      </button>
    </Panel>
  );
}

// Champs de la fiche de reconnaissance affichée dans FirePanel — mêmes items qu'un message de
// reconnaissance feu classique, hors localisation (déjà donnée par la position du point sur la
// carte). Stockés comme de simples libellés (voir MOTIF_PRESETS pour la même convention).
const FIRE_ENV_OPTIONS = ["Urbain", "Périurbain", "Forestier"];
const FIRE_MENACE_OPTIONS = ["Le massif", "Des bâtiments à proximité"];
const FIRE_SMOKE_COLOR_OPTIONS = ["Blanche", "Grise", "Noire", "Brune"];
const FIRE_SMOKE_SHAPE_OPTIONS = ["En filet", "En panache"];
const FIRE_SMOKE_TILT_OPTIONS = ["Verticale", "Couchée"];
const FIRE_DIRECTION_OPTIONS = ["N", "NE", "E", "SE", "S", "SO", "O", "NO"];
const FIRE_HEIGHT_OPTIONS = ["Sous la cime des arbres", "Au niveau de la cime", "Au-dessus de la cime", "Au-dessus de la colline"];

// Groupe de boutons à choix (unique ou multiple) pour un champ de la fiche de reconnaissance —
// `value` est soit un libellé (choix unique), soit un tableau de libellés (multi).
function FireChoiceGroup({ title, options, value, onChange, multi }) {
  const isSelected = (opt) => (multi ? (value || []).includes(opt) : value === opt);
  function toggle(opt) {
    if (multi) {
      const cur = value || [];
      onChange(cur.includes(opt) ? cur.filter((o) => o !== opt) : [...cur, opt]);
    } else {
      onChange(value === opt ? null : opt);
    }
  }
  return (
    <div style={{ marginBottom: 10 }}>
      <div style={{ fontSize: 11.5, color: "var(--text-muted)", fontWeight: 600, marginBottom: 4 }}>{title}</div>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
        {options.map((opt) => (
          <button key={opt} type="button" onClick={() => toggle(opt)} style={{
            padding: "5px 9px", borderRadius: 6, fontSize: 11, fontWeight: 600, cursor: "pointer",
            border: `1px solid ${isSelected(opt) ? "#f87171" : "var(--border)"}`,
            background: isSelected(opt) ? "#f87171" : "var(--bg3)",
            color: isSelected(opt) ? "#0a0d0f" : "var(--text)",
          }}>{opt}</button>
        ))}
      </div>
    </div>
  );
}

// Résumé en lecture seule de la fiche de reconnaissance (réaffiché à l'étape "Confirmé", pour
// avoir sous les yeux ce qui a été relevé au moment d'appeler le 18) — mêmes champs que
// FireChoiceGroup mais juste listés, plus de boutons à cette étape.
function FireReconSummary({ recon, gridRef }) {
  const r = recon || {};
  const rows = [
    ["Localisation", gridRef],
    ["Environnement", r.environnement],
    ["Menace", r.menace && r.menace.length > 0 ? r.menace.join(", ") : null],
    ["Couleur de la fumée", r.couleurFumee],
    ["Base des flammes visible", r.baseFlammesVisible],
    ["Forme de la fumée", r.formeFumee],
    ["Inclinaison", r.inclinaison],
    ["Direction de la fumée", r.direction],
    ["Hauteur de la fumée", r.hauteur],
  ].filter(([, v]) => v);
  if (rows.length === 0) {
    return <div style={{ fontSize: 11.5, color: "var(--text-faint)", marginBottom: 10 }}>Aucune information de reconnaissance renseignée.</div>;
  }
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 4, marginBottom: 12 }}>
      {rows.map(([label, value]) => (
        <div key={label} style={{ fontSize: 11.5, display: "flex", gap: 6 }}>
          <span style={{ color: "var(--text-faint)", flexShrink: 0 }}>{label} :</span>
          <span style={{ color: "var(--text)", fontWeight: 600 }}>{value}</span>
        </div>
      ))}
    </div>
  );
}

// Tunnel d'étapes : à tout instant, une seule action possible s'affiche (celle qui fait
// avancer l'étape en cours), pas une liste de statuts au choix — cf. les fonctions
// registerFireInfo / confirmFireStart / markFirefightersInformed / deleteFire dans app.jsx.
function FirePanel({ fire, onRegister, onConfirmFire, onFirefightersInformed, onFalseAlarm, onExtinguish, onUpdateRecon, onClose, dfciGrid }) {
  const recon = fire.recon || {};
  function set(field, value) { onUpdateRecon(fire.id, { [field]: value }); }
  const gridRef = findDfciGridRef(fire.x, fire.y, dfciGrid);
  const title = `Fumée / départ de feu${gridRef ? ` en ${gridRef}` : ""}`;
  return (
    <Panel title={title} accent="#ef4444" onClose={onClose}>
      <div style={{ fontSize: 12, color: "var(--text-muted)", marginBottom: 4 }}>Détecté à {fmtTime(fire.time)}</div>
      <div style={{ fontSize: 11, color: "var(--text-faint)", marginBottom: 10, display: "flex", alignItems: "center", gap: 5 }}>
        <MousePointerClick size={12} /> Glisse le marqueur pour affiner sa position
      </div>

      <div style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "3px 9px", borderRadius: 999, background: `${FIRE_COLOR[fire.status]}22`, border: `1px solid ${FIRE_COLOR[fire.status]}55`, marginBottom: 12 }}>
        <span style={{ width: 7, height: 7, borderRadius: "50%", background: FIRE_COLOR[fire.status] }} />
        <span style={{ fontSize: 11.5, fontWeight: 700, color: FIRE_COLOR[fire.status] }}>{FIRE_LABEL[fire.status]}</span>
      </div>

      {fire.status === "signale" && (
        <>
          <div style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text)", marginBottom: 8 }}>Reconnaissance</div>
          <FireChoiceGroup title="Environnement" options={FIRE_ENV_OPTIONS} value={recon.environnement} onChange={(v) => set("environnement", v)} />
          <FireChoiceGroup title="Menace" options={FIRE_MENACE_OPTIONS} value={recon.menace} onChange={(v) => set("menace", v)} multi />
          <FireChoiceGroup title="Couleur de la fumée" options={FIRE_SMOKE_COLOR_OPTIONS} value={recon.couleurFumee} onChange={(v) => set("couleurFumee", v)} />
          <FireChoiceGroup title="Base des flammes visible" options={["Oui", "Non"]} value={recon.baseFlammesVisible} onChange={(v) => set("baseFlammesVisible", v)} />
          <FireChoiceGroup title="Fumée en filet ou en panache" options={FIRE_SMOKE_SHAPE_OPTIONS} value={recon.formeFumee} onChange={(v) => set("formeFumee", v)} />
          <FireChoiceGroup title="Inclinaison" options={FIRE_SMOKE_TILT_OPTIONS} value={recon.inclinaison} onChange={(v) => set("inclinaison", v)} />
          <FireChoiceGroup title="Direction de la fumée" options={FIRE_DIRECTION_OPTIONS} value={recon.direction} onChange={(v) => set("direction", v)} />
          <FireChoiceGroup title="Hauteur de la fumée / à l'environnement" options={FIRE_HEIGHT_OPTIONS} value={recon.hauteur} onChange={(v) => set("hauteur", v)} />
          <button onClick={onRegister} style={{
            width: "100%", padding: "9px", borderRadius: 7, border: "none", background: FIRE_COLOR.enregistre, color: "#0a0d0f", fontWeight: 700, fontSize: 12.5, cursor: "pointer", marginTop: 4, marginBottom: 12,
            display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
          }}><ChevronRight size={14} /> Enregistrer</button>
        </>
      )}

      {fire.status === "enregistre" && (
        <>
          <div style={{ fontSize: 12, color: "var(--text-muted)", marginBottom: 10 }}>
            Fumée/départ de feu enregistré. S'agit-il réellement d'un départ de feu ?
          </div>
          <div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
            <button onClick={onFalseAlarm} style={{
              flex: 1, padding: "9px", borderRadius: 7, border: "1px solid var(--border)", background: "transparent", color: "var(--text-muted)", fontWeight: 600, fontSize: 12, cursor: "pointer",
            }}>Fausse alerte</button>
            <button onClick={onConfirmFire} style={{
              flex: 1, padding: "9px", borderRadius: 7, border: "none", background: FIRE_COLOR.confirme, color: "#0a0d0f", fontWeight: 700, fontSize: 12, cursor: "pointer",
            }}>Confirmer le départ de feu</button>
          </div>
        </>
      )}

      {fire.status === "confirme" && (
        <>
          <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", borderRadius: 8, background: "#ef444422", border: "1px solid #ef444455", marginBottom: 12 }}>
            <ShieldAlert size={18} color="#ef4444" />
            <div style={{ fontSize: 12.5, fontWeight: 700, color: "#ef4444" }}>Appeler le 18 — départ de feu confirmé</div>
          </div>
          <div style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text)", marginBottom: 8 }}>Informations relevées</div>
          <FireReconSummary recon={recon} gridRef={gridRef} />
          <button onClick={onFirefightersInformed} style={{
            width: "100%", padding: "9px", borderRadius: 7, border: "none", background: FIRE_COLOR.pompiers, color: "#0a0d0f", fontWeight: 700, fontSize: 12.5, cursor: "pointer", marginBottom: 12,
            display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
          }}><ChevronRight size={14} /> Pompiers informés</button>
        </>
      )}

      {fire.status === "pompiers" && (
        <>
          <div style={{ fontSize: 12, color: "var(--text-muted)", marginBottom: 10 }}>
            Les pompiers ont été informés. Ce point reste affiché sur la carte jusqu'à l'extinction du feu.
          </div>
          <button onClick={onExtinguish} style={{
            width: "100%", padding: "9px", borderRadius: 7, border: "none", background: FIRE_COLOR.eteint, color: "#0a0d0f", fontWeight: 700, fontSize: 12.5, cursor: "pointer", marginBottom: 12,
            display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
          }}><ChevronRight size={14} /> Feu éteint</button>
        </>
      )}

      <div style={{ height: 1, background: "var(--border)", margin: "4px 0 12px" }} />

      <div style={{ fontSize: 11.5, color: "var(--text-muted)", marginBottom: 6, fontWeight: 600 }}>Historique</div>
      <div style={{ maxHeight: 130, overflowY: "auto", display: "flex", flexDirection: "column", gap: 4 }}>
        {fire.log.map((l, i) => (
          <div key={i} style={{ fontSize: 11.5, color: "var(--text)", display: "flex", gap: 8 }}>
            <span style={{ color: "var(--text-faint)", fontFamily: "ui-monospace, monospace" }}>{fmtTime(l.time)}</span>{l.note}
          </div>
        ))}
      </div>
    </Panel>
  );
}

function RoutinePanel({ missions, now, onPoint, onSelect }) {
  const rows = [...missions].sort((a, b) => a.lastContact - b.lastContact);
  return (
    <Panel title="Missions en cours" accent="#4d9fef">
      {rows.length === 0 && <div style={{ fontSize: 12, color: "var(--text-faint)" }}>Aucune mission placée sur la carte.</div>}
      <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
        {rows.map((m) => {
          const min = (now - m.lastContact) / 60000;
          const color = min >= ROUTINE_DANGER_MIN ? "#f87171" : min >= ROUTINE_WARN_MIN ? "#fbbf24" : "#34d399";
          return (
            <div key={m.id} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "7px 9px", borderRadius: 7, background: "var(--bg4)", border: `1px solid ${color}44` }}>
              <div onClick={() => onSelect(m.id)} style={{ cursor: "pointer", flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                  {m.label}
                </div>
                <div style={{ fontSize: 11, color, display: "flex", alignItems: "center", gap: 4 }}>
                  <Clock size={11} /> {fmtElapsed(m.lastContact, now)}
                </div>
              </div>
              <button onClick={() => onPoint(m.id)} title="Pointage radio" style={{
                background: "#1c242a", border: "1px solid #3a444b", color: "#4d9fef", borderRadius: 6, width: 28, height: 28, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer",
              }}><Radio size={13} /></button>
            </div>
          );
        })}
      </div>
    </Panel>
  );
}

// Fond légèrement teinté de rouge (var(--bg2-red), déclinée par thème dans app.jsx) plutôt
// que le fond neutre des autres panneaux — permet de repérer ce panneau au premier coup
// d'œil comme signalant quelque chose d'urgent. N'apparaît que s'il y a au moins un feu non
// éteint (comme ActivityFeed, plutôt qu'une case vide permanente comme RoutinePanel).
function FiresPanel({ fires, now, onSelect, dfciGrid }) {
  const active = fires.filter((f) => f.status !== "eteint").sort((a, b) => a.time - b.time);
  if (active.length === 0) return null;
  return (
    <div style={{ marginTop: 12, background: "var(--bg2-red)", border: "1px solid #ef444455", borderRadius: 10, padding: 14 }}>
      <div style={{ fontWeight: 700, fontSize: 13.5, color: "#ef4444", marginBottom: 10 }}>Feux en cours</div>
      <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
        {active.map((f) => {
          const gridRef = findDfciGridRef(f.x, f.y, dfciGrid);
          return (
            <div key={f.id} onClick={() => onSelect(f.id)} style={{
              cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8,
              padding: "7px 9px", borderRadius: 7, background: "var(--bg4)", border: `1px solid ${FIRE_COLOR[f.status]}44`,
            }}>
              <div style={{ flex: 1, minWidth: 0, overflow: "hidden" }}>
                <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                  {FIRE_LABEL[f.status]}{gridRef ? ` · ${gridRef}` : ""}
                </div>
                <div style={{ fontSize: 11, color: FIRE_COLOR[f.status], display: "flex", alignItems: "center", gap: 4 }}>
                  <Clock size={11} /> {fmtElapsed(f.time, now)}
                </div>
              </div>
              <Flame size={14} color={FIRE_COLOR[f.status]} />
            </div>
          );
        })}
      </div>
    </div>
  );
}

// En-tête cliquable réduisant un panneau à son seul titre — largeur inchangée, juste la hauteur
// (cf. WeatherPanel/TipsPanel/ActivityFeed) : sert à libérer de la place pour "Missions en
// cours"/"Feux en cours" et le panneau de détail ouvert, sans faire disparaître complètement
// météo/astuces/activité récente.
function CollapsibleHeader({ children, collapsed, onToggle }) {
  return (
    <button onClick={onToggle} style={{
      width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 6,
      background: "none", border: "none", padding: 0, marginBottom: collapsed ? 0 : 8, cursor: "pointer", textAlign: "left",
    }}>
      <span style={{ fontWeight: 700, fontSize: 12.5, color: "var(--text-muted)", display: "flex", alignItems: "center", gap: 6 }}>{children}</span>
      <ChevronRight size={13} color="var(--text-faint)" style={{ transform: collapsed ? "none" : "rotate(90deg)", transition: "transform .1s", flexShrink: 0 }} />
    </button>
  );
}

function ActivityFeed({ activity, now }) {
  const [collapsed, setCollapsed] = useState2(true);
  if (activity.length === 0) return null;
  return (
    <div style={{ marginTop: 12, background: "var(--bg2)", border: "1px solid var(--border)", borderRadius: 10, padding: 12 }}>
      <CollapsibleHeader collapsed={collapsed} onToggle={() => setCollapsed((c) => !c)}>Activité récente</CollapsibleHeader>
      {!collapsed && (
        <div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
          {activity.map((a, i) => (
            <div key={i} style={{ fontSize: 12, display: "flex", gap: 8, alignItems: "baseline" }}>
              <span style={{ width: 6, height: 6, borderRadius: "50%", background: a.color, flexShrink: 0 }} />
              <span style={{ color: "var(--text-faint)", fontFamily: "ui-monospace, monospace", fontSize: 11 }}>{fmtTime(a.time)}</span>
              <span style={{ color: "var(--text)" }}>{a.text}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ===== Météo (Open-Meteo, prévisions Météo-France dont le modèle AROME pour la France
// métropolitaine à courte échéance) — pas d'API publique exploitable directement en client
// statique pour Météociel (site non prévu pour ça, pas de CORS), donc repli sur Open-Meteo qui
// s'appuie déjà sur les modèles Météo-France officiels et fonctionne sans clé ni serveur relais.
const WMO_WEATHER = {
  0: { label: "Ciel dégagé", icon: "☀️" },
  1: { label: "Peu nuageux", icon: "🌤️" },
  2: { label: "Partiellement nuageux", icon: "⛅" },
  3: { label: "Couvert", icon: "☁️" },
  45: { label: "Brouillard", icon: "🌫️" },
  48: { label: "Brouillard givrant", icon: "🌫️" },
  51: { label: "Bruine légère", icon: "🌦️" },
  53: { label: "Bruine modérée", icon: "🌦️" },
  55: { label: "Bruine dense", icon: "🌧️" },
  56: { label: "Bruine verglaçante", icon: "🌧️" },
  57: { label: "Bruine verglaçante forte", icon: "🌧️" },
  61: { label: "Pluie légère", icon: "🌧️" },
  63: { label: "Pluie modérée", icon: "🌧️" },
  65: { label: "Pluie forte", icon: "🌧️" },
  66: { label: "Pluie verglaçante", icon: "🌧️" },
  67: { label: "Pluie verglaçante forte", icon: "🌧️" },
  71: { label: "Neige légère", icon: "🌨️" },
  73: { label: "Neige modérée", icon: "🌨️" },
  75: { label: "Neige forte", icon: "❄️" },
  77: { label: "Neige en grains", icon: "❄️" },
  80: { label: "Averses légères", icon: "🌦️" },
  81: { label: "Averses modérées", icon: "🌧️" },
  82: { label: "Averses violentes", icon: "⛈️" },
  85: { label: "Averses de neige", icon: "🌨️" },
  86: { label: "Averses de neige fortes", icon: "❄️" },
  95: { label: "Orage", icon: "⛈️" },
  96: { label: "Orage avec grêle", icon: "⛈️" },
  99: { label: "Orage avec grêle fort", icon: "⛈️" },
};
function weatherInfo(code) {
  return WMO_WEATHER[code] || { label: "—", icon: "—" };
}
function windCompass(deg) {
  const dirs = ["N", "NE", "E", "SE", "S", "SO", "O", "NO"];
  return dirs[Math.round(deg / 45) % 8];
}
function weatherDayLabel(dateStr, i) {
  if (i === 0) return "Aujourd'hui";
  if (i === 1) return "Demain";
  const s = new Date(dateStr).toLocaleDateString("fr-FR", { weekday: "long" });
  return s.charAt(0).toUpperCase() + s.slice(1);
}

const WEATHER_REFRESH_MS = 30 * 60 * 1000; // 30 min — suffisant pour un outil de coordination sur plusieurs jours

function WeatherPanel() {
  const [data, setData] = useState2(null);
  const [error, setError] = useState2(null);
  const [collapsed, setCollapsed] = useState2(false);

  useEffect2(() => {
    function load() {
      const { lat, lon } = CONFIG.CAMP_POSITION;
      const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}` +
        `&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m` +
        `&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max,wind_gusts_10m_max` +
        `&timezone=Europe%2FParis&forecast_days=3`;
      fetch(url)
        .then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
        .then((d) => { setData(d); setError(null); })
        .catch((e) => setError(e.message));
    }
    load();
    const t = setInterval(load, WEATHER_REFRESH_MS);
    return () => clearInterval(t);
  }, []);

  return (
    <div style={{ marginTop: 12, background: "var(--bg2)", border: "1px solid var(--border)", borderRadius: 10, padding: 12 }}>
      <CollapsibleHeader collapsed={collapsed} onToggle={() => setCollapsed((c) => !c)}>Météo — Calanques (13009)</CollapsibleHeader>
      {!collapsed && (
        <>
          {error && <div style={{ fontSize: 11.5, color: "#f87171" }}>Météo indisponible : {error}</div>}
          {!error && !data && <div style={{ fontSize: 11.5, color: "var(--text-faint)" }}>Chargement de la météo…</div>}
          {data && (
            <>
              <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
                <span style={{ fontSize: 26 }}>{weatherInfo(data.current.weather_code).icon}</span>
                <div>
                  <div style={{ fontSize: 18, fontWeight: 800, color: "var(--text)" }}>
                    {Math.round(data.current.temperature_2m)}°C
                    <span style={{ fontSize: 11, fontWeight: 600, color: "var(--text-faint)" }}> (ressenti {Math.round(data.current.apparent_temperature)}°C)</span>
                  </div>
                  <div style={{ fontSize: 11.5, color: "var(--text-muted)" }}>{weatherInfo(data.current.weather_code).label}</div>
                </div>
              </div>
              <div style={{ fontSize: 11, color: "var(--text-faint)", marginBottom: 10 }}>
                Vent {Math.round(data.current.wind_speed_10m)} km/h ({windCompass(data.current.wind_direction_10m)}), rafales {Math.round(data.current.wind_gusts_10m)} km/h · Humidité {data.current.relative_humidity_2m}%
              </div>
              <div style={{ display: "flex", gap: 8 }}>
                {[1, 2].map((i) => (
                  <div key={i} style={{ flex: 1, background: "var(--bg3)", border: "1px solid var(--border)", borderRadius: 7, padding: "7px 8px", textAlign: "center" }}>
                    <div style={{ fontSize: 10.5, fontWeight: 700, color: "var(--text-muted)", marginBottom: 3 }}>{weatherDayLabel(data.daily.time[i], i)}</div>
                    <div style={{ fontSize: 18 }}>{weatherInfo(data.daily.weather_code[i]).icon}</div>
                    <div style={{ fontSize: 11, color: "var(--text)", fontWeight: 700 }}>
                      {Math.round(data.daily.temperature_2m_max[i])}° <span style={{ color: "var(--text-faint)", fontWeight: 600 }}>{Math.round(data.daily.temperature_2m_min[i])}°</span>
                    </div>
                    <div style={{ fontSize: 10, color: "var(--text-faint)" }}>💧 {data.daily.precipitation_probability_max[i]}%</div>
                  </div>
                ))}
              </div>
              <div style={{ fontSize: 9.5, color: "var(--text-faint)", marginTop: 8, textAlign: "right" }}>Données Météo-France via Open-Meteo</div>
            </>
          )}
        </>
      )}
    </div>
  );
}

// Petit rappel des manipulations de la carte qui ne sont pas évidentes au premier abord
// (raccourcis clavier/souris) — affiché en bas de la colonne de gauche de l'onglet Carte &
// missions, sous "Activité récente".
const MAP_TIPS = [
  "Ctrl + clic-glisser sur la carte : incliner la vue (passage en 3D)",
  "Clique-glisse un point ou l'étiquette d'une mission : déplacer la mission",
];
function TipsPanel() {
  const [collapsed, setCollapsed] = useState2(false);
  return (
    <div style={{ marginTop: 12, background: "var(--bg2)", border: "1px solid var(--border)", borderRadius: 10, padding: 12 }}>
      <CollapsibleHeader collapsed={collapsed} onToggle={() => setCollapsed((c) => !c)}>
        <Info size={13} /> Astuces
      </CollapsibleHeader>
      {!collapsed && (
        <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
          {MAP_TIPS.map((tip, i) => (
            <div key={i} style={{ fontSize: 11.5, color: "var(--text-faint)", display: "flex", gap: 7, alignItems: "flex-start" }}>
              <MousePointerClick size={12} style={{ flexShrink: 0, marginTop: 2 }} />
              <span>{tip}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function EffectifTab({ roster, setRoster, entries, setEntries, onArrival, onSortie, onReturn, vehicles, setVehicles, missions, onCreateMission, presets }) {
  const [query, setQuery] = useState2("");
  const [bulkText, setBulkText] = useState2("");
  const [groupText, setGroupText] = useState2("");
  const [vehicleBulkText, setVehicleBulkText] = useState2("");
  const [vehiclePlate, setVehiclePlate] = useState2("");
  // Sortie en attente de confirmation dans le formulaire central (motif/véhicule) — une
  // personne ou tout un groupe. Cliquer "Sortie" ne sort plus personne directement.
  const [pendingSortie, setPendingSortie] = useState2(null); // { people: [{id,name}], label }
  // Ajoute des personnes à la sortie en attente plutôt que de la remplacer — cliquer
  // "Sortie" sur une personne puis sur une autre (ou sur un groupe) doit cumuler tout le
  // monde dans le même panneau, pas repartir de zéro à chaque clic.
  function queueSortie(people) {
    setPendingSortie((cur) => {
      const existing = cur ? cur.people : [];
      const existingIds = new Set(existing.map((p) => p.id));
      const additions = people.filter((p) => !existingIds.has(p.id));
      return { people: [...existing, ...additions] };
    });
  }

  // Une couleur unique par groupe (voir buildGroupColorMap) — recalculée à chaque rendu à
  // partir de l'effectif complet, pas seulement de la liste filtrée/affichée, pour que la
  // couleur d'un groupe ne change pas selon la colonne ou la recherche en cours.
  const groupColorMap = buildGroupColorMap(roster, missions.map((m) => m.label));
  function colorForGroup(name) { return groupColorMap.get(name) || "var(--text-faint)"; }

  // Groupes déjà utilisés dans l'effectif — proposés en suggestion (datalist) pour rattacher
  // facilement un nouvel arrivant à un groupe déjà sur le camp, sans retaper/mal orthographier.
  const existingGroups = Array.from(new Set(roster.map((p) => p.group).filter(Boolean))).sort((a, b) => a.localeCompare(b));
  // "Invités" est toujours proposé, même sans personne encore inscrite dedans — groupe
  // "fourre-tout" par défaut, distingué des groupes réels par une couleur différente.
  const groupButtons = [INVITES_GROUP, ...existingGroups.filter((g) => g !== INVITES_GROUP)];
  const canRegister = bulkText.trim().length > 0 && groupText.trim().length > 0;

  function addFromBulk() {
    const names = bulkText.split("\n").map((n) => n.trim()).filter(Boolean);
    if (names.length === 0) return;
    const group = groupText.trim() || null;
    setRoster((r) => {
      const existing = new Set(r.map((p) => normalizeText(p.name)));
      const additions = names.filter((n) => !existing.has(normalizeText(n))).map((n) => ({ id: uid("p"), name: n, group }));
      return [...r, ...additions];
    });
    setBulkText("");
    setGroupText("");
  }
  function addVehiclesFromBulk() {
    const names = vehicleBulkText.split("\n").map((n) => n.trim()).filter(Boolean);
    if (names.length === 0) return;
    const plate = names.length === 1 ? vehiclePlate.trim() || null : null; // plaque : un seul véhicule à la fois
    setVehicles((v) => {
      const existing = new Set(v.map((x) => normalizeText(x.name)));
      const additions = names.filter((n) => !existing.has(normalizeText(n))).map((n) => ({ id: uid("v"), name: n, plate }));
      return [...v, ...additions];
    });
    setVehicleBulkText("");
    setVehiclePlate("");
  }
  // Départ définitif du camp (motif "Sortie définitive" dans le formulaire central) : retire
  // ces personnes de l'effectif une fois leur sortie enregistrée dans le journal — remplace
  // le bouton "poubelle" retiré, pour éviter une suppression accidentelle en un clic.
  function removePeople(ids) {
    const idSet = new Set(ids);
    setRoster((r) => r.filter((p) => !idSet.has(p.id)));
  }

  const withStatus = roster.map((p) => ({ ...p, ...personStatus(entries, p.id), vehicule: personVehicle(entries, p.id), motif: personMotif(entries, p.id), missionId: personMissionId(entries, p.id) }));

  // Étiquette de regroupement "Hors du camp" : la RAISON de la sortie plutôt que le groupe
  // d'inscription — le nom de la mission en cours s'il y en a une, sinon le motif choisi (ex.
  // "Médical"), même si tout le monde dans ce lot vient du même groupe d'inscription (ex.
  // Bordeaux) : ce qui compte ici, c'est pourquoi ces gens sont sortis, pas avec qui ils sont
  // arrivés au camp. Le groupe d'inscription ne reste utilisé que pour "Présents sur le camp"
  // et en dernier recours ici, pour une sortie sans motif renseigné (jour non rouge).
  function awayGroupKey(p) {
    if (p.missionId) {
      const m = missions.find((mm) => mm.id === p.missionId);
      if (m) return m.label;
    }
    if (p.motif) return p.motif;
    return p.group;
  }
  const q = normalizeText(query.trim());
  const filtered = q
    ? withStatus.filter((p) => normalizeText(p.name).split(" ").some((w) => w.startsWith(q)) || normalizeText(p.name).startsWith(q))
    : withStatus;
  const order = { present: 0, sorti: 1, non_arrive: 2 };
  filtered.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));

  // Une personne mise en attente de sortie (bouton "Sortie"/"Sortir le groupe") disparaît de
  // "Présents sur le camp" — elle n'existe plus que dans le bloc Sortie tant que ce n'est pas
  // confirmé, pour éviter de la sélectionner une seconde fois par erreur.
  const pendingIds = new Set(pendingSortie ? pendingSortie.people.map((p) => p.id) : []);

  // Deux colonnes : présents sur le camp / hors du camp (sortis + jamais arrivés), plutôt
  // qu'une seule liste triée par statut — plus rapide à lire d'un coup d'œil.
  const presentList = filtered.filter((p) => p.status === "present" && !pendingIds.has(p.id));
  const awayList = filtered.filter((p) => p.status !== "present");

  function renderPersonRow(p, groupKeyOverride) {
    const meta = STATUS_META[p.status];
    // Sans groupe : couleur de statut (vert/orange) comme avant. Avec groupe : couleur du
    // groupe sur le point + le contour, pour repérer un groupe d'un coup d'œil — le statut
    // reste lisible via le petit texte (Présent/Sorti) et surtout via la colonne. `groupKeyOverride`
    // vient du regroupement affiché (mission en cours pour "Hors du camp", cf. awayGroupKey) plutôt
    // que systématiquement du groupe d'inscription, pour que le point colorré corresponde à
    // l'étiquette réellement affichée au-dessus.
    const groupKey = groupKeyOverride !== undefined ? groupKeyOverride : p.group;
    const accent = groupKey ? colorForGroup(groupKey) : meta.color;
    return (
      <div key={p.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 10px", borderRadius: 8, background: "var(--bg2)", border: `1px solid ${accent}55` }}>
        <div style={{ width: 8, height: 8, borderRadius: "50%", background: accent, flexShrink: 0 }} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 12.5, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", display: "flex", alignItems: "center", gap: 6 }}>
            <span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{p.name}</span>
            {p.vehicule && (
              <span style={{ fontSize: 10, fontWeight: 700, color: "#c084fc", background: "#c084fc22", border: "1px solid #c084fc55", borderRadius: 5, padding: "1px 6px", flexShrink: 0 }}>{p.vehicule}</span>
            )}
          </div>
          <div style={{ fontSize: 11, color: meta.color }}>{meta.label}{p.time ? ` · ${fmtTime(p.time)}` : ""}{p.motif ? ` · ${p.motif}` : ""}</div>
        </div>
        <div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
          {p.status !== "present" && (
            <button onClick={() => (p.status === "sorti" ? onReturn(p) : onArrival(p))} title="Marquer arrivée / retour" style={{
              display: "flex", alignItems: "center", gap: 4, background: "#34d399", border: "none", color: "#0a0d0f", borderRadius: 6, padding: "6px 9px", fontSize: 11, fontWeight: 700, cursor: "pointer",
            }}><LogIn size={12} /> {p.status === "sorti" ? "Retour" : "Arrivée"}</button>
          )}
          {p.status === "present" && (
            <button onClick={() => queueSortie([{ id: p.id, name: p.name }])} title="Préparer une sortie (motif/véhicule dans le panneau central)" style={{
              display: "flex", alignItems: "center", gap: 4, background: "transparent", border: "1px solid #fb923c55", color: "#fb923c", borderRadius: 6, padding: "6px 9px", fontSize: 11, fontWeight: 700, cursor: "pointer",
            }}><LogOut size={12} /> Sortie</button>
          )}
        </div>
      </div>
    );
  }

  // Regroupe une liste (déjà filtrée) par groupe — sert à afficher un bouton d'action en
  // masse par groupe, tout en gardant les actions individuelles ligne par ligne (ex : une
  // seule personne d'un groupe qui rentre pendant que le reste est encore dehors). Pour
  // "Hors du camp" (kind === "away"), le regroupement suit la mission en cours quand il y en
  // a une (awayGroupKey) — le groupe d'inscription ne sert plus que de repli pour une sortie
  // de groupe sans mission associée.
  function groupEntries(list, kind) {
    const groups = new Map();
    const ungrouped = [];
    for (const p of list) {
      const key = kind === "away" ? awayGroupKey(p) : p.group;
      if (key) {
        if (!groups.has(key)) groups.set(key, []);
        groups.get(key).push(p);
      } else {
        ungrouped.push(p);
      }
    }
    return { groups, ungrouped };
  }

  function handleGroupArrival(members) {
    members.forEach((p) => (p.status === "sorti" ? onReturn(p) : onArrival(p)));
  }

  function renderColumn(list, kind) {
    const { groups, ungrouped } = groupEntries(list, kind);
    const groupNames = Array.from(groups.keys()).sort((a, b) => a.localeCompare(b));
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
        {groupNames.map((name) => {
          const members = groups.get(name);
          const gcolor = colorForGroup(name);
          return (
            <div key={name} style={{ borderLeft: `3px solid ${gcolor}`, paddingLeft: 8 }}>
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, padding: "5px 4px", marginBottom: 6 }}>
                <div style={{ fontSize: 15, fontWeight: 800, color: gcolor, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                  {name} <span style={{ color: "var(--text-faint)", fontWeight: 400, fontSize: 12 }}>({members.length})</span>
                </div>
                {kind === "present" ? (
                  <button onClick={() => queueSortie(members.map((p) => ({ id: p.id, name: p.name })))} title="Préparer la sortie du groupe (motif/véhicule dans le panneau central)" style={{
                    display: "flex", alignItems: "center", gap: 4, background: "#fb923c", border: "none", color: "#0a0d0f", borderRadius: 6, padding: "6px 9px", fontSize: 11, fontWeight: 700, cursor: "pointer", flexShrink: 0, boxShadow: "0 1px 3px rgba(0,0,0,0.35)",
                  }}><LogOut size={12} /> Sortir le groupe</button>
                ) : (
                  <button onClick={() => handleGroupArrival(members)} title="Faire rentrer tout le groupe" style={{
                    display: "flex", alignItems: "center", gap: 4, background: "#34d399", border: "none", color: "#0a0d0f", borderRadius: 6, padding: "6px 9px", fontSize: 11, fontWeight: 700, cursor: "pointer", flexShrink: 0, boxShadow: "0 1px 3px rgba(0,0,0,0.35)",
                  }}><LogIn size={12} /> Faire rentrer le groupe</button>
                )}
              </div>
              <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>{members.map((p) => renderPersonRow(p, name))}</div>
            </div>
          );
        })}
        {ungrouped.length > 0 && <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>{ungrouped.map(renderPersonRow)}</div>}
      </div>
    );
  }

  return (
    <div className="effectif-wrap" style={{ padding: "12px 16px", display: "flex", gap: 16, flexWrap: "wrap" }}>
      <div className="effectif-left-col" style={{ flex: "0 0 320px", minWidth: 280, display: "flex", flexDirection: "column", gap: 12 }}>
        <div style={{ background: "var(--bg2)", border: "1px solid var(--border)", borderRadius: 10, padding: 14 }}>
          <div style={{ fontSize: 12.5, fontWeight: 700, color: "#4d9fef", marginBottom: 8 }}>Inscrire sur le camp</div>
          <div style={{ fontSize: 11, color: "var(--text-muted)", marginBottom: 6 }}>Un nom par ligne (colle une liste entière si besoin)</div>
          <textarea value={bulkText} onChange={(e) => setBulkText(e.target.value)} placeholder={"Camille Dupont\nMatthieu Martin\n..."} rows={5}
            style={{ width: "100%", padding: "8px 10px", borderRadius: 7, border: "1px solid var(--border)", background: "var(--bg3)", color: "var(--text)", fontSize: 12.5, boxSizing: "border-box", resize: "vertical", fontFamily: "inherit" }} />
          <label style={{ fontSize: 11, color: "var(--text-muted)", fontWeight: 600, display: "block", marginTop: 8 }}>
            Groupe <span style={{ color: "#f87171" }}>(obligatoire)</span>
          </label>
          <input value={groupText} onChange={(e) => setGroupText(e.target.value)} placeholder="Ex : Louveteaux, Staff..."
            style={{ width: "100%", marginTop: 4, padding: "7px 8px", borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg3)", color: "var(--text)", fontSize: 12, boxSizing: "border-box" }} />
          <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 8 }}>
            {groupButtons.map((g) => {
              const active = groupText.trim() === g;
              const color = g === INVITES_GROUP ? "#94a3b8" : "#4d9fef";
              return (
                <button key={g} type="button" onClick={() => setGroupText((cur) => (cur.trim() === g ? "" : g))} style={{
                  padding: "5px 9px", borderRadius: 6, fontSize: 11.5, fontWeight: 600, cursor: "pointer",
                  border: `1px solid ${color}66`, background: active ? color : "var(--bg3)", color: active ? "#0a0d0f" : color,
                }}>{g}</button>
              );
            })}
          </div>
          <div style={{ fontSize: 10.5, color: "var(--text-faint)", marginTop: 6 }}>
            Reprends le nom d'un groupe existant (bouton ci-dessus) pour y rattacher un nouvel arrivant — ex. quelqu'un qui rejoint son groupe en cours de séjour — ou tape un nouveau nom pour créer un groupe.
          </div>
          <button onClick={addFromBulk} disabled={!canRegister} style={{
            width: "100%", marginTop: 8, padding: "9px", borderRadius: 7, border: "none", fontWeight: 700, fontSize: 12.5,
            background: canRegister ? "#4d9fef" : "var(--bg4)", color: canRegister ? "#0a0d0f" : "var(--text-faint)", cursor: canRegister ? "pointer" : "not-allowed",
          }}>Inscrire sur le camp</button>
        </div>

        <div style={{ background: "var(--bg2)", border: "1px solid var(--border)", borderRadius: 10, padding: 14 }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
            <div style={{ fontSize: 12.5, fontWeight: 700, color: "#c084fc" }}>Véhicules du camp</div>
            <div style={{ fontSize: 12, color: "var(--text-muted)" }}>{vehicles.filter((v) => vehicleStatus(entries, v.id).status === "present").length} présent(s)</div>
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 6, marginBottom: 10 }}>
            <input value={vehicleBulkText} onChange={(e) => setVehicleBulkText(e.target.value)} placeholder="Nom du véhicule (ex : Kangoo bleu)"
              onKeyDown={(e) => { if (e.key === "Enter" && vehicleBulkText.trim()) addVehiclesFromBulk(); }}
              style={{ width: "100%", boxSizing: "border-box", padding: "7px 8px", borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg3)", color: "var(--text)", fontSize: 12 }} />
            <input value={vehiclePlate} onChange={(e) => setVehiclePlate(e.target.value)} placeholder="Plaque (optionnel)"
              onKeyDown={(e) => { if (e.key === "Enter" && vehicleBulkText.trim()) addVehiclesFromBulk(); }}
              style={{ width: "100%", boxSizing: "border-box", padding: "7px 8px", borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg3)", color: "var(--text)", fontSize: 12 }} />
            <button onClick={addVehiclesFromBulk} style={{ padding: "7px 10px", borderRadius: 6, border: "none", background: "#c084fc", color: "#0a0d0f", fontWeight: 700, fontSize: 12, cursor: "pointer" }}>Inscrire un véhicule</button>
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
            {vehicles.length === 0 && <div style={{ fontSize: 11.5, color: "var(--text-faint)" }}>Aucun véhicule enregistré.</div>}
            {vehicles.map((v) => {
              const vs = vehicleStatus(entries, v.id);
              const color = vs.status === "present" ? "#34d399" : "#fb923c";
              return (
                <div key={v.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", borderRadius: 7, background: "var(--bg4)", border: `1px solid ${color}44` }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 12, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{v.name}</div>
                    {v.plate && <div style={{ fontSize: 10, color: "var(--text-faint)", fontFamily: "monospace" }}>{v.plate}</div>}
                    <div style={{ fontSize: 10.5, color }}>{vs.status === "present" ? "Présent" : "Sorti"}{vs.time ? ` · ${fmtTime(vs.time)}` : ""}</div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </div>

      <div className="effectif-main" style={{ flex: "1 1 600px", minWidth: 300 }}>
        <div style={{ position: "relative", marginBottom: 10 }}>
          <Search size={14} color="var(--text-faint)" style={{ position: "absolute", left: 10, top: "50%", transform: "translateY(-50%)" }} />
          <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Tape les premières lettres d'un prénom…"
            autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
            style={{ width: "100%", padding: "9px 10px 9px 30px", borderRadius: 8, border: "1px solid var(--border)", background: "var(--bg2)", color: "var(--text)", fontSize: 13, boxSizing: "border-box" }} />
        </div>
        {roster.length === 0 && (
          <div style={{ fontSize: 12, color: "var(--text-faint)", marginBottom: 12 }}>Aucune personne dans l'effectif pour l'instant.</div>
        )}
        {query.trim() && filtered.length === 0 && (
          <div style={{ fontSize: 12, color: "var(--text-faint)", marginBottom: 12 }}>Aucun résultat.</div>
        )}
        <div style={{ display: "flex", gap: 16, flexWrap: "wrap", alignItems: "flex-start" }}>
          <div className="effectif-present-col" style={{ flex: "1 1 260px", minWidth: 240 }}>
            <div style={{ background: "#34d399", color: "var(--bg0)", borderRadius: 8, padding: "9px 12px", marginBottom: 10, display: "flex", alignItems: "center", gap: 7 }}>
              <ArrowDown size={17} strokeWidth={2.75} />
              <span style={{ fontWeight: 800, fontSize: 20 }}>{presentList.length}</span>
              <span style={{ fontWeight: 800, fontSize: 16 }}>Présents sur le camp</span>
            </div>
            {presentList.length === 0
              ? <div style={{ fontSize: 11.5, color: "var(--text-faint)" }}>Personne présent pour l'instant.</div>
              : renderColumn(presentList, "present")}
          </div>

          <div className="effectif-form-col" style={{ flex: "0 0 300px", minWidth: 260, alignSelf: "stretch" }}>
            {/* position:sticky + colonne étirée sur toute la hauteur de la ligne (alignSelf:
                stretch ci-dessus) : le bloc Sortie suit le défilement plutôt que de disparaître
                en haut de l'écran quand la colonne "Présents" est longue. */}
            <div style={{ position: "sticky", top: 12 }}>
              <EntryForm setEntries={setEntries} roster={roster} vehicles={vehicles}
                pendingSortie={pendingSortie} setPendingSortie={setPendingSortie} onSortie={onSortie} onRemovePeople={removePeople}
                onCreateMission={onCreateMission} presets={presets} />
            </div>
          </div>

          <div className="effectif-away-col" style={{ flex: "1 1 260px", minWidth: 240 }}>
            <div style={{ background: "#fb923c", color: "var(--bg0)", borderRadius: 8, padding: "9px 12px", marginBottom: 10, display: "flex", alignItems: "center", gap: 7 }}>
              <ArrowUp size={17} strokeWidth={2.75} />
              <span style={{ fontWeight: 800, fontSize: 20 }}>{awayList.length}</span>
              <span style={{ fontWeight: 800, fontSize: 16 }}>Hors du camp</span>
            </div>
            {awayList.length === 0
              ? <div style={{ fontSize: 11.5, color: "var(--text-faint)" }}>Tout le monde est sur le camp.</div>
              : renderColumn(awayList, "away")}
          </div>
        </div>
      </div>
    </div>
  );
}

// Formulaire de saisie entrée/sortie (+ motif/véhicule) — vit maintenant dans l'onglet
// Effectif, entre les deux colonnes Présents/Hors du camp, plutôt que dans le Journal (qui
// n'est plus qu'un historique en lecture seule, groupé par jour/heure).
// Ce formulaire ne sert plus qu'à faire SORTIR des gens (avec motif/véhicule) — l'entrée sur
// le camp se fait exclusivement via "Inscrire sur le camp" + "Marquer arrivée" dans les
// colonnes Présents/Hors du camp, pour éviter de dupliquer ce mécanisme.
function EntryForm({ setEntries, roster, vehicles, pendingSortie, setPendingSortie, onSortie, onRemovePeople, onCreateMission, presets }) {
  const [names, setNames] = useState2("");
  const [motif, setMotif] = useState2("");
  const [motifChoice, setMotifChoice] = useState2(null); // libellé du bouton motif sélectionné ("Autre" = texte libre ci-dessous)
  const [missionType, setMissionType] = useState2(null); // type choisi quand motifChoice === "Mission" (vigie/patrouille/barriere)
  const [missionLabel, setMissionLabel] = useState2(null); // nom précis choisi parmi les presets du type (ex. "Vigie Luminy")
  const [vehicleId, setVehicleId] = useState2(null);
  const [driverId, setDriverId] = useState2(null); // qui conduit le véhicule sélectionné, parmi les personnes de la sortie
  const [error, setError] = useState2("");

  function pickMotif(preset) {
    if (motifChoice === preset) { setMotifChoice(null); setMotif(""); setMissionType(null); setMissionLabel(null); return; }
    setMotifChoice(preset);
    setMotif(preset === "Autre" || preset === "Mission" ? "" : preset);
    setMissionType(null);
    setMissionLabel(null);
  }
  function pickMissionType(key) {
    setMissionType((cur) => (cur === key ? null : key));
    setMissionLabel(null); // le nom précis dépend du type, on redemande à chaque fois
  }

  // Mode dédié : une sortie a été mise en attente depuis une des deux colonnes (bouton
  // "Sortie" individuel ou "Sortir le groupe") — le formulaire affiche les personnes déjà
  // choisies (au lieu du champ texte libre) et ne fait plus que collecter motif/véhicule.
  const isPending = !!pendingSortie;

  const nq = normalizeText(names.trim());
  const suggestions = !isPending && nq && !roster.some((p) => normalizeText(p.name) === nq)
    ? roster.filter((p) => normalizeText(p.name).split(" ").some((w) => w.startsWith(nq)) || normalizeText(p.name).startsWith(nq)).slice(0, 6)
    : [];

  function removePendingPerson(id) {
    setPendingSortie((cur) => {
      if (!cur) return cur;
      const people = cur.people.filter((p) => p.id !== id);
      return people.length ? { ...cur, people } : null;
    });
  }
  function cancelPending() {
    setPendingSortie(null);
    setMotif(""); setMotifChoice(null); setMissionType(null); setMissionLabel(null); setVehicleId(null); setDriverId(null); setError("");
  }
  function pickVehicle(id) {
    setVehicleId((cur) => (cur === id ? null : id));
    setDriverId(null); // le conducteur dépend du véhicule choisi, on redemande à chaque fois
  }

  // Plutôt qu'un message d'erreur après coup, le bouton "Enregistrer"/"Confirmer la sortie"
  // reste grisé tant que le formulaire n'est pas valide : motif choisi (ou "Mission", validée
  // plus loin dans son propre tunnel type/nom), et — si un véhicule est sélectionné pour une
  // sortie à plusieurs personnes — un conducteur désigné.
  const motifOk = !!motif.trim() || motifChoice === "Mission";
  const needsDriver = isPending && !!vehicleId && pendingSortie.people.length > 1 && !driverId;
  const canSubmit = motifOk && !needsDriver;

  function submit() {
    if (!motif.trim() && motifChoice !== "Mission") { setError("Le motif est obligatoire."); return; }
    // "Mission" crée réellement une mission dans l'onglet Carte & missions — n'a de sens que
    // pour une sortie déjà rattachée à de vraies personnes de l'effectif (bouton "Sortie"),
    // pas pour la saisie libre par simple texte, qui n'a pas d'identifiant de personne.
    if (motifChoice === "Mission" && !isPending) {
      setError("Choisis d'abord les personnes concernées via le bouton \"Sortie\", pas en texte libre.");
      return;
    }
    const veh = vehicles.find((v) => v.id === vehicleId);

    if (isPending && motifChoice === "Mission") {
      if (!missionType) { setError("Choisis un type de mission."); return; }
      if (!missionLabel) { setError("Choisis la mission concernée."); return; }
      let driver = null;
      if (veh) {
        driver = pendingSortie.people.length === 1 ? pendingSortie.people[0] : pendingSortie.people.find((p) => p.id === driverId);
        if (!driver) { setError("Indique qui conduit le véhicule."); return; }
      }
      // Crée réellement la mission dans l'onglet Carte & missions (avec l'équipe en crew) —
      // les deux onglets restent ainsi toujours cohérents pour les sorties de mission.
      const crew = pendingSortie.people.map((p) => ({ personId: p.id, name: p.name }));
      onCreateMission(missionType, missionLabel, crew, veh || null, driver ? driver.id : null);
      setPendingSortie(null);
    } else if (isPending) {
      // Un seul conducteur possible par véhicule : implicite s'il n'y a qu'une personne dans
      // cette sortie, sinon il faut le désigner — utile pour savoir qui conduisait en cas
      // d'amende reçue plus tard sur un véhicule du camp.
      let driver = null;
      if (veh) {
        driver = pendingSortie.people.length === 1 ? pendingSortie.people[0] : pendingSortie.people.find((p) => p.id === driverId);
        if (!driver) { setError("Indique qui conduit le véhicule."); return; }
      }
      pendingSortie.people.forEach((p) => onSortie(p, motif.trim(), veh && p.id === driver.id ? veh : null));
      if (motifChoice === "Sortie définitive") onRemovePeople(pendingSortie.people.map((p) => p.id));
      setPendingSortie(null);
    } else {
      if (!names.trim()) { setError("Indique au moins le nom des personnes concernées."); return; }
      setEntries((es) => [
        { id: uid("e"), type: "sortie", names: names.trim(), motif: motif.trim(), vehicleId: veh ? veh.id : null, vehicule: veh ? veh.name : "", time: Date.now(), returnedAt: null },
        ...es,
      ]);
      setNames("");
    }
    setMotif(""); setMotifChoice(null); setMissionType(null); setMissionLabel(null); setVehicleId(null); setDriverId(null); setError("");
  }

  return (
    <div style={{ background: "var(--bg2)", border: `1px solid ${isPending ? "#fb923c88" : "var(--border)"}`, borderRadius: 10, padding: 14 }}>
      <div style={{ fontSize: 16, fontWeight: 800, color: isPending ? "#fb923c" : "var(--text)", marginBottom: 10, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
        <span style={{ display: "flex", alignItems: "center", gap: 7 }}>
          <LogOut size={16} strokeWidth={2.5} />
          {isPending ? `Sortie — ${pendingSortie.people.length === 1 ? pendingSortie.people[0].name : pendingSortie.people.length + " personnes"}` : "Sortie"}
        </span>
        {isPending && (
          <button onClick={cancelPending} title="Annuler cette sortie" style={{ background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", display: "flex" }}><X size={15} /></button>
        )}
      </div>

      <label style={{ fontSize: 11.5, color: "var(--text-muted)", fontWeight: 600 }}>Personnes concernées</label>
      {isPending ? (
        <div style={{ display: "flex", flexWrap: "wrap", gap: 5, margin: "6px 0 10px" }}>
          {pendingSortie.people.map((p) => (
            <div key={p.id} style={{ display: "flex", alignItems: "center", gap: 4, padding: "4px 8px", borderRadius: 6, background: "#fb923c22", border: "1px solid #fb923c55", fontSize: 11.5, color: "var(--text)" }}>
              {p.name}
              <button onClick={() => removePendingPerson(p.id)} style={{ background: "none", border: "none", color: "var(--text-faint)", cursor: "pointer", display: "flex" }}><X size={11} /></button>
            </div>
          ))}
        </div>
      ) : (
        <>
          <input value={names} onChange={(e) => setNames(e.target.value)} placeholder="Noms, ou nombre de personnes"
            autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
            style={{ ...inputStyle, marginBottom: suggestions.length > 0 ? 6 : 10 }} />
          {suggestions.length > 0 && (
            <div style={{ display: "flex", flexDirection: "column", gap: 3, marginBottom: 10 }}>
              {suggestions.map((p) => (
                <button key={p.id} type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => setNames(p.name)}
                  style={{ textAlign: "left", padding: "6px 8px", borderRadius: 6, background: "var(--bg3)", border: "1px solid var(--border)", fontSize: 12, color: "var(--text)", cursor: "pointer" }}>{p.name}</button>
              ))}
            </div>
          )}
        </>
      )}

      <div style={{ height: 1, background: "var(--border)", margin: "14px 0" }} />

      <>
        <label style={{ fontSize: 14, color: "var(--text)", fontWeight: 700 }}>Motif</label>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 4, marginBottom: motifChoice === "Autre" ? 8 : 10 }}>
          {MOTIF_PRESETS.map((preset) => {
            const isFinal = preset === "Sortie définitive";
            const activeColor = isFinal ? "#f87171" : "#4d9fef";
            const active = motifChoice === preset;
            return (
              <button key={preset} type="button" onClick={() => pickMotif(preset)} style={{
                padding: "6px 10px", borderRadius: 7, fontSize: 11.5, fontWeight: 600, cursor: "pointer",
                border: `1px solid ${active ? activeColor : isFinal ? "#f8717166" : "var(--border)"}`,
                background: active ? activeColor : isFinal ? "#f8717118" : "var(--bg3)",
                color: active ? "#0a0d0f" : isFinal ? "#f87171" : "var(--text)",
              }}>{preset}</button>
            );
          })}
        </div>
        {motifChoice === "Autre" && (
          <input autoFocus value={motif} onChange={(e) => setMotif(e.target.value)} placeholder="Précise le motif..." style={inputStyle} />
        )}
        {motifChoice === "Mission" && (
          <>
            <div style={{ height: 1, background: "var(--border)", margin: "14px 0" }} />
            <label style={{ fontSize: 14, color: "var(--text)", fontWeight: 700 }}>Type de mission</label>
            {!isPending && (
              <div style={{ fontSize: 11, color: "var(--text-faint)", marginTop: 4, marginBottom: 8 }}>
                Choisis d'abord les personnes concernées via le bouton "Sortie" (colonne Présents) pour créer la mission.
              </div>
            )}
            <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 4, marginBottom: missionType ? 8 : 10 }}>
              {Object.entries(MISSION_TYPES).map(([key, mt]) => {
                const active = missionType === key;
                return (
                  <button key={key} type="button" onClick={() => pickMissionType(key)} style={{
                    padding: "6px 10px", borderRadius: 7, fontSize: 11.5, fontWeight: 600, cursor: "pointer",
                    border: `1px solid ${active ? mt.color : "var(--border)"}`,
                    background: active ? mt.color : "var(--bg3)",
                    color: active ? "#0a0d0f" : "var(--text)",
                  }}>{mt.label}</button>
                );
              })}
            </div>
            {missionType && (
              <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 10 }}>
                {(presets[missionType] || []).map((label) => {
                  const active = missionLabel === label;
                  const color = MISSION_TYPES[missionType].color;
                  return (
                    <button key={label} type="button" onClick={() => setMissionLabel(active ? null : label)} style={{
                      padding: "6px 10px", borderRadius: 7, fontSize: 11.5, fontWeight: 600, cursor: "pointer",
                      border: `1px solid ${active ? color : "var(--border)"}`,
                      background: active ? color : "var(--bg3)",
                      color: active ? "#0a0d0f" : "var(--text)",
                    }}>{label}</button>
                  );
                })}
              </div>
            )}
          </>
        )}

        <div style={{ height: 1, background: "var(--border)", margin: "14px 0" }} />

        <label style={{ fontSize: 14, color: "var(--text)", fontWeight: 700 }}>Véhicule</label>
        {vehicles.length === 0 ? (
          <div style={{ fontSize: 11.5, color: "var(--text-faint)", marginBottom: 10 }}>Aucun véhicule enregistré — ajoutes-en ci-dessus.</div>
        ) : (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 8, marginBottom: isPending && vehicleId && pendingSortie.people.length > 1 ? 8 : 10 }}>
            {vehicles.map((v) => (
              <button key={v.id} type="button" onClick={() => pickVehicle(v.id)} style={{
                padding: "6px 10px", borderRadius: 7, fontSize: 11.5, fontWeight: 600, cursor: "pointer",
                border: `1px solid ${vehicleId === v.id ? "#c084fc" : "var(--border)"}`, background: vehicleId === v.id ? "#c084fc" : "var(--bg3)", color: vehicleId === v.id ? "#0a0d0f" : "var(--text)",
              }}>{v.name}</button>
            ))}
          </div>
        )}
        {isPending && vehicleId && pendingSortie.people.length > 1 && (
          <>
            <label style={{ fontSize: 11.5, color: "var(--text-muted)", fontWeight: 600 }}>
              Conducteur
            </label>
            <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 4, marginBottom: 10 }}>
              {pendingSortie.people.map((p) => (
                <button key={p.id} type="button" onClick={() => setDriverId(driverId === p.id ? null : p.id)} style={{
                  padding: "6px 10px", borderRadius: 7, fontSize: 11.5, fontWeight: 600, cursor: "pointer",
                  border: `1px solid ${driverId === p.id ? "#c084fc" : "var(--border)"}`, background: driverId === p.id ? "#c084fc" : "var(--bg3)", color: driverId === p.id ? "#0a0d0f" : "var(--text)",
                }}>{p.name}</button>
              ))}
            </div>
          </>
        )}
      </>
      {error && <div style={{ color: "#f87171", fontSize: 11.5, marginTop: 4 }}>{error}</div>}
      <button onClick={submit} disabled={!canSubmit} style={{
        width: "100%", marginTop: 10, padding: "10px", borderRadius: 7, border: "none", fontWeight: 700, fontSize: 13,
        background: canSubmit ? (isPending ? "#fb923c" : "#4d9fef") : "var(--bg4)",
        color: canSubmit ? "#0a0d0f" : "var(--text-faint)",
        cursor: canSubmit ? "pointer" : "not-allowed",
      }}>
        {isPending ? "Confirmer la sortie" : "Enregistrer"}
      </button>
    </div>
  );
}

// "Aujourd'hui" / "Hier" / date complète en français — repère rapide pour naviguer dans
// l'historique des jours précédents.
function journalDayLabel(ts) {
  const d = new Date(ts);
  const today = new Date();
  const yesterday = new Date(today);
  yesterday.setDate(today.getDate() - 1);
  const sameDay = (a, b) => a.toDateString() === b.toDateString();
  if (sameDay(d, today)) return "Aujourd'hui";
  if (sameDay(d, yesterday)) return "Hier";
  const s = d.toLocaleDateString("fr-FR", { weekday: "long", day: "numeric", month: "long", year: "numeric" });
  return s.charAt(0).toUpperCase() + s.slice(1);
}
function journalHourLabel(ts) {
  return `${String(new Date(ts).getHours()).padStart(2, "0")}h`;
}
// Regroupe le journal (déjà trié du plus récent au plus ancien) par jour puis par heure —
// facilite la navigation/recherche dans les jours précédents d'un camp de plusieurs jours.
function groupJournalByDayAndHour(entries) {
  const days = [];
  let curDayLabel = null, curDay = null, curHourLabel = null, curHour = null;
  for (const e of entries) {
    const dLabel = journalDayLabel(e.time);
    if (dLabel !== curDayLabel) {
      curDayLabel = dLabel;
      curDay = { label: dLabel, hours: [] };
      days.push(curDay);
      curHourLabel = null;
    }
    const hLabel = journalHourLabel(e.time);
    if (hLabel !== curHourLabel) {
      curHourLabel = hLabel;
      curHour = { label: hLabel, entries: [] };
      curDay.hours.push(curHour);
    }
    curHour.entries.push(e);
  }
  return days;
}

// Une ligne texte par mouvement — mêmes informations que l'affichage à l'écran (EntryRow),
// pour que le fichier téléchargé corresponde exactement à ce qu'on voit dans l'onglet.
function journalEntryLine(e, missions) {
  const who = e.names || (e.vehicule ? `Véhicule : ${e.vehicule}` : "—");
  const details = [e.motif, e.names && e.vehicule ? e.vehicule : null].filter(Boolean);
  if (e.missionId && missions) {
    const m = missions.find((mm) => mm.id === e.missionId);
    if (m) details.push(`Mission : ${m.label}`);
  }
  const statusWord = e.type === "entree" ? "Entré" : "Sorti";
  let line = `  [${fmtTime(e.time)}] ${who} — ${statusWord}`;
  if (details.length > 0) line += ` (${details.join(" · ")})`;
  if (e.type === "sortie" && e.returnedAt) line += ` — Rentré ${fmtTime(e.returnedAt)}`;
  return line;
}

// Assemble un journal déjà groupé par jour puis par heure (cf. groupJournalByDayAndHour) en
// un texte brut, prêt à être proposé au téléchargement — `lineFn` formate une entrée en une
// ligne ; générique pour servir aux deux colonnes (mouvements et missions).
function buildDayHourText(header, days, lineFn) {
  const body = days.map((day) => {
    const dayTitle = `\n${day.label}\n${"=".repeat(day.label.length)}`;
    const hours = day.hours.map((hour) => {
      const lines = hour.entries.map(lineFn).join("\n");
      return `\n${hour.label}\n${lines}`;
    }).join("\n");
    return `${dayTitle}\n${hours}`;
  }).join("\n");
  return header + body + "\n";
}
function buildJournalText(days, missions) {
  const header = `Journal des mouvements — VigiCamp DFCI Calanques\nExporté le ${new Date().toLocaleString("fr-FR")}\n`;
  return buildDayHourText(header, days, (e) => journalEntryLine(e, missions));
}
function buildMissionsJournalText(days) {
  const header = `Journal des missions — VigiCamp DFCI Calanques\nExporté le ${new Date().toLocaleString("fr-FR")}\n`;
  return buildDayHourText(header, days, (item) => `  [${fmtTime(item.time)}] ${item.text}`);
}

function downloadTextFile(text, filename) {
  const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}

// Un jour/heure replié par défaut sauf le plus récent — même logique de tiroirs pour les deux
// colonnes de l'onglet Journal (mouvements et missions), factorisée ici plutôt que dupliquée.
function JournalDayHourList({ days, renderItem, unitLabel }) {
  const [closedDays, setClosedDays] = useState2(() => new Set(days.slice(1).map((_, i) => i + 1)));
  const [closedHours, setClosedHours] = useState2(() => new Set());
  function toggleDay(i) {
    setClosedDays((s) => { const n = new Set(s); if (n.has(i)) n.delete(i); else n.add(i); return n; });
  }
  function toggleHour(key) {
    setClosedHours((s) => { const n = new Set(s); if (n.has(key)) n.delete(key); else n.add(key); return n; });
  }
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
      {days.map((day, di) => {
        const dayOpen = !closedDays.has(di);
        const dayCount = day.hours.reduce((n, h) => n + h.entries.length, 0);
        return (
          <div key={day.label}>
            <button onClick={() => toggleDay(di)} style={{
              width: "100%", display: "flex", alignItems: "center", gap: 8, cursor: "pointer",
              background: "none", border: "none", padding: "6px 0", borderBottom: "1px solid var(--border)",
              position: "sticky", top: 0, backgroundColor: "var(--bg0)", textAlign: "left",
            }}>
              <ChevronRight size={13} color="var(--text-faint)" style={{ transform: dayOpen ? "rotate(90deg)" : "none", transition: "transform .1s", flexShrink: 0 }} />
              <span style={{ fontSize: 13, fontWeight: 800, color: "var(--text)" }}>{day.label}</span>
              <span style={{ fontSize: 11, color: "var(--text-faint)", fontWeight: 600 }}>{dayCount} {unitLabel}{dayCount > 1 ? "s" : ""}</span>
            </button>
            {dayOpen && (
              <div style={{ display: "flex", flexDirection: "column", gap: 6, marginTop: 10 }}>
                {day.hours.map((hour, hi) => {
                  const key = di + ":" + hi;
                  const hourOpen = !closedHours.has(key);
                  return (
                    <div key={hour.label}>
                      <button onClick={() => toggleHour(key)} style={{
                        display: "flex", alignItems: "center", gap: 6, cursor: "pointer",
                        background: "none", border: "none", padding: "3px 0", textAlign: "left",
                      }}>
                        <ChevronRight size={11} color="var(--text-faint)" style={{ transform: hourOpen ? "rotate(90deg)" : "none", transition: "transform .1s", flexShrink: 0 }} />
                        <span style={{ fontSize: 11, color: "var(--text-faint)", fontFamily: "ui-monospace, monospace" }}>{hour.label}</span>
                        <span style={{ fontSize: 10.5, color: "var(--text-faint)" }}>({hour.entries.length})</span>
                      </button>
                      {hourOpen && (
                        <div style={{ display: "flex", flexDirection: "column", gap: 6, marginTop: 4, marginLeft: 19 }}>
                          {hour.entries.map(renderItem)}
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

// En-tête coloré pleine largeur (même esprit que les colonnes Présents/Hors du camp de
// l'onglet Effectif, cf. EffectifTab) — sert ici de gros bouton de téléchargement pour toute
// la colonne, plutôt qu'un simple titre.
function JournalColumnHeader({ title, color, onDownload }) {
  return (
    <button onClick={onDownload} title={`${title} (.txt, groupé par jour et par heure)`} style={{
      width: "100%", background: color, color: "var(--bg0)", border: "none", borderRadius: 8, padding: "9px 12px", marginBottom: 12,
      cursor: "pointer", display: "flex", alignItems: "center", gap: 8, textAlign: "left",
    }}>
      <Download size={16} />
      <span style={{ fontWeight: 800, fontSize: 15 }}>{title}</span>
    </button>
  );
}

// Onglet Journal : deux colonnes en lecture seule. À gauche, les mouvements d'entrée/sortie
// (saisis uniquement depuis l'onglet Effectif). À droite, l'historique complet de l'onglet
// Carte & missions (missions + feux/fumées) — exactement ce que montre "Activité récente",
// mais sans plafond et téléchargeable à part. Chaque en-tête de colonne est lui-même le
// bouton de téléchargement de cette colonne.
function EntriesTab({ entries, missions, missionsJournal }) {
  const [moveQuery, setMoveQuery] = useState2("");
  const stillOut = entries.filter((e) => e.type === "sortie" && !e.returnedAt).length;
  // Le téléchargement (.txt) reste toujours complet — seul l'affichage à l'écran est filtré
  // par la recherche, pour retrouver rapidement toutes les lignes concernant une personne.
  const mq = normalizeText(moveQuery.trim());
  const filteredEntries = mq ? entries.filter((e) => normalizeText(e.names || "").includes(mq)) : entries;
  const moveDays = groupJournalByDayAndHour(entries);
  const filteredMoveDays = groupJournalByDayAndHour(filteredEntries);
  const missionDays = groupJournalByDayAndHour(missionsJournal);

  return (
    <div style={{ padding: "12px 16px", display: "flex", gap: 20, flexWrap: "wrap" }}>
      <div className="journal-col" style={{ flex: "1 1 340px", minWidth: 280 }}>
        <JournalColumnHeader title="Télécharger le journal des mouvements" color="#22d3ee" onDownload={() => downloadTextFile(buildJournalText(moveDays, missions), `journal-mouvements-pc-dfci-${new Date().toISOString().slice(0, 10)}.txt`)} />
        <div style={{ position: "relative", marginBottom: 10 }}>
          <Search size={14} color="var(--text-faint)" style={{ position: "absolute", left: 10, top: "50%", transform: "translateY(-50%)" }} />
          <input value={moveQuery} onChange={(e) => setMoveQuery(e.target.value)} placeholder="Rechercher une personne…"
            autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
            style={{ width: "100%", padding: "9px 10px 9px 30px", borderRadius: 8, border: "1px solid var(--border)", background: "var(--bg2)", color: "var(--text)", fontSize: 13, boxSizing: "border-box" }} />
        </div>
        {stillOut > 0 && <div style={{ fontSize: 11.5, color: "#fb923c", fontWeight: 700, marginBottom: 10 }}>{stillOut} actuellement à l'extérieur</div>}
        {filteredEntries.length === 0 && (
          <div style={{ fontSize: 12, color: "var(--text-faint)" }}>{mq ? "Aucun mouvement pour cette personne." : "Aucun mouvement enregistré."}</div>
        )}
        <JournalDayHourList days={filteredMoveDays} unitLabel="mouvement" renderItem={(e) => {
          const open = e.type === "sortie" && !e.returnedAt;
          return <EntryRow key={e.id} e={e} open={open} missions={missions} />;
        }} />
      </div>

      <div className="journal-col" style={{ flex: "1 1 340px", minWidth: 280 }}>
        <JournalColumnHeader title="Télécharger le journal des missions" color="#4d9fef" onDownload={() => downloadTextFile(buildMissionsJournalText(missionDays), `journal-missions-pc-dfci-${new Date().toISOString().slice(0, 10)}.txt`)} />
        {missionsJournal.length === 0 && <div style={{ fontSize: 12, color: "var(--text-faint)" }}>Aucune action enregistrée.</div>}
        <JournalDayHourList days={missionDays} unitLabel="action" renderItem={(item, i) => <MissionJournalRow key={i} item={item} />} />
      </div>
    </div>
  );
}

function MissionJournalRow({ item }) {
  return (
    <div style={{ display: "flex", alignItems: "flex-start", gap: 10, padding: "9px 12px", borderRadius: 8, background: "var(--bg2)", border: `1px solid ${item.color}44` }}>
      <span style={{ width: 8, height: 8, borderRadius: "50%", background: item.color, flexShrink: 0, marginTop: 5 }} />
      <div style={{ flex: 1, minWidth: 0, fontSize: 12.5, color: "var(--text)" }}>{item.text}</div>
      <div style={{ fontSize: 11, color: "var(--text-muted)", fontFamily: "ui-monospace, monospace", flexShrink: 0 }}>{fmtTime(item.time)}</div>
    </div>
  );
}

function EntryRow({ e, open, missions }) {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 12px", borderRadius: 8, background: "var(--bg2)", border: `1px solid ${e.type === "entree" ? "#34d39944" : open ? "#fb923c88" : "#fb923c44"}` }}>
      {e.type === "entree" ? <LogIn size={15} color="#34d399" /> : <LogOut size={15} color="#fb923c" />}
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 12.5, fontWeight: 600 }}>{e.names || (e.vehicule ? `Véhicule : ${e.vehicule}` : "—")}</div>
        <div style={{ fontSize: 11, color: "var(--text-muted)" }}>
          {[e.motif, e.names && e.vehicule].filter(Boolean).join(" · ") || "—"}
          {e.missionId && missions && (() => {
            const m = missions.find((mm) => mm.id === e.missionId);
            return m ? <span style={{ color: MISSION_TYPES[m.type].color }}> · → {m.label}</span> : null;
          })()}
        </div>
      </div>
      <div style={{ fontSize: 11.5, color: "var(--text-muted)", fontFamily: "ui-monospace, monospace", textAlign: "right", flexShrink: 0 }}>
        <div>{e.type === "entree" ? "Entré" : "Sorti"} {fmtTime(e.time)}</div>
        {e.type === "sortie" && e.returnedAt && <div style={{ color: "#34d399" }}>Rentré {fmtTime(e.returnedAt)}</div>}
      </div>
    </div>
  );
}
