// ─────────────────────────────────────────────────────────────
// DoricMatch AI — src/app.jsx
// Orchestrateur principal, routage d'auth et montage React.
// Chargé via <script type="text/babel"> dans index.html (ordre important).
// ─────────────────────────────────────────────────────────────

function InvestMatchApp({ onLogout, isAdmin }) {
  const [page, setPage] = useState("dashboard");
  const [deal, setDeal] = useState(null);
  const [emailFor, setEmailFor] = useState(null);
  const [openId, setOpenId] = useState(null);
  const [analyzing, setAnalyzing] = useState(false);
  const [investors, setInvestors] = useState([]);
  const [creating, setCreating] = useState(false);
  const [importOpen, setImportOpen] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);
  const [settings, setSettings] = useState({
    company: "DORIC GROUPE", broker: "Eric THYBERT", role: "Broker en immobilier d'investissement",
    email: "ethybert@doric.fr", phone: "06 37 62 67 28", carteT: "", address: "",
    mentions: "Document confidentiel — Ne pas diffuser sans accord préalable de DORIC GROUPE", logo: "",
  });
  const settingsLoaded = useRef(false);
  useEffect(() => { (async () => {
    const { data } = await sb.from("settings").select("data").limit(1).maybeSingle();
    if (data && data.data) {
      const { apiKey, ...rest } = data.data; // on ignore toute clé héritée : elle ne doit plus vivre côté client ni en base
      setSettings(s => ({ ...s, ...rest }));  // la sauvegarde auto réécrira les réglages SANS apiKey → purge de l'ancienne clé
    }
    settingsLoaded.current = true;
  })(); }, []);
  useEffect(() => {
    if (!settingsLoaded.current) return;
    const t = setTimeout(async () => {
      const { data: u } = await sb.auth.getUser();
      const owner = u && u.user ? u.user.id : null;
      if (owner) await sb.from("settings").upsert({ owner, data: settings }, { onConflict: "owner" });
    }, 800);
    return () => clearTimeout(t);
  }, [settings]);
  const [loading, setLoading] = useState(true);


  useEffect(() => { (async () => {
    const { data, error } = await sb.from("investors").select("*").order("created_at", { ascending: false });
    if (!error && data) setInvestors(data.map(rowToInv));
    setLoading(false);
  })(); }, []);

  const addInvestor = async (inv) => {
    const { data, error } = await sb.from("investors").insert(invToRow(inv)).select().single();
    if (!error && data) setInvestors(l => [rowToInv(data), ...l]);
    setCreating(false);
  };
  // Import en masse (CSV / export Sales Navigator) — insertion par lots, anti-doublons côté modale.
  const importInvestors = async (list) => {
    try {
      const rows = list.map(invToRow);
      let inserted = [];
      for (let i = 0; i < rows.length; i += 200) {
        const { data, error } = await sb.from("investors").insert(rows.slice(i, i + 200)).select();
        if (error) return { inserted: inserted.length, error: error.message };
        if (data) inserted = inserted.concat(data);
      }
      if (inserted.length) setInvestors(l => [...inserted.map(rowToInv), ...l]);
      return { inserted: inserted.length, error: null };
    } catch (e) { return { inserted: 0, error: String((e && e.message) || e) }; }
  };
  const updateInvestor = async (inv) => {
    const { data, error } = await sb.from("investors").update(invToRow(inv)).eq("id", inv.id).select().single();
    if (!error && data) setInvestors(l => l.map(i => i.id === inv.id ? rowToInv(data) : i));
  };
  const deleteInvestor = async (id) => {
    await sb.from("investors").delete().eq("id", id);
    setInvestors(l => l.filter(i => i.id !== id)); setOpenId(null);
  };
  const applyEnrichment = async (id, entry) => {
    const inv = investors.find(i => i.id === id); if (!inv) return;
    const next = { ...inv, enrichments: [entry, ...(inv.enrichments || [])] };
    setInvestors(l => l.map(i => i.id === id ? next : i));
    await sb.from("investors").update({ enrichments: next.enrichments }).eq("id", id);
  };
  const attachDossier = async (ids, dossier) => {
    for (const id of ids) {
      const inv = investors.find(i => i.id === id); if (!inv) continue;
      const next = [{ ...dossier, id: Date.now() + "_" + id }, ...(inv.dossiers || [])];
      setInvestors(l => l.map(i => i.id === id ? { ...i, dossiers: next } : i));
      await sb.from("investors").update({ dossiers: next }).eq("id", id);
    }
  };
  // Correspondance critère IA → champ de la fiche (pour le badge « IA »).
  const INV_FIELD_KEY = { typologie: "typologies", tenant: "tenants", region: "regions", departement: "departements", strategy: "strategies", yieldMin: "yieldMin", budgetMax: "budgetMax", walbMin: "walbMin", monovalent: "monovalent" };
  const applyCriterion = async (id, crit) => {
    const inv = investors.find(i => i.id === id); if (!inv) return;
    const next = { ...inv };
    const uni = (arr, v) => [...new Set([...(arr || []), v])];
    switch (crit.field) {
      case "typologie": next.typologies = uni(inv.typologies, crit.value); break;
      case "tenant": next.tenants = uni(inv.tenants, crit.value); break;
      case "region": next.regions = uni(inv.regions, crit.value); break;
      case "departement": next.departements = uni(inv.departements, crit.value); break;
      case "strategy": next.strategies = uni(inv.strategies, crit.value); break;
      case "yieldMin": { const n = parseFloat(String(crit.value).replace(",", ".")); if (!isNaN(n)) next.yieldMin = n; break; }
      case "budgetMax": { const n = parseFloat(String(crit.value).replace(/[^0-9.]/g, "")); if (!isNaN(n)) next.budgetMax = n; break; }
      case "walbMin": { const n = parseFloat(String(crit.value).replace(",", ".")); if (!isNaN(n)) next.walbMin = n; break; }
      case "monovalent": next.monovalent = crit.value; break;
      default: break;
    }
    const key = INV_FIELD_KEY[crit.field];
    if (key) next.aiFields = [...new Set([...(inv.aiFields || []), key])];
    next.enrichments = [{ field: "other", label: "Critère affiné", value: crit.label + " : " + crit.value, source: crit.source || "Enrichissement IA", url: "", confidence: crit.confidence || 85, date: new Date().toLocaleDateString("fr-FR") }, ...(inv.enrichments || [])];
    setInvestors(l => l.map(i => i.id === id ? next : i));
    await sb.from("investors").update(invToRow(next)).eq("id", id);
  };
  // Application GROUPÉE et automatique d'un enrichissement IA (plusieurs critères + infos en une
  // seule écriture atomique → les champs se remplissent seuls, sans clic, sans perte).
  const applyInvEnrichmentBatch = async (id, entries, criteria) => {
    const inv = investors.find(i => i.id === id); if (!inv) return;
    const next = { ...inv };
    const uni = (arr, v) => [...new Set([...(arr || []), v])];
    const aiKeys = new Set(inv.aiFields || []);
    (criteria || []).forEach(c => {
      switch (c.field) {
        case "typologie": next.typologies = uni(next.typologies, c.value); break;
        case "tenant": next.tenants = uni(next.tenants, c.value); break;
        case "region": next.regions = uni(next.regions, c.value); break;
        case "departement": next.departements = uni(next.departements, c.value); break;
        case "strategy": next.strategies = uni(next.strategies, c.value); break;
        case "yieldMin": { const n = parseFloat(String(c.value).replace(",", ".")); if (!isNaN(n)) next.yieldMin = n; break; }
        case "budgetMax": { const n = parseFloat(String(c.value).replace(/[^0-9.]/g, "")); if (!isNaN(n)) next.budgetMax = n; break; }
        case "walbMin": { const n = parseFloat(String(c.value).replace(",", ".")); if (!isNaN(n)) next.walbMin = n; break; }
        case "monovalent": next.monovalent = c.value; break;
        default: break;
      }
      const k = INV_FIELD_KEY[c.field]; if (k) aiKeys.add(k);
    });
    next.aiFields = [...aiKeys];
    next.enrichments = [...(entries || []), ...(inv.enrichments || [])];
    setInvestors(l => l.map(i => i.id === id ? next : i));
    await sb.from("investors").update(invToRow(next)).eq("id", id);
  };
  const openInv = investors.find(i => i.id === openId) || null;

  const matches = useMemo(() => deal
    ? investors.map(inv => ({ inv, ...scoreInvestor(inv, deal) })).sort((a, b) => b.global - a.global)
    : [], [deal, investors]);

  const [analyzeErr, setAnalyzeErr] = useState("");
  // Analyse réelle d'un dossier (PDF/image/texte) → extraction structurée → géocodage → matching
  const startAnalyze = async (payload) => {
    // payload : { file:{b64,mime} } ou { text } ou undefined (démo)
    setAnalyzeErr("");
    if (!payload || (!payload.file && !payload.text)) {
      // démo (gratuite)
      setAnalyzing(true); setTimeout(() => { setDeal({ ...SAMPLE_DEAL, runId: "run_" + Date.now() }); setAnalyzing(false); setPage("results"); }, 1600); return;
    }
    if (!confirmCost("Analyse d'un dossier par IA (0,50 €).", IA_COSTS.analyse)) return;
    setAnalyzing(true);
    const sys = `Tu es analyste en immobilier d'investissement. À partir du dossier fourni (brochure, état locatif, mémorandum…), extrais l'actif sous forme de JSON STRICT (sans backticks) :
{"name":"nom court de l'actif","city":"ville","region":"région française","departement":"code et nom du département, ex: 14 — Calvados (déduis-le de la ville/adresse)","address":"adresse si dispo","typology":"une typologie parmi: Locaux commerciaux loués, Bureaux, Entrepôt logistique, Local d'activité, Commerce de pied d'immeuble, Résidentiel, Hôtel, Santé/EHPAD, Autre","price":nombre_en_euros_ou_null,"rent":loyer_annuel_HT_HC_ou_null,"yield":rendement_pourcentage_ou_null,"surface":m2_ou_null,"occupation":"texte ex: Loué (100%)","strategy":"core|core+|value-add|opportuniste","waltDeal":WALB_en_annees_ou_null,"monovalent":true_si_actif_monovalent_sinon_false,"tenants":"locataire(s)","strengths":["3-5 points forts"],"risks":["1-3 risques"],"vacancy":true_si_vacance,"works":true_si_travaux_a_prevoir}
Déduis le rendement si prix et loyer sont connus. Sois factuel.`;
    const content = [];
    if (payload.file) {
      content.push(payload.file.mime === "application/pdf"
        ? { type: "document", source: { type: "base64", media_type: "application/pdf", data: payload.file.b64 } }
        : { type: "image", source: { type: "base64", media_type: payload.file.mime, data: payload.file.b64 } });
    }
    if (payload.text && payload.text.trim()) {
      content.push({ type: "text", text: "Informations complémentaires saisies par le broker :\n\n" + payload.text });
    }
    content.push({ type: "text", text: (payload.file && payload.text && payload.text.trim())
      ? "Extrais l'actif en combinant le document importé ET les informations complémentaires ci-dessus. Les deux décrivent le MÊME dossier ; fusionne-les, et en cas d'écart privilégie l'information la plus précise et la plus récente."
      : "Extrais l'actif à partir des éléments fournis." });
    try {
      const data = await callClaude({ model: AI_MODELS.standard, max_tokens: 1500, system: sys, messages: [{ role: "user", content }] });
      const txt = data.content.filter(b => b.type === "text").map(b => b.text).join("").replace(/```json|```/g, "").trim();
      const p = JSON.parse(txt.slice(txt.indexOf("{"), txt.lastIndexOf("}") + 1));
      // géocodage ville/adresse → lat/lng pour le scoring de zone
      let lat = null, lng = null;
      try { const g = await geocode([p.address, p.city, p.region, "France"].filter(Boolean).join(", ")); if (g) { lat = g.lat; lng = g.lng; } } catch (_) {}
      const deal = {
        runId: "run_" + Date.now(),
        name: p.name || "Dossier analysé", city: p.city || "", region: p.region || "", departement: p.departement || "", lat, lng,
        typology: p.typology || "Autre", price: p.price || 0, rent: p.rent || 0,
        yield: p.yield != null ? p.yield : (p.price && p.rent ? +(p.rent / p.price * 100).toFixed(1) : 0),
        surface: p.surface || 0, occupation: p.occupation || "n.c.", strategy: p.strategy || "core+",
        waltDeal: p.waltDeal || 0, monovalent: p.monovalent === true, tenants: p.tenants || "", strengths: p.strengths || [], risks: p.risks || [],
        vacancy: !!p.vacancy, works: !!p.works,
      };
      setDeal(deal); setAnalyzing(false); setPage("results");
    } catch (e) {
      setAnalyzing(false);
      setAnalyzeErr(e.isAuth
        ? "IA indisponible : le secret ANTHROPIC_API_KEY est manquant ou invalide côté serveur (Supabase). Vous pouvez lancer la démo en attendant."
        : "Analyse impossible. Vérifiez le document, ou lancez la démo.");
    }
  };

  // ── Enseignes (preneurs à bail) — état + couche de données ───
  const [enseignes, setEnseignes] = useState([]);
  const [creatingEns, setCreatingEns] = useState(false);
  const [importEnsOpen, setImportEnsOpen] = useState(false);
  const [openEnsId, setOpenEnsId] = useState(null);
  const [ensEmailFor, setEnsEmailFor] = useState(null);
  const [localDeal, setLocalDeal] = useState(null);
  const [analyzingEns, setAnalyzingEns] = useState(false);
  const [ensAnalyzeErr, setEnsAnalyzeErr] = useState("");

  useEffect(() => { (async () => {
    const { data, error } = await sb.from("enseignes").select("*").order("created_at", { ascending: false });
    if (!error && data) setEnseignes(data.map(rowToEns));
  })(); }, []);

  const addEnseigne = async (ens) => {
    const { data, error } = await sb.from("enseignes").insert(ensToRow(ens)).select().single();
    if (!error && data) setEnseignes(l => [rowToEns(data), ...l]);
    setCreatingEns(false);
  };
  const importEnseignes = async (list) => {
    try {
      const rows = list.map(ensToRow);
      let inserted = [];
      for (let i = 0; i < rows.length; i += 200) {
        const { data, error } = await sb.from("enseignes").insert(rows.slice(i, i + 200)).select();
        if (error) return { inserted: inserted.length, error: error.message };
        if (data) inserted = inserted.concat(data);
      }
      if (inserted.length) setEnseignes(l => [...inserted.map(rowToEns), ...l]);
      return { inserted: inserted.length, error: null };
    } catch (e) { return { inserted: 0, error: String((e && e.message) || e) }; }
  };
  const updateEnseigne = async (ens) => {
    const { data, error } = await sb.from("enseignes").update(ensToRow(ens)).eq("id", ens.id).select().single();
    if (!error && data) setEnseignes(l => l.map(e => e.id === ens.id ? rowToEns(data) : e));
  };
  const deleteEnseigne = async (id) => {
    await sb.from("enseignes").delete().eq("id", id);
    setEnseignes(l => l.filter(e => e.id !== id)); setOpenEnsId(null);
  };
  const applyEnsEnrichment = async (id, entry) => {
    const ens = enseignes.find(e => e.id === id); if (!ens) return;
    const next = { ...ens, enrichments: [entry, ...(ens.enrichments || [])] };
    setEnseignes(l => l.map(e => e.id === id ? next : e));
    await sb.from("enseignes").update({ enrichments: next.enrichments }).eq("id", id);
  };
  const attachLocal = async (ids, localR) => {
    for (const id of ids) {
      const ens = enseignes.find(e => e.id === id); if (!ens) continue;
      const next = [{ ...localR, id: Date.now() + "_" + id }, ...(ens.locaux || [])];
      setEnseignes(l => l.map(e => e.id === id ? { ...e, locaux: next } : e));
      await sb.from("enseignes").update({ locaux: next }).eq("id", id);
    }
  };
  const ENS_FIELD_KEY = { localType: "localTypes", sector: "sector", region: "regions", departement: "departements", surfaceMin: "surfaceMin", surfaceMax: "surfaceMax", rentMax: "rentMax", frontageMin: "frontageMin", fluxMin: "fluxMin" };
  const applyEnsCriterion = async (id, crit) => {
    const ens = enseignes.find(e => e.id === id); if (!ens) return;
    const next = { ...ens };
    const uni = (arr, v) => [...new Set([...(arr || []), v])];
    const num = (v) => { const n = parseFloat(String(v).replace(",", ".").replace(/[^0-9.]/g, "")); return isNaN(n) ? null : n; };
    switch (crit.field) {
      case "localType": next.localTypes = uni(ens.localTypes, crit.value); break;
      case "sector": next.sector = crit.value; break;
      case "region": next.regions = uni(ens.regions, crit.value); break;
      case "departement": next.departements = uni(ens.departements, crit.value); break;
      case "surfaceMin": { const n = num(crit.value); if (n != null) next.surfaceMin = n; break; }
      case "surfaceMax": { const n = num(crit.value); if (n != null) next.surfaceMax = n; break; }
      case "rentMax": { const n = num(crit.value); if (n != null) next.rentMax = n; break; }
      case "frontageMin": { const n = num(crit.value); if (n != null) next.frontageMin = n; break; }
      case "fluxMin": { const n = num(crit.value); if (n != null) next.fluxMin = n; break; }
      default: break;
    }
    const key = ENS_FIELD_KEY[crit.field];
    if (key) next.aiFields = [...new Set([...(ens.aiFields || []), key])];
    next.enrichments = [{ field: "other", label: "Critère affiné", value: (crit.label || crit.field) + " : " + crit.value, source: crit.source || "Enrichissement IA", url: "", confidence: crit.confidence || 85, date: new Date().toLocaleDateString("fr-FR") }, ...(ens.enrichments || [])];
    setEnseignes(l => l.map(e => e.id === id ? next : e));
    await sb.from("enseignes").update(ensToRow(next)).eq("id", id);
  };
  const applyEnsEnrichmentBatch = async (id, entries, criteria) => {
    const ens = enseignes.find(e => e.id === id); if (!ens) return;
    const next = { ...ens };
    const uni = (arr, v) => [...new Set([...(arr || []), v])];
    const num = (v) => { const n = parseFloat(String(v).replace(",", ".").replace(/[^0-9.]/g, "")); return isNaN(n) ? null : n; };
    const aiKeys = new Set(ens.aiFields || []);
    (criteria || []).forEach(c => {
      switch (c.field) {
        case "localType": next.localTypes = uni(next.localTypes, c.value); break;
        case "sector": next.sector = c.value; break;
        case "region": next.regions = uni(next.regions, c.value); break;
        case "departement": next.departements = uni(next.departements, c.value); break;
        case "surfaceMin": { const n = num(c.value); if (n != null) next.surfaceMin = n; break; }
        case "surfaceMax": { const n = num(c.value); if (n != null) next.surfaceMax = n; break; }
        case "rentMax": { const n = num(c.value); if (n != null) next.rentMax = n; break; }
        case "frontageMin": { const n = num(c.value); if (n != null) next.frontageMin = n; break; }
        case "fluxMin": { const n = num(c.value); if (n != null) next.fluxMin = n; break; }
        default: break;
      }
      const k = ENS_FIELD_KEY[c.field]; if (k) aiKeys.add(k);
    });
    next.aiFields = [...aiKeys];
    next.enrichments = [...(entries || []), ...(ens.enrichments || [])];
    setEnseignes(l => l.map(e => e.id === id ? next : e));
    await sb.from("enseignes").update(ensToRow(next)).eq("id", id);
  };
  const openEns = enseignes.find(e => e.id === openEnsId) || null;
  const ensMatches = useMemo(() => localDeal
    ? enseignes.map(ens => ({ ens, ...scoreEnseigne(ens, localDeal) })).sort((a, b) => b.global - a.global)
    : [], [localDeal, enseignes]);

  const startAnalyzeLocal = async (payload) => {
    setEnsAnalyzeErr("");
    if (!payload || (!payload.file && !payload.text)) {
      setAnalyzingEns(true); setTimeout(() => { setLocalDeal({ ...SAMPLE_LOCAL, runId: "run_" + Date.now() }); setAnalyzingEns(false); setPage("ensResults"); }, 1600); return;
    }
    if (!confirmCost("Analyse d'un local par IA (0,50 €).", IA_COSTS.analyse)) return;
    setAnalyzingEns(true);
    const sys = `Tu es analyste en immobilier commercial. À partir de l'annonce d'un LOCAL À LOUER (commerce ou bureaux), extrais le local sous forme de JSON STRICT (sans backticks) :
{"name":"nom court du local","city":"ville","region":"région française","departement":"code et nom du département, ex: 14 — Calvados (déduis-le de la ville/adresse)","address":"adresse si dispo","localType":"un type parmi: ${LOCAL_TYPE_OPTIONS.join(", ")}","surface":m2_ou_null,"rentAnnual":loyer_annuel_HT_HC_ou_null,"rentSqm":loyer_en_euros_par_m2_par_an_ou_null,"frontage":lineaire_facade_m_ou_null,"flux":passages_jour_estimes_ou_null,"droitAuBail":montant_pas_de_porte_en_euros_ou_0,"sector":"activité conseillée ou vide","strengths":["2-4 atouts"],"risks":["1-3 points de vigilance"]}
Déduis rentSqm si loyer annuel et surface connus. Sois factuel.`;
    const content = [];
    if (payload.file) {
      content.push(payload.file.mime === "application/pdf"
        ? { type: "document", source: { type: "base64", media_type: "application/pdf", data: payload.file.b64 } }
        : { type: "image", source: { type: "base64", media_type: payload.file.mime, data: payload.file.b64 } });
    }
    if (payload.text && payload.text.trim()) {
      content.push({ type: "text", text: "Informations complémentaires saisies par le broker :\n\n" + payload.text });
    }
    content.push({ type: "text", text: (payload.file && payload.text && payload.text.trim())
      ? "Extrais le local en combinant l'annonce importée ET les informations complémentaires ci-dessus. Les deux décrivent le MÊME local ; fusionne-les, et en cas d'écart privilégie l'information la plus précise et la plus récente."
      : "Extrais le local à partir des éléments fournis." });
    try {
      const data = await callClaude({ model: AI_MODELS.standard, max_tokens: 1500, system: sys, messages: [{ role: "user", content }] });
      const txt = data.content.filter(b => b.type === "text").map(b => b.text).join("").replace(/```json|```/g, "").trim();
      const p = JSON.parse(txt.slice(txt.indexOf("{"), txt.lastIndexOf("}") + 1));
      let lat = null, lng = null;
      try { const g = await geocode([p.address, p.city, p.region, "France"].filter(Boolean).join(", ")); if (g) { lat = g.lat; lng = g.lng; } } catch (_) {}
      const localDealObj = {
        runId: "run_" + Date.now(),
        name: p.name || "Local analysé", city: p.city || "", region: p.region || "", departement: p.departement || "", lat, lng,
        localType: p.localType || "Pied d'immeuble centre-ville", surface: p.surface || 0,
        rentAnnual: p.rentAnnual || 0, rentSqm: p.rentSqm || (p.surface && p.rentAnnual ? Math.round(p.rentAnnual / p.surface) : 0),
        frontage: p.frontage || 0, flux: p.flux || 0, droitAuBail: p.droitAuBail || 0, sector: p.sector || "",
        strengths: p.strengths || [], risks: p.risks || [],
      };
      setLocalDeal(localDealObj); setAnalyzingEns(false); setPage("ensResults");
    } catch (e) {
      setAnalyzingEns(false);
      setEnsAnalyzeErr(e.isAuth ? "IA indisponible : le secret ANTHROPIC_API_KEY est manquant ou invalide côté serveur (Supabase). Vous pouvez lancer la démo en attendant." : "Analyse impossible. Vérifiez le document, ou lancez la démo.");
    }
  };

  // ── Historique des matchings (persisté dans Supabase : table match_history) ──
  const [matchHistory, setMatchHistory] = useState([]);
  useEffect(() => { (async () => {
    const { data, error } = await sb.from("match_history").select("*").order("created_at", { ascending: false }).limit(300);
    if (!error && data) setMatchHistory(data.map(r => ({ id: r.id, runId: r.run_id, kind: r.kind, subject: r.subject || {}, matches: r.matches || [], ai: r.ai || null, sent: r.sent || [], date: r.created_at })));
  })(); }, []);
  const recordMatchRun = async ({ runId, kind, subject, matches, ai, sent }) => {
    if (!runId) return;
    const row = { run_id: runId, kind, subject: subject || {}, matches: matches || [], ai: ai || null, sent: sent || [] };
    const { data, error } = await sb.from("match_history").upsert(row, { onConflict: "run_id" }).select().single();
    const entry = { id: data ? data.id : runId, runId, kind, subject: row.subject, matches: row.matches, ai: row.ai, sent: row.sent, date: (data && data.created_at) || new Date().toISOString() };
    if (!error) setMatchHistory(l => [entry, ...l.filter(x => x.runId !== runId)]);
  };

  // ── Contacts : mouvements détectés via LinkedIn (déplacer / ajouter / créer société) ──
  const contactKey = (c) => ((c && (c.linkedin || c.name)) || "").trim().toLowerCase();
  const baseCtx = (base) => base === "investors"
    ? { list: investors, setList: setInvestors, toRow: invToRow, fromRow: rowToInv, table: "investors" }
    : { list: enseignes, setList: setEnseignes, toRow: ensToRow, fromRow: rowToEns, table: "enseignes" };
  const moveContact = async ({ base, fromId, toId, contact, newRole }) => {
    const { list, setList, toRow, table } = baseCtx(base);
    const from = list.find(x => x.id === fromId), to = list.find(x => x.id === toId);
    if (!from || !to) return;
    const k = contactKey(contact);
    const moved = { ...contact }; if (newRole) moved.role = newRole;
    const fromNext = { ...from, contacts: (from.contacts || []).filter(c => contactKey(c) !== k) };
    const has = (to.contacts || []).some(c => contactKey(c) === k);
    const toNext = { ...to, contacts: has ? (to.contacts || []).map(c => contactKey(c) === k ? moved : c) : [...(to.contacts || []), moved] };
    setList(l => l.map(x => x.id === fromId ? fromNext : x.id === toId ? toNext : x));
    await sb.from(table).update(toRow(fromNext)).eq("id", fromId);
    await sb.from(table).update(toRow(toNext)).eq("id", toId);
  };
  const addContactToFiche = async ({ base, ficheId, contact }) => {
    const { list, setList, toRow, table } = baseCtx(base);
    const f = list.find(x => x.id === ficheId); if (!f) return;
    const k = contactKey(contact);
    if ((f.contacts || []).some(c => contactKey(c) === k)) return;
    const next = { ...f, contacts: [...(f.contacts || []), contact] };
    setList(l => l.map(x => x.id === ficheId ? next : x));
    await sb.from(table).update(toRow(next)).eq("id", ficheId);
  };
  const createCompanyAndMoveContact = async ({ base, name, type, fromId, contact, newRole, city }) => {
    const today = new Date().toLocaleDateString("fr-FR");
    const moved = { ...(contact || {}) }; if (newRole) moved.role = newRole;
    const enr = [{ field: "other", label: "Société créée", value: "Créée depuis le suivi LinkedIn des contacts" + (type ? " · type : " + type : ""), source: "Suivi LinkedIn", url: "", confidence: 80, date: today }];
    let newId = null;
    if (base === "investors") {
      const inv = { ...EMPTY, name, type: TYPE_OPTIONS.includes(type) ? type : "Investisseur privé", city: city || "", contacts: contact ? [moved] : [], status: "Actif", enrichments: enr };
      const { data, error } = await sb.from("investors").insert(invToRow(inv)).select().single();
      if (!error && data) { const row = rowToInv(data); newId = row.id; setInvestors(l => [row, ...l]); }
    } else {
      const ens = { ...EMPTY_ENS, name, type: "Enseigne nationale", sector: type || EMPTY_ENS.sector, city: city || "", contacts: contact ? [moved] : [], status: "Actif", enrichments: enr };
      const { data, error } = await sb.from("enseignes").insert(ensToRow(ens)).select().single();
      if (!error && data) { const row = rowToEns(data); newId = row.id; setEnseignes(l => [row, ...l]); }
    }
    // retire le contact de son ancienne société
    if (fromId && contact) {
      const { list, setList, toRow, table } = baseCtx(base);
      const from = list.find(x => x.id === fromId);
      if (from) {
        const k = contactKey(contact);
        const fromNext = { ...from, contacts: (from.contacts || []).filter(c => contactKey(c) !== k) };
        setList(l => l.map(x => x.id === fromId ? fromNext : x));
        await sb.from(table).update(toRow(fromNext)).eq("id", fromId);
      }
    }
    return newId;
  };

  const nav = [["dashboard", "Tableau de bord", "◧"], ["investors", "Base investisseurs", "◴"], ["upload", "Analyser un dossier", "⬆"], ["enseignes", "Base enseignes", "▦"], ["uploadLocal", "Analyser un local", "▭"], ["results", "Résultats matching", "◈"], ["settings", "Paramètres du compte", "⚙"]];

  return (
    <div style={{ minHeight: "100vh", background: C.bg, color: C.text, fontFamily: "'Outfit','Helvetica Neue',sans-serif" }}>
      <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,700&display=swap" rel="stylesheet" />
      <style>{`
        * { box-sizing: border-box; }
        ::-webkit-scrollbar { width: 9px; height: 9px; }
        ::-webkit-scrollbar-thumb { background: ${C.lineStrong}; border-radius: 9px; }
        ::selection { background: ${C.violet}; color:#fff; }
        @keyframes rise { from { opacity:0; transform:translateY(14px);} to {opacity:1; transform:none;} }
        @keyframes slideIn { from { transform:translateX(40px); opacity:0;} to {transform:none; opacity:1;} }
        @keyframes spin { to { transform: rotate(360deg);} }
        @keyframes pulse { 0%,100%{opacity:.4;} 50%{opacity:1;} }
        @keyframes grow { from { width:0;} }
        .serif { font-family:'Fraunces',Georgia,serif; }
        .rise { animation: rise .5s cubic-bezier(.2,.7,.3,1) both; }
        .nav { cursor:pointer; padding:11px 14px; border-radius:11px; font-size:14px; font-weight:500; display:flex; align-items:center; gap:11px; transition:.18s; color:${C.dim}; }
        .nav:hover { background:${C.sky}; color:${C.indigo}; }
        .nav.on { background:linear-gradient(100deg,${C.indigo},${C.violet}); color:#fff; box-shadow:0 8px 22px -10px ${C.violet}; }
        .card { background:${C.panel}; border:1px solid ${C.line}; border-radius:18px; box-shadow:0 1px 3px rgba(28,27,51,.04); }
        .btn { cursor:pointer; border:none; border-radius:12px; font-family:inherit; font-size:13px; font-weight:600; padding:11px 20px; transition:.18s; }
        .btn:hover { transform:translateY(-2px); }
        .btn-p { background:linear-gradient(100deg,${C.indigo},${C.violet}); color:#fff; box-shadow:0 10px 26px -12px ${C.violet}; }
        .btn-g { background:${C.panel2}; color:${C.text}; border:1px solid ${C.line}; }
        .chip { font-size:11px; padding:4px 10px; border-radius:8px; background:${C.panel2}; border:1px solid ${C.line}; color:${C.dim}; }
        .bar { height:6px; border-radius:99px; background:${C.panel2}; overflow:hidden; }
        .fill { height:100%; border-radius:99px; animation: grow .8s cubic-bezier(.2,.7,.3,1) both; }
        input,textarea,select { font-family:inherit; }
        input[type=range]{ accent-color:${C.violet}; }
        a { color:${C.indigo}; }

        /* ===== Responsive ===== */
        .app-shell { display:flex; }
        .sidebar { width:252px; padding:28px 18px; border-right:1px solid ${C.line}; position:sticky; top:0; height:100vh; background:${C.panel}; z-index:40; flex:none; }
        .main { flex:1; padding:40px 50px; max-width:1600px; }
        .topbar { display:none; }
        .scrim { display:none; }
        .grid-4 { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; }
        .grid-stats { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; }
        .grid-2 { display:grid; grid-template-columns:1fr 1fr; }

        @media (max-width: 860px) {
          .topbar { display:flex; align-items:center; gap:12px; position:sticky; top:0; z-index:30;
            background:${C.panel}; border-bottom:1px solid ${C.line}; padding:12px 16px; }
          .sidebar { position:fixed; left:0; top:0; height:100vh; transform:translateX(-100%);
            transition:transform .25s cubic-bezier(.2,.7,.3,1); box-shadow:0 0 40px rgba(28,27,51,.18); }
          .sidebar.open { transform:translateX(0); }
          .scrim.show { display:block; position:fixed; inset:0; background:rgba(28,27,51,.35); z-index:35; }
          .main { padding:18px 16px 60px; max-width:100%; }
          .app-shell { flex-direction:column; }
          .grid-4 { grid-template-columns:1fr 1fr; }
          .grid-stats { grid-template-columns:1fr 1fr; }
          .grid-2 { grid-template-columns:1fr; }
          .grid-crit { grid-template-columns:1fr 1fr !important; }
          h1.serif, .serif h1 { font-size:25px !important; }
          .drawer-panel { width:100vw !important; max-width:100vw !important; padding:22px 18px !important; }
          .drawer-cols { grid-template-columns:1fr !important; }
          .modal-card { padding:20px !important; border-radius:16px !important; }
          .hide-sm { display:none !important; }
          table { font-size:12px; }
          th, td { padding:9px 8px !important; }
        }
        @media (max-width: 480px) {
          .grid-4 { grid-template-columns:1fr; }
          .grid-stats { grid-template-columns:1fr 1fr; }
          .grid-crit { grid-template-columns:1fr 1fr !important; }
        }
      `}</style>

      <div className="app-shell" style={{ minHeight: "100vh", width: "100%" }}>
      <div className="topbar">
        <span onClick={() => setMenuOpen(true)} style={{ fontSize: 22, cursor: "pointer", lineHeight: 1, color: C.indigo }}>☰</span>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <div style={{ width: 26, height: 26, borderRadius: 8, background: `linear-gradient(135deg,${C.indigo},${C.violet})`, display: "grid", placeItems: "center", fontWeight: 800, fontSize: 14, color: "#fff" }}>D</div>
          <span className="serif" style={{ fontSize: 17, fontWeight: 700 }}>DoricMatch</span>
        </div>
      </div>
      <div className={"scrim" + (menuOpen ? " show" : "")} onClick={() => setMenuOpen(false)} />

      <aside className={"sidebar" + (menuOpen ? " open" : "")}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 4 }}>
          <div style={{ width: 34, height: 34, borderRadius: 10, background: `linear-gradient(135deg,${C.indigo},${C.violet})`, display: "grid", placeItems: "center", fontWeight: 800, fontSize: 18, color: "#fff", boxShadow: `0 8px 18px -8px ${C.violet}` }}>D</div>
          <div className="serif" style={{ fontSize: 21, fontWeight: 700 }}>DoricMatch</div>
        </div>
        <div style={{ fontSize: 11, color: C.faint, marginBottom: 30, marginLeft: 44, marginTop: -4, letterSpacing: ".5px" }}>AI · matching immobilier</div>
        {nav.map(([k, label, ic]) => (
          <div key={k} className={"nav" + (page === k ? " on" : "")} onClick={() => { setPage(k); setMenuOpen(false); }} style={{ marginBottom: 5 }}>
            <span style={{ fontSize: 15, width: 18, textAlign: "center" }}>{ic}</span>{label}
          </div>
        ))}
        <div onClick={onLogout} className="nav" style={{ position: "absolute", bottom: 84, left: 18, right: 18, justifyContent: "center", fontSize: 12 }}>↪ Se déconnecter</div>
        <div className="card" style={{ position: "absolute", bottom: 18, left: 18, right: 18, padding: 14 }}>
          <div style={{ fontSize: 11, color: C.dim, marginBottom: 6 }}>Enrichissement IA</div>
          <div style={{ fontSize: 11.5, color: C.faint, lineHeight: 1.45 }}>Manuel — depuis le tableau de bord, bouton « Lancer l'enrichissement IA ».</div>
        </div>
      </aside>

      <main className="main">
        {page === "dashboard" && <Dashboard go={setPage} investors={investors} enseignes={enseignes} onEnrich={applyEnrichment} onCriterion={applyCriterion} onEnsEnrich={applyEnsEnrichment} onEnsCriterion={applyEnsCriterion} onAutoEnrich={applyInvEnrichmentBatch} onAutoEnrichEns={applyEnsEnrichmentBatch} onOpen={setOpenId} onOpenEns={setOpenEnsId} onMoveContact={moveContact} onAddContact={addContactToFiche} onCreateCompanyMove={createCompanyAndMoveContact} />}
        {page === "investors" && <Investors investors={investors} onOpen={setOpenId} onCreate={() => setCreating(true)} onDelete={deleteInvestor} onOpenImport={() => setImportOpen(true)} />}
        {page === "upload" && <Upload onAnalyze={startAnalyze} analyzing={analyzing} analyzeErr={analyzeErr} />}
        {page === "enseignes" && <Enseignes enseignes={enseignes} local={localDeal} onOpen={setOpenEnsId} onCreate={() => setCreatingEns(true)} onDelete={deleteEnseigne} onOpenImport={() => setImportEnsOpen(true)} onEnrich={applyEnsEnrichment} onCriterion={applyEnsCriterion} go={setPage} />}
        {page === "uploadLocal" && <UploadLocal onAnalyze={startAnalyzeLocal} analyzing={analyzingEns} analyzeErr={ensAnalyzeErr} />}
        {page === "ensResults" && <EnseigneResults local={localDeal} matches={ensMatches} onEmail={setEnsEmailFor} onOpen={setOpenEnsId} onAttach={attachLocal} history={matchHistory} onRecordRun={recordMatchRun} goOpenInv={setOpenId} go={setPage} />}
        {page === "results" && <Results deal={deal} matches={matches} onEmail={setEmailFor} onOpen={setOpenId} onAttach={attachDossier} go={setPage} history={matchHistory} onRecordRun={recordMatchRun} onOpenEns={setOpenEnsId} />}
        {page === "settings" && <Settings settings={settings} setSettings={setSettings} isAdmin={isAdmin} />}
      </main>
      </div>

      {emailFor && <EmailModal m={emailFor} deal={deal} onClose={() => setEmailFor(null)} />}
      {openInv && <InvestorDrawer inv={openInv} deal={deal} onClose={() => setOpenId(null)} onSave={updateInvestor} onDelete={deleteInvestor} />}
      {creating && <CreateInvestorModal onClose={() => setCreating(false)} onSave={addInvestor} />}
      {importOpen && <ImportInvestorsModal existing={investors} onClose={() => setImportOpen(false)} onImport={importInvestors} />}
      {ensEmailFor && <EnseigneEmailModal m={ensEmailFor} local={localDeal} settings={settings} onClose={() => setEnsEmailFor(null)} />}
      {openEns && <EnseigneDrawer ens={openEns} local={localDeal} onClose={() => setOpenEnsId(null)} onSave={updateEnseigne} onDelete={deleteEnseigne} />}
      {creatingEns && <CreateEnseigneModal onClose={() => setCreatingEns(false)} onSave={addEnseigne} />}
      {importEnsOpen && <ImportEnseignesModal existing={enseignes} onClose={() => setImportEnsOpen(false)} onImport={importEnseignes} />}
    </div>
  );
}

function App() {
  const [session, setSession] = useState(undefined);
  const [access, setAccess] = useState("loading"); // loading | approved | pending | rejected | none
  useEffect(() => {
    sb.auth.getSession().then(({ data }) => setSession(data.session));
    const { data: sub } = sb.auth.onAuthStateChange((_e, s) => setSession(s));
    return () => sub.subscription.unsubscribe();
  }, []);
  const user = session ? session.user : null;
  const uid = user ? user.id : null;
  const email = user ? user.email : null;
  const isAdmin = !!(email && email === ADMIN_EMAIL);
  const checkAccess = React.useCallback(async () => {
    if (!uid) return;
    if (email === ADMIN_EMAIL) { setAccess("approved"); return; }
    setAccess("loading");
    const { data, error } = await sb.from("profiles").select("status").eq("id", uid).maybeSingle();
    if (error) { setAccess("none"); return; }
    setAccess(data && data.status ? data.status : "none");
  }, [uid, email]);
  useEffect(() => { if (uid) checkAccess(); }, [uid, email, checkAccess]);

  if (session === undefined) return <Splash text="Chargement…" />;
  if (!session) return <Login />;
  if (access === "loading") return <Splash text="Vérification de l'accès…" />;
  if (access !== "approved") return <PendingApproval status={access} email={email} onRecheck={checkAccess} onLogout={() => sb.auth.signOut()} />;
  return <InvestMatchApp onLogout={() => sb.auth.signOut()} isAdmin={isAdmin} />;
}

function PendingApproval({ status, email, onRecheck, onLogout }) {
  const [checking, setChecking] = useState(false);
  const rejected = status === "rejected";
  const recheck = async () => { setChecking(true); await onRecheck(); setChecking(false); };
  const wrap = { minHeight: "100vh", display: "grid", placeItems: "center", padding: 20, fontFamily: "'Outfit',sans-serif", background: "radial-gradient(140% 120% at 50% -10%, #eef0ff 0%, #f5f6fb 55%)" };
  const card = { width: "100%", maxWidth: 440, background: "#fff", border: "1px solid #e4e3f0", borderRadius: 22, padding: "40px 36px", textAlign: "center", boxShadow: "0 30px 80px -34px rgba(45,42,90,.4)" };
  const btnP = { width: "100%", padding: "12px", borderRadius: 12, border: "none", background: "linear-gradient(100deg,#5b63e8,#7c5cff)", color: "#fff", fontSize: 14, fontWeight: 600, cursor: "pointer", fontFamily: "inherit" };
  const btnG = { width: "100%", marginTop: 10, padding: "11px", borderRadius: 12, border: "1px solid #e4e3f0", background: "#fff", color: "#6b6a85", fontSize: 13.5, fontWeight: 500, cursor: "pointer", fontFamily: "inherit" };
  return (
    <div style={wrap}>
      <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Fraunces:opsz,wght@9..144,600&display=swap" rel="stylesheet" />
      <div style={card}>
        <div style={{ width: 56, height: 56, borderRadius: 16, margin: "0 auto 18px", display: "grid", placeItems: "center", background: rejected ? "rgba(226,72,107,.1)" : "rgba(91,99,232,.1)", color: rejected ? "#e2486b" : "#5b63e8" }}>
          {rejected
            ? <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="m15 9-6 6M9 9l6 6" /></svg>
            : <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 7.5V12l3 2" /></svg>}
        </div>
        <div style={{ fontFamily: "'Fraunces',serif", fontSize: 22, fontWeight: 600, color: "#1c1b33" }}>
          {rejected ? "Accès refusé" : "Compte en attente de validation"}
        </div>
        <p style={{ fontSize: 13.5, color: "#6b6a85", lineHeight: 1.55, margin: "10px 0 24px" }}>
          {rejected
            ? "Votre demande d'accès n'a pas été validée. Contactez l'administrateur si vous pensez qu'il s'agit d'une erreur."
            : <>Le compte <b style={{ color: "#1c1b33" }}>{email}</b> a bien été créé. Un administrateur doit valider votre accès avant que vous puissiez utiliser DoricMatch.</>}
        </p>
        {!rejected && <button style={{ ...btnP, opacity: checking ? .6 : 1 }} onClick={recheck} disabled={checking}>{checking ? "Vérification…" : "Vérifier à nouveau"}</button>}
        <button style={btnG} onClick={onLogout}>Se déconnecter</button>
      </div>
    </div>
  );
}

function Login() {
  const [email, setEmail] = useState(""); const [pw, setPw] = useState("");
  const [mode, setMode] = useState("login"); const [msg, setMsg] = useState(""); const [busy, setBusy] = useState(false);
  const [showPw, setShowPw] = useState(false);
  const submit = async (e) => {
    if (e && e.preventDefault) e.preventDefault();
    // On lit les valeurs réelles du DOM : capte aussi le remplissage automatique de Chrome.
    const form = (e && e.target && e.target.tagName === "FORM") ? e.target : null;
    const elEmail = (form && form.elements) ? form.elements.email : null;
    const elPw = (form && form.elements) ? form.elements.password : null;
    const emailV = ((elEmail && elEmail.value != null ? elEmail.value : email) || "").trim();
    const pwV = (elPw && elPw.value != null ? elPw.value : pw) || "";
    if (!emailV || !pwV) { setMsg("Renseignez votre email et votre mot de passe."); return; }
    setBusy(true); setMsg("");
    if (mode === "login") { const { error } = await sb.auth.signInWithPassword({ email: emailV, password: pwV }); if (error) setMsg("Identifiants incorrects."); }
    else { const { error } = await sb.auth.signUp({ email: emailV, password: pwV }); setMsg(error ? "Erreur : " + error.message : "Compte créé. Vous pouvez vous connecter."); if (!error) setMode("login"); }
    setBusy(false);
  };
  const ok = msg.startsWith("Compte");
  const year = new Date().getFullYear();
  return (
    <div className="lg-wrap">
      <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&display=swap" rel="stylesheet" />
      <style>{`
        .lg-wrap{min-height:100vh;display:grid;place-items:center;padding:20px;font-family:'Outfit',sans-serif;background:radial-gradient(140% 120% at 50% -10%, #eef0ff 0%, ${C.bg} 55%);}
        .lg-shell{width:100%;max-width:940px;display:grid;grid-template-columns:1fr 1.05fr;background:#fff;border:1px solid ${C.line};border-radius:24px;overflow:hidden;box-shadow:0 30px 80px -34px rgba(45,42,90,.4);animation:lg-in .55s cubic-bezier(.2,.7,.3,1) both;}
        .lg-brand{position:relative;overflow:hidden;padding:42px 40px;display:flex;flex-direction:column;color:#fff;background:radial-gradient(120% 95% at 0% 0%, rgba(150,156,255,.55) 0%, rgba(150,156,255,0) 52%), linear-gradient(155deg,#343a9c 0%, ${C.indigo} 52%, ${C.violet} 100%);}
        .lg-logo{display:flex;align-items:center;gap:11px;position:relative;z-index:1;}
        .lg-logo-mark{width:38px;height:38px;border-radius:12px;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.3);display:grid;place-items:center;font-family:'Fraunces',serif;font-weight:700;font-size:20px;}
        .lg-logo-name{font-family:'Fraunces',serif;font-weight:700;font-size:22px;letter-spacing:.2px;}
        .lg-logo-badge{font-size:9.5px;font-weight:700;letter-spacing:.14em;padding:3px 7px;border-radius:999px;background:rgba(255,255,255,.18);}
        .lg-headline{position:relative;z-index:1;font-family:'Fraunces',Georgia,serif;font-size:33px;line-height:1.1;font-weight:600;margin:40px 0 0;letter-spacing:-.4px;}
        .lg-headline span{color:#dcd9ff;}
        .lg-lead{position:relative;z-index:1;margin:16px 0 0;font-size:13.5px;line-height:1.55;color:rgba(255,255,255,.82);max-width:320px;}
        .lg-feats{position:relative;z-index:1;list-style:none;margin:30px 0 0;padding:0;display:flex;flex-direction:column;gap:15px;}
        .lg-feat{display:flex;gap:12px;align-items:flex-start;}
        .lg-feat-ic{flex:none;width:34px;height:34px;border-radius:10px;background:rgba(255,255,255,.13);border:1px solid rgba(255,255,255,.22);display:grid;place-items:center;}
        .lg-feat-t{font-size:13.5px;font-weight:600;}
        .lg-feat-d{font-size:12px;color:rgba(255,255,255,.72);margin-top:1px;line-height:1.4;}
        .lg-foot{position:relative;z-index:1;margin-top:auto;padding-top:30px;font-size:11.5px;color:rgba(255,255,255,.62);letter-spacing:.04em;}
        .lg-orbit{position:absolute;right:-96px;bottom:-96px;width:300px;height:300px;opacity:.55;pointer-events:none;}
        .lg-form{padding:48px 46px;display:flex;align-items:center;}
        .lg-form-inner{width:100%;max-width:340px;margin:0 auto;}
        .lg-title{font-family:'Fraunces',Georgia,serif;font-size:26px;font-weight:600;color:${C.text};margin:0;}
        .lg-formsub{font-size:13px;color:${C.dim};margin:5px 0 26px;}
        .lg-label{display:block;font-size:11px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:${C.faint};margin:0 0 7px;}
        .lg-field{position:relative;margin-bottom:16px;}
        .lg-ic{position:absolute;left:13px;top:50%;transform:translateY(-50%);color:${C.faint};display:flex;pointer-events:none;}
        .lg-input{width:100%;box-sizing:border-box;padding:13px 14px 13px 42px;border:1.5px solid ${C.line};border-radius:12px;font-size:14.5px;font-family:inherit;color:${C.text};background:#fff;outline:none;transition:border-color .16s, box-shadow .16s;}
        .lg-input::placeholder{color:#bdbbce;}
        .lg-input:focus{border-color:${C.indigo};box-shadow:0 0 0 4px rgba(91,99,232,.14);}
        .lg-input-pw{padding-right:46px;}
        .lg-eye{position:absolute;right:7px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:${C.faint};padding:7px;border-radius:9px;display:flex;transition:color .15s, background .15s;}
        .lg-eye:hover,.lg-eye:focus-visible{color:${C.indigo};background:${C.panel2};outline:none;}
        .lg-cta{width:100%;margin-top:4px;padding:13px;border:none;border-radius:12px;background:linear-gradient(100deg,${C.indigo},${C.violet});color:#fff;font-family:inherit;font-size:14.5px;font-weight:600;letter-spacing:.2px;cursor:pointer;box-shadow:0 14px 30px -14px rgba(124,92,255,.7);transition:transform .16s, filter .16s, opacity .16s;display:flex;align-items:center;justify-content:center;gap:9px;min-height:46px;}
        .lg-cta:hover:not(:disabled){transform:translateY(-1px);filter:brightness(1.05);}
        .lg-cta:focus-visible{outline:2px solid ${C.indigo};outline-offset:2px;}
        .lg-cta:disabled{opacity:.65;cursor:default;box-shadow:none;}
        .lg-spin{width:16px;height:16px;border:2px solid rgba(255,255,255,.45);border-top-color:#fff;border-radius:50%;animation:lg-spin .7s linear infinite;}
        .lg-msg{font-size:12.5px;line-height:1.45;padding:10px 12px;border-radius:10px;margin-bottom:14px;}
        .lg-msg-err{background:rgba(226,72,107,.08);color:${C.rose};border:1px solid rgba(226,72,107,.18);}
        .lg-msg-ok{background:rgba(16,168,126,.09);color:${C.emerald};border:1px solid rgba(16,168,126,.2);}
        .lg-toggle{text-align:center;margin-top:20px;font-size:13px;color:${C.dim};}
        .lg-toggle b{color:${C.indigo};cursor:pointer;font-weight:600;}
        .lg-secure{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:20px;font-size:11.5px;color:#b1afc4;line-height:1.4;text-align:center;}
        @keyframes lg-in{from{opacity:0;transform:translateY(14px);}to{opacity:1;transform:none;}}
        @keyframes lg-spin{to{transform:rotate(360deg);}}
        @media (max-width:880px){
          .lg-shell{grid-template-columns:1fr;max-width:430px;}
          .lg-brand{padding:26px 28px;}
          .lg-headline{font-size:22px;margin-top:18px;}
          .lg-lead,.lg-feats,.lg-foot,.lg-orbit{display:none;}
          .lg-form{padding:30px 28px;}
          .lg-form-inner{max-width:none;}
        }
        @media (prefers-reduced-motion:reduce){
          .lg-shell{animation:none;}
          .lg-spin{animation:none;border-top-color:rgba(255,255,255,.45);}
          .lg-cta:hover:not(:disabled){transform:none;}
        }
      `}</style>

      <div className="lg-shell">
        <aside className="lg-brand">
          <div className="lg-logo">
            <div className="lg-logo-mark">D</div>
            <div className="lg-logo-name">DoricMatch</div>
            <span className="lg-logo-badge">AI</span>
          </div>
          <h1 className="lg-headline">Le bon bien,<br /><span>le bon partenaire.</span></h1>
          <p className="lg-lead">Le CRM de matching immobilier de DORIC GROUPE. Reliez chaque actif à l'investisseur — et chaque local à louer à l'enseigne — dont les critères correspondent.</p>
          <ul className="lg-feats">
            <li className="lg-feat">
              <span className="lg-feat-ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3Z" /><path d="M18.5 14l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8.8-2.2Z" /></svg></span>
              <div><div className="lg-feat-t">Matching IA double sens</div><div className="lg-feat-d">Actif ↔ investisseur et local ↔ enseigne, scoring instantané.</div></div>
            </li>
            <li className="lg-feat">
              <span className="lg-feat-ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="3" width="14" height="18" rx="2" /><path d="M8.5 8h7M8.5 12h7M8.5 16h4" /></svg></span>
              <div><div className="lg-feat-t">Dossiers & mémorandums</div><div className="lg-feat-d">Dossiers d'investissement et de commercialisation en quelques minutes.</div></div>
            </li>
            <li className="lg-feat">
              <span className="lg-feat-ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="8.5" /><path d="M3.5 12h17M12 3.5c2.4 2.4 2.4 14.6 0 17M12 3.5c-2.4 2.4-2.4 14.6 0 17" /></svg></span>
              <div><div className="lg-feat-t">Enrichissement web</div><div className="lg-feat-d">Acquéreurs et plans de développement des enseignes, tenus à jour.</div></div>
            </li>
          </ul>
          <div className="lg-foot">© {year} DORIC GROUPE</div>
          <svg className="lg-orbit" viewBox="0 0 300 300" fill="none" aria-hidden="true">
            <circle cx="150" cy="150" r="62" stroke="rgba(255,255,255,.5)" strokeWidth="1" />
            <circle cx="150" cy="150" r="104" stroke="rgba(255,255,255,.38)" strokeWidth="1" />
            <circle cx="150" cy="150" r="146" stroke="rgba(255,255,255,.26)" strokeWidth="1" />
            <circle cx="212" cy="150" r="4" fill="rgba(255,255,255,.85)" />
            <circle cx="150" cy="46" r="3.5" fill="rgba(255,255,255,.6)" />
            <circle cx="96" cy="238" r="3" fill="rgba(255,255,255,.45)" />
          </svg>
        </aside>

        <main className="lg-form">
          <div className="lg-form-inner">
            <h2 className="lg-title">{mode === "login" ? "Connexion" : "Créer un compte"}</h2>
            <p className="lg-formsub">{mode === "login" ? "Accédez à votre espace." : "Créez votre accès en quelques secondes."}</p>
            <form onSubmit={submit} autoComplete="on">
              <label className="lg-label" htmlFor="lg-email">Email</label>
              <div className="lg-field">
                <span className="lg-ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5" /><path d="m3.5 7 8.5 6 8.5-6" /></svg></span>
                <input id="lg-email" className="lg-input" type="email" name="email" autoComplete="username" placeholder="vous@societe.fr" value={email} onChange={e => setEmail(e.target.value)} />
              </div>
              <label className="lg-label" htmlFor="lg-pw">Mot de passe</label>
              <div className="lg-field">
                <span className="lg-ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><rect x="4.5" y="10.5" width="15" height="10" rx="2.2" /><path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" /></svg></span>
                <input id="lg-pw" className="lg-input lg-input-pw" type={showPw ? "text" : "password"} name="password" autoComplete={mode === "login" ? "current-password" : "new-password"} placeholder="••••••••" value={pw} onChange={e => setPw(e.target.value)} />
                <button type="button" className="lg-eye" onClick={() => setShowPw(v => !v)} aria-label={showPw ? "Masquer le mot de passe" : "Afficher le mot de passe"}>
                  {showPw
                    ? <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="m3 3 18 18" /><path d="M10.6 6.2A10.9 10.9 0 0 1 12 6c6.5 0 10 6 10 6a17.8 17.8 0 0 1-3.4 4" /><path d="M6.6 6.7A17.6 17.6 0 0 0 2 12s3.5 7 10 7a10.7 10.7 0 0 0 4-.8" /><path d="M9.5 9.5a3 3 0 0 0 4.2 4.2" /></svg>
                    : <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" /><circle cx="12" cy="12" r="3" /></svg>}
                </button>
              </div>
              {msg && <div className={"lg-msg " + (ok ? "lg-msg-ok" : "lg-msg-err")}>{msg}</div>}
              <button type="submit" className="lg-cta" disabled={busy}>
                {busy ? <span className="lg-spin"></span> : (mode === "login" ? "Se connecter" : "Créer le compte")}
              </button>
            </form>
            <div className="lg-toggle">
              {mode === "login" ? "Pas encore d'accès ? " : "Déjà un compte ? "}
              <b onClick={() => { setMode(mode === "login" ? "signup" : "login"); setMsg(""); }}>{mode === "login" ? "Créer un compte" : "Se connecter"}</b>
            </div>
            <div className="lg-secure">
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="4.5" y="10.5" width="15" height="10" rx="2.2" /><path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" /></svg>
              Session non conservée — reconnexion à la fermeture du navigateur.
            </div>
          </div>
        </main>
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
