// app.jsx - VaultCova orchestrator (shell, routing, chat engine, tweaks)
const { useState, useEffect, useRef, useCallback } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "dark": false,
  "accent": "#5b3df5",
  "logoVariant": "vault",
  "heroVariant": "blend",
  "heroScene": "auto",
  "vaultLayout": "cards",
  "chatStyle": "glass",
  "voiceMode": "autosend",
  "covaVoice": "on"
}/*EDITMODE-END*/;

const ACCENTS = {
  "#5b3df5": { name: "Violet", light: { a: "#5b3df5", a2: "#7a5cff", ink: "#3a23c9", soft: "#ece8ff" }, dark: { a: "#7c5cff", a2: "#9a80ff", ink: "#b7a6ff", soft: "#1d1b44" } },
  "#2a6fdb": { name: "Azure", light: { a: "#2a6fdb", a2: "#4f8ff5", ink: "#1b4ea3", soft: "#e3edfc" }, dark: { a: "#5b9bff", a2: "#7fb4ff", ink: "#a9cdff", soft: "#0f2140" } },
  "#0b9d6f": { name: "Emerald", light: { a: "#0b9d6f", a2: "#23c08a", ink: "#077a55", soft: "#dcf5ec" }, dark: { a: "#34d399", a2: "#5ee6b4", ink: "#9af0cf", soft: "#0c2b22" } },
  "#b5872a": { name: "Gold", light: { a: "#b5872a", a2: "#d4a23e", ink: "#8a6418", soft: "#f6ecd6" }, dark: { a: "#e0b252", a2: "#f0c674", ink: "#f3d99a", soft: "#2e2410" } },
};

function accentVars(hex, isDark) {
  const p = (ACCENTS[hex] || ACCENTS["#5b3df5"])[isDark ? "dark" : "light"];
  return { "--accent": p.a, "--accent-2": p.a2, "--accent-ink": p.ink, "--accent-soft": p.soft, "--shadow-glow": `0 18px 50px ${p.a}59` };
}

// Resolve the asset a user named in chat (e.g. "the Corolla", "old laptop") to a
// real vault entry, by counting shared words. Used by Cova's delete/rename.
function matchAssetByName(assets, query) {
  const q = String(query || "").toLowerCase().trim();
  if (!q) return null;
  let best = null, score = 0;
  for (const a of assets) {
    const name = a.name.toLowerCase();
    if (q === name) return a;
    let s = name.split(/\s+/).filter((w) => w.length > 2 && q.includes(w)).length;
    if (q.includes(name) || name.includes(q)) s += 2;
    if (s > score) { score = s; best = a; }
  }
  return score > 0 ? best : null;
}
// A vault asset is "bound" (has live cover) once an insurer is attached.
function isAssetBound(a) { return !!a && (a.status === "insured" || !!a.insurer); }

function useIsMobile() {
  const [m, setM] = useState(() => window.matchMedia("(max-width: 760px)").matches);
  useEffect(() => {
    const q = window.matchMedia("(max-width: 760px)");
    const h = (e) => setM(e.matches);
    q.addEventListener("change", h);
    return () => q.removeEventListener("change", h);
  }, []);
  return m;
}

const NAV = [
  { id: "dashboard", tkey: "nav.home", icon: "dashboard" },
  { id: "vaultlog", tkey: "nav.vaultlog", icon: "vault" },
  { id: "insure", tkey: "nav.insure", icon: "shield" },
  { id: "claims", tkey: "nav.claims", icon: "claim" },
  { id: "insight", tkey: "nav.insight", icon: "eye" },
  { id: "rewards", tkey: "nav.rewards", icon: "trend" },
];

const LANGS = window.VC_LANGS;
// render-synced globals so chrome + chat can read language / voice settings
// without threading props through every identical line
let __vcLang = "en";

// infer personal vs business from the visitor's first sentence (guest flow)
function inferProfile(msg) {
  return /\b(fleet|truck|lorry|logistics|compan|business|warehouse|cargo|inventory|stock|staff|office|plant|generator|forklift|factory|shop|enterprise|ltd)\b/i.test(msg || "") ? "business" : "individual";
}
const WELCOME = {
  en: (n, biz) => `Welcome to VaultCova, ${n}! 👋 I'm **Cova** - I live right here, on every screen. I've started your VaultLog with your ${biz ? "company's " : ""}known assets and spotted a few coverage gaps.`,
  pcm: (n, biz) => `${n}, how far! 👋 Na me be **Cova** - I dey here on every screen. I don already arrange your VaultLog with ${biz ? "your company " : "your "}assets, and I see small gaps for your cover.`,
  yo: (n, biz) => `Káàbọ̀ sí VaultCova, ${n}! 👋 Èmi ni **Cova** - mo wà níbí lórí gbogbo ojú-ìwé. Mo ti ṣàkọ́sílẹ̀ àwọn ohun-ìní rẹ sínú VaultLog, mo sì rí àwọn àlàfo ìdáàbòbò díẹ̀.`,
  ha: (n, biz) => `Barka da zuwa VaultCova, ${n}! 👋 Ni ne **Cova** - ina nan a kowane shafi. Na riga na shirya VaultLog ɗinka da kadarorinka, kuma na ga wasu gibin inshora.`,
  ig: (n, biz) => `Nnọọ na VaultCova, ${n}! 👋 Abụ m **Cova** - anọ m ebe a na ihu ọ bụla. Edebela m VaultLog gị na akụ gị ndi amaara, ahụkwara m ụfọdụ oghere mkpuchi.`,
};

// ---- session persistence + inactivity policy ----
// Makes the customer session behave for real: it survives refresh, a true
// sign-out clears it (client state + backend cookie), and the account
// auto-locks after a period of inactivity.
const SESSION_KEY = "vc_auth_v1";
// Auto sign-out after 5 minutes idle. `window.__VC_IDLE_MS` lets tests shorten it.
const IDLE_MS = (typeof window !== "undefined" && window.__VC_IDLE_MS) || 5 * 60 * 1000;
function loadSession() {
  try { return JSON.parse(localStorage.getItem(SESSION_KEY) || "null"); } catch (e) { return null; }
}
function saveSession(s) {
  try { s ? localStorage.setItem(SESSION_KEY, JSON.stringify(s)) : localStorage.removeItem(SESSION_KEY); } catch (e) {}
}

// The user's explicit light/dark choice, persisted per-browser. Absent until they
// pick one — so a brand-new browser still starts on the ops-configured default.
const THEME_KEY = "vc_theme";
function loadUserTheme() {
  try { const s = localStorage.getItem(THEME_KEY); return s === "dark" ? true : s === "light" ? false : null; } catch (e) { return null; }
}

function App() {
  // Seed the initial theme from the user's saved choice so it's correct on the
  // very first paint (no flash), falling back to the build/admin default.
  const initialTweaks = React.useMemo(() => {
    const ut = loadUserTheme();
    return ut === null ? TWEAK_DEFAULTS : { ...TWEAK_DEFAULTS, dark: ut };
  }, []);
  const [t, setTweak] = window.useTweaks(initialTweaks);
  // An explicit user theme choice: apply AND persist, so it survives sessions
  // until the user changes it again. All light/dark toggles route through this.
  const setDark = useCallback((next) => {
    setTweak("dark", !!next);
    try { localStorage.setItem(THEME_KEY, next ? "dark" : "light"); } catch (e) {}
  }, [setTweak]);
  const restored = useRef(loadSession());
  // A ?reset=TOKEN link (forgot password) opens straight onto the new-password
  // form — it outranks any persisted session, since the person may be locked out.
  const resetTok = useRef((() => { try { return new URLSearchParams(location.search).get("reset"); } catch (e) { return null; } })());
  // Start on the app surface if a session was persisted (survive refresh).
  const [screen, setScreen] = useState(() => (resetTok.current ? "auth" : restored.current?.authed ? "app" : "landing")); // landing | auth | onboarding | app
  const [profile, setProfile] = useState(() => restored.current?.profile || "business");
  const [tab, setTab] = useState(() => {
    // Clean entry aliases (/vault, /dashboard) are rewritten to the app; open the
    // matching tab when a signed-in user lands on one. Everything else = dashboard.
    try { const p = (location.pathname || "").replace(/\/+$/, ""); if (p === "/vault" || p === "/vaultlog") return "vaultlog"; } catch (e) {}
    return "dashboard";
  });
  const [assets, setAssets] = useState([]);
  const [claims, setClaims] = useState([]);
  // Real escalations only — Cova's addFlag pushes them during the session.
  // (The old AI_FLAGS mock put a fake red badge on every account.)
  const [flags, setFlags] = useState([]);
  const [flashId, setFlashId] = useState(null);
  const [insurePreselect, setInsurePreselect] = useState(null);
  // Ops-curated capability catalog (which plans include what — /admin/capabilities).
  const [caps, setCaps] = useState(null);
  const [settingsOpenSec, setSettingsOpenSec] = useState(null); // deep-link a Settings tab (e.g. billing from an upgrade CTA)
  const [toasts, setToasts] = useState([]);
  const [dockOpen, setDockOpen] = useState(true);
  const [messages, setMessages] = useState([]);
  const messagesRef = useRef([]);
  messagesRef.current = messages; // always-fresh transcript for the Cova call (no stale closure)
  const [busy, setBusy] = useState(false);
  const [payInfo, setPayInfo] = useState(null); // billing checkout {amount,label}
  const [lang, setLang] = useState(() => window.I18N.loadLang());
  const changeLang = useCallback((l) => { setLang(l); window.I18N.saveLang(l); }, []);
  const isMobile = useIsMobile();
  // on mobile the Cova dock is a full-screen sheet - start closed
  useEffect(() => { if (isMobile) setDockOpen(false); }, [isMobile]);
  const [migrateOpen, setMigrateOpen] = useState(false);
  const pendingMsg = useRef(null); // first message typed on the landing page
  const [fromGuest, setFromGuest] = useState(false); // arrived at auth from a guest chat
  const [authMode, setAuthMode] = useState("signup"); // open Auth in signup or login
  const [graph, setGraph] = useState(() => window.VC_DATA.emptyGraph());
  const [plan, setPlan] = useState("free");
  const [notifs, setNotifs] = useState({ unread: 0, items: [] });
  const [panel, setPanel] = useState([]); // live underwriter panel (ops-managed)
  const [openAssetId, setOpenAssetId] = useState(null); // deep-link: open this asset in VaultLog
  const shownRecs = useRef({});
  const nudgedUpgrade = useRef(false);

  // Driven by the signed-in account (set from the session/bootstrap response).
  // Empty until we know who they are (bootstrap/session/signup set the real
  // name). Guests are never given a placeholder name — Cova won't invent one.
  const [userName, setUserName] = useState("");
  const [userAvatar, setUserAvatar] = useState(""); // preset id ("av1".."av5") or uploaded data URL
  const [userAddress, setUserAddress] = useState(""); // verified address, stamped onto schedules/policies
  const [businessName, setBusinessName] = useState(""); // company name (business accounts), picked by Cova
  // KYC is deliberately NOT shown on entry — the user settles in and builds their
  // vault first. Verification is only required (and only pops up) when they go to
  // buy insurance. `kycDone` reflects whether they've already verified.
  const [kycDone, setKycDone] = useState(false);

  // ---- backend bridge: real DB + commission engine + Claude assessment ----
  const api = (path, body) => fetch(path, {
    method: body ? "POST" : "GET",
    headers: { "Content-Type": "application/json" },
    body: body ? JSON.stringify(body) : undefined,
  }).then((r) => (r.ok ? r.json() : Promise.reject(r))).catch((e) => { console.warn("api", path, e); return null; });

  // Edit an asset's identity fields (brand, condition, location, serial) any time.
  const onUpdateAsset = useCallback((id, patch) => {
    // Optimistic: reflect the raw keys we sent immediately.
    setAssets((prev) => prev.map((a) => (a.id === id ? { ...a, ...patch } : a)));
    fetch("/api/assets/" + id, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch) })
      .then((r) => (r.ok ? r.json() : null))
      .then((d) => {
        // Apply the authoritative server proto so EVERY derived field stays in
        // sync — purchaseValue/replacementValue/sumInsured/status — not only the
        // keys we sent. Without this the valuation editor (which reads
        // purchaseValue/replacementValue) showed stale numbers after a save.
        // Keep the client-only bundle tag, which the proto doesn't carry.
        if (d && d.id) setAssets((prev) => prev.map((a) => (a.id === id ? { ...a, ...d, bundle: a.bundle } : a)));
      })
      .catch(() => {});
  }, []);

  // Delete an asset by id (from the VaultLog detail sheet). Insured/partial
  // assets are blocked server-side (409) and hidden in the UI. Optimistic with
  // rollback; guest-only assets (VA- ids) are removed locally without a call.
  const onDeleteAsset = useCallback((id) => {
    let removed = null;
    setAssets((prev) => { removed = prev.find((x) => x.id === id) || null; return prev.filter((x) => x.id !== id); });
    const persisted = id && !String(id).startsWith("VA-");
    if (!persisted) { if (removed) toast(`Removed ${removed.name} from your VaultLog`, "check"); return; }
    fetch("/api/assets/" + id, { method: "DELETE" })
      .then((r) => r.json().then((res) => ({ ok: r.ok, res })))
      .then(({ ok, res }) => {
        if (!ok) { toast((res && res.error) || "Couldn't delete that.", "alert"); if (removed) setAssets((prev) => [removed, ...prev]); }
        else if (removed) toast(`Removed ${removed.name} from your VaultLog`, "check");
      })
      .catch(() => { toast("Couldn't reach the vault, try again.", "alert"); if (removed) setAssets((prev) => [removed, ...prev]); });
  }, [toast]);

  // Notifications (what ops sent back after reviewing a flagged submission).
  const loadNotifs = useCallback(() => { api("/api/notifications").then((d) => { if (d && Array.isArray(d.items)) setNotifs(d); }); }, []);
  // The live underwriter panel — quote previews and chat offers mirror ops config.
  const loadPanel = useCallback(() => { api("/api/panel").then((d) => { if (d && Array.isArray(d.panel)) setPanel(d.panel); }); }, []);
  // Mirror the verified owner + address to globals so the PDF generator (pdf.jsx)
  // can stamp them onto schedules/certificates without threading through props.
  React.useEffect(() => { window.VC_ADDRESS = userAddress || ""; window.VC_OWNER = userName || ""; window.VC_BUSINESS = businessName || ""; }, [userAddress, userName, businessName]);
  const markNotifsRead = useCallback(() => {
    setNotifs((n) => ({ unread: 0, items: n.items.map((x) => ({ ...x, read: true })) }));
    api("/api/notifications", { all: true });
  }, []);

  // The ops console is a SEPARATE, password-gated app at /admin. The customer
  // app never switches into it, so there's no in-app admin entry here.

  // theme
  useEffect(() => {
    document.documentElement.dataset.theme = t.dark ? "dark" : "light";
  }, [t.dark]);

  // html lang attribute follows the chosen language
  useEffect(() => { document.documentElement.lang = lang; }, [lang]);

  // mirror language + voice settings to render-synced globals (read by Root + chat)
  __vcLang = lang;
  window.__vcVoiceMode = t.voiceMode;
  // "off" silences spoken replies; any other value means on (legacy persona ids
  // from the retired YarnGPT voice map to plain "on").
  window.__vcCovaVoice = t.covaVoice === "off" ? "off" : "on";

  // one-time migration to preferred brand defaults (vault mark + blend hero)
  useEffect(() => {
    try {
      if (!localStorage.getItem("vc_pref_v3")) {
        setTweak({ logoVariant: "vault", heroVariant: "blend", vaultLayout: "cards", chatStyle: "glass" });
        localStorage.setItem("vc_pref_v3", "1");
      }
    } catch (e) {}
  }, []);

  // Apply the ops-managed appearance (set in admin → Appearance). Brand look is
  // applied on every load; the default theme applies ONLY when the user hasn't
  // made their own light/dark choice yet — once they have, their choice wins and
  // persists across sessions (loadUserTheme / setDark).
  useEffect(() => {
    api("/api/settings").then((d) => {
      // Company identity → global used by the footer and PDF documents.
      if (d && d.company) window.VC_COMPANY = d.company;
      // Bundle savings config → used by the insure flow to price bundles.
      if (d && d.bundle) window.VC_BUNDLE = d.bundle;
      // Capability catalog → gates plan-limited features (Cova Insight, rewards…).
      if (d && Array.isArray(d.capabilities)) { setCaps(d.capabilities); window.VC_CAPS = d.capabilities; }
      const ap = d && d.appearance;
      if (!ap) return;
      ["accent", "logoVariant", "heroScene", "chatStyle", "vaultLayout"].forEach((k) => { if (ap[k] != null) setTweak(k, ap[k]); });
      if (loadUserTheme() === null && ap.dark != null) setTweak("dark", !!ap.dark);
    });
  }, []);

  // Capability gating. A feature is available when it's enabled platform-wide AND
  // the current plan is in its tier list (ops sets both in /admin/capabilities).
  // Unknown ids fail OPEN so a config gap never hides a working feature.
  const TIER_ORDER = ["free", "individual", "pro", "business"];
  const capOK = useCallback((id) => {
    const list = caps || window.VC_CAPS || null;
    if (!list) return true; // catalog not loaded yet — don't flash a gate
    const c = list.find((x) => x.id === id);
    if (!c) return true;
    if (!c.enabled) return false;
    return Array.isArray(c.tiers) && c.tiers.includes(plan || "free");
  }, [caps, plan]);
  // Lowest plan that unlocks a capability (for the upgrade CTA).
  const capTier = useCallback((id) => {
    const list = caps || window.VC_CAPS || [];
    const c = list.find((x) => x.id === id);
    const tiers = (c && c.tiers) || [];
    return TIER_ORDER.find((t) => tiers.includes(t)) || "individual";
  }, [caps]);
  const goUpgrade = useCallback(() => { setSettingsOpenSec("billing"); setTab("settings"); }, []);
  // Which nav tabs are plan-gated for this user (drives the lock badge).
  const tabLocked = useCallback((id) => {
    const m = { insight: "cova_insights", rewards: "rewards" };
    return m[id] ? !capOK(m[id]) : false;
  }, [capOK]);
  // Leaving Settings clears the deep-link so a later normal visit opens Profile.
  useEffect(() => { if (tab !== "settings" && settingsOpenSec) setSettingsOpenSec(null); }, [tab]);

  // entrance-animation reveal watchdog: guarantee content is visible even if the
  // animation clock is throttled (backgrounded tab / automated viewers)
  useEffect(() => {
    const id = setTimeout(() => document.documentElement.classList.add("vc-ready"), 2500);
    return () => clearTimeout(id);
  }, []);

  // deliver the first message once the chat surface is live (guest chat or full app)
  useEffect(() => {
    if ((screen === "app" || screen === "guest") && pendingMsg.current) {
      const m = pendingMsg.current;
      pendingMsg.current = null;
      setTimeout(() => send(m), screen === "guest" ? 650 : 1600);
    }
  }, [screen]);

  const toast = useCallback((msg, icon = "check") => {
    const id = Math.random();
    setToasts((ts) => [...ts, { id, msg, icon }]);
    setTimeout(() => setToasts((ts) => ts.filter((x) => x.id !== id)), 2800);
  }, []);

  const flash = useCallback((id) => { setFlashId(id); setTimeout(() => setFlashId(null), 1800); }, []);

  // Apply a subscription plan. Paid upgrades run through the checkout first (see
  // the Payment onSuccess below) — this only lands once that completes; the free
  // tier applies directly since there's nothing to charge.
  const applyPlan = useCallback((code) => {
    setPlan(code);
    api("/api/plan", { code });
    toast("Welcome to " + (window.VC_DATA.PLANS.find((x) => x.id === code)?.name || code), "check");
  }, [toast]);

  // ---- chat engine ----
  const pushMsg = (m) => setMessages((ms) => [...ms, m]);

  const runActions = useCallback((actions) => {
    actions.forEach((act) => {
      if (act.type === "addAsset") {
        setAssets((prev) => [act.asset, ...prev]);
        if (act.flash) setTimeout(() => flash(act.asset.id), 200);
        const catFact = { vehicle: "vehicle", property: "property", equipment: "equipment", inventory: "inventory", marine: "transit", jewelry: "contents", electronics: "contents" }[act.asset.category];
        if (catFact) setGraph((g) => window.VC_DATA.graphLearn(g, catFact, 0.95, "VaultLog asset"));
        // persist to the real VaultLog, then reconcile the optimistic id -> DB id.
        // A 402 means the free asset cap is hit: revert the optimistic add and nudge to upgrade.
        fetch("/api/assets", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: act.asset.name, category: act.asset.category, replacement: act.asset.replacement, purchase: act.asset.purchase, brand: act.asset.brand, condition: act.asset.condition, location: act.asset.location, serial: act.asset.serial, place: act.asset.place }) })
          .then(async (r) => {
            if (r.status === 402) {
              const d = await r.json().catch(() => ({}));
              setAssets((prev) => prev.filter((a) => a.id !== act.asset.id));
              toast(d.error || "You've reached your Free Vault 35-asset limit. Upgrade to add more.", "alert");
              setTimeout(() => setTab("settings"), 900);
              return;
            }
            if (!r.ok) return;
            const real = await r.json();
            if (real && real.id) setAssets((prev) => prev.map((a) => a.id === act.asset.id ? { ...a, id: real.id } : a));
          }).catch(() => {});
      } else if (act.type === "navigate") {
        setTab(act.tab === "quotes" ? "insure" : act.tab);
      } else if (act.type === "navigateSoon") {
        setTimeout(() => { setTab(act.tab === "quotes" ? "insure" : act.tab); }, 1400);
      } else if (act.type === "downloadSchedule") {
        // Documents require an account — guests are nudged to sign up first.
        if (screen === "guest") { toast("Create your free account to download your schedule of assets.", "sparkle"); setTimeout(() => { setFromGuest(true); setAuthMode("signup"); setScreen("auth"); }, 900); return; }
        setTimeout(() => window.downloadSchedule(assets, profile, userName), 700);
      } else if (act.type === "addFlag") {
        setFlags((fs) => [{ id: "FLG-" + (120 + fs.length), user: profile === "business" ? "ABC Logistics Ltd." : "Adebola Okafor", time: "Just now", status: "open", ...act.flag }, ...fs]);
      } else if (act.type === "promptSignup") {
        // Cova decided the guest is done building their vault: open sign-up to save it.
        setTimeout(() => { setFromGuest(true); setAuthMode("signup"); setScreen("auth"); }, 1100);
      } else if (act.type === "promptLogin") {
        // The guest says they already have an account (e.g. they asked about
        // their totals/policies/claims): open log-in. Anything they built in
        // guest mode merges into their vault after they sign in.
        setTimeout(() => { setFromGuest(true); setAuthMode("login"); setScreen("auth"); }, 1100);
      } else if (act.type === "openMigrate") {
        setTimeout(() => setMigrateOpen(true), 900);
      } else if (act.type === "deleteAsset") {
        const a = matchAssetByName(assets, act.name);
        if (!a) { toast(`I couldn't find "${act.name}" in your vault.`, "alert"); return; }
        if (isAssetBound(a)) { toast(`${a.name} is insured, so it can't be deleted.`, "alert"); return; }
        setAssets((prev) => prev.filter((x) => x.id !== a.id)); // optimistic
        fetch("/api/assets/" + a.id, { method: "DELETE" })
          .then((r) => r.json().then((res) => ({ ok: r.ok, res })))
          .then(({ ok, res }) => { if (!ok) { toast((res && res.error) || "Couldn't delete that.", "alert"); setAssets((prev) => [a, ...prev]); } })
          .catch(() => { toast("Couldn't reach the vault, try again.", "alert"); setAssets((prev) => [a, ...prev]); });
        toast(`Removed ${a.name} from your VaultLog`, "check");
      } else if (act.type === "renameAsset") {
        const a = matchAssetByName(assets, act.name);
        const newName = String(act.newName || "").trim();
        if (!a) { toast(`I couldn't find "${act.name}" in your vault.`, "alert"); return; }
        if (!newName) return;
        if (isAssetBound(a)) { toast(`${a.name} is insured, so its name is locked.`, "alert"); return; }
        const prevName = a.name;
        setAssets((prev) => prev.map((x) => (x.id === a.id ? { ...x, name: newName } : x))); // optimistic
        fetch("/api/assets/" + a.id, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newName }) })
          .then((r) => r.json().then((res) => ({ ok: r.ok, res })))
          .then(({ ok, res }) => { if (!ok) { toast((res && res.error) || "Couldn't rename that.", "alert"); setAssets((prev) => prev.map((x) => (x.id === a.id ? { ...x, name: prevName } : x))); } })
          .catch(() => { setAssets((prev) => prev.map((x) => (x.id === a.id ? { ...x, name: prevName } : x))); });
        toast(`Renamed to ${newName}`, "check");
      } else if (act.type === "groupAssets") {
        // Assign a named place to EXISTING assets (never re-adds — no duplicates).
        const place = String(act.place || "").trim();
        if (!place) return;
        let targets = [];
        if (act.scope === "category" && act.category) targets = assets.filter((a) => a.category === act.category);
        else if (act.scope === "named" && Array.isArray(act.names) && act.names.length) {
          const seen = new Set();
          act.names.forEach((n) => { const m = matchAssetByName(assets, n); if (m && !seen.has(m.id)) { seen.add(m.id); targets.push(m); } });
        } else targets = assets.slice(); // "all"
        if (!targets.length) { toast("I couldn't find those assets to group.", "alert"); return; }
        const ids = new Set(targets.map((a) => a.id));
        setAssets((prev) => prev.map((a) => (ids.has(a.id) ? { ...a, place } : a))); // optimistic
        targets.forEach((a) => { fetch("/api/assets/" + a.id, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ place }) }).catch(() => {}); });
        toast(`Grouped ${targets.length} ${targets.length > 1 ? "assets" : "asset"} under ${place}`, "layers");
      } else if (act.type === "setBusinessName") {
        const bn = String(act.name || "").trim();
        if (!bn) return;
        setBusinessName(bn);
        fetch("/api/profile", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ businessName: bn }) }).catch(() => {});
        toast(`Business name saved: ${bn}`, "check");
      }
    });
  }, [flash, assets, profile, userName, toast]);

  // Risk-discovery + gentle cross-sell, run on every message regardless of brain.
  const learnAndCrossSell = useCallback((text, baseDelay) => {
    const D = window.VC_DATA;
    const signals = D.detectRiskSignals(text);
    let g2 = graph;
    signals.forEach((s) => { g2 = D.graphLearn(g2, s.fact, s.conf, s.source, s.value); });
    if (g2 !== graph) setGraph(g2);
    if (!signals.length) return;
    // Keep the risk graph learning for everyone, but never push a product
    // recommendation or upgrade nudge to a guest — they're still building their
    // vault and haven't even signed up. Those only surface inside the real app.
    if (screen !== "app") return;
    const recs = D.crossSell(g2, assets).filter((r) => !shownRecs.current[r.id]);
    if (recs.length) {
      const rec = recs[0];
      shownRecs.current[rec.id] = true;
      setTimeout(() => pushMsg({ role: "assistant", text: rec.reason, card: { type: "crosssell", rec }, chips: ["Explore protection options", "Not now"] }), baseDelay + 1400);
    }
    const up = D.upgradeSignal(plan, profile, assets, g2);
    if (up && !nudgedUpgrade.current && !recs.length) {
      nudgedUpgrade.current = true;
      setTimeout(() => pushMsg({ role: "assistant", text: up.reason + " No pressure - everything you already use stays free.", chips: ["See plans", "Maybe later"] }), baseDelay + 1400);
    }
  }, [assets, profile, graph, plan, screen]);

  // The proven deterministic engine - offline / no-key fallback. Assumes the user
  // message is already pushed and busy is set. Guarded so a parse error can never
  // leave the chat stuck "typing".
  const localBrain = useCallback((text) => {
    let result;
    try { result = window.interpret(text, { assets, profile, graph, panel }); }
    catch (e) {
      console.error("interpret failed", e);
      pushMsg({ role: "assistant", text: "Sorry, I didn't quite catch that. Could you put it another way?" });
      setBusy(false);
      return;
    }
    const { msgs = [], actions = [] } = result || {};
    let delay = 0;
    msgs.forEach((m, i) => {
      delay += m.delay || 650;
      setTimeout(() => { pushMsg({ role: "assistant", ...m }); if (i === msgs.length - 1) setBusy(false); }, delay);
    });
    if (!msgs.length) setBusy(false);
    runActions(actions);
    learnAndCrossSell(text, delay);
  }, [assets, profile, graph, runActions, learnAndCrossSell]);

  const send = useCallback((text) => {
    if (typeof text !== "string" || !text.trim()) return;
    // The "Import from a file" chip opens the picker directly — importing is an
    // action, not a conversation turn (the composer's paperclip listens for this).
    if (/^import (from )?a file$/i.test(text.trim())) { window.dispatchEvent(new CustomEvent("vc-open-import")); return; }
    pushMsg({ role: "user", text });
    setBusy(true);
    // Real understanding via Claude when ANTHROPIC_API_KEY is set; the proven
    // engine otherwise. Either way Cova performs the user's task, and it can
    // NEVER stall: if Claude is slow/unreachable we fall back after a timeout.
    let settled = false;
    const fallback = () => { if (settled) return; settled = true; localBrain(text); };
    const timer = setTimeout(fallback, 12000);
    // Send the recent transcript so Cova remembers the conversation (not just the
    // latest line). Only role + text; the server normalises it for Claude.
    const history = messagesRef.current
      .filter((m) => (m.role === "user" || m.role === "assistant") && m.text)
      .slice(-12)
      .map((m) => ({ role: m.role, text: m.text }));
    api("/api/cova", { message: text, history, context: { assets: assets.map((a) => ({ name: a.name, category: a.category, replacement: a.replacement, status: a.status, sumInsured: a.sumInsured || 0, place: a.place || "" })), profile, userName, guest: screen === "guest", plan: screen === "guest" ? "" : plan } })
      .then((res) => {
        if (settled) return;
        clearTimeout(timer);
        if (!res || res.fallback) { settled = true; localBrain(text); return; }
        settled = true;
        runActions(res.actions || []);
        pushMsg({ role: "assistant", text: res.reply || "…", chips: res.chips });
        learnAndCrossSell(text, 0);
        setBusy(false);
      })
      .catch(() => { clearTimeout(timer); fallback(); });
  }, [assets, profile, runActions, localBrain, learnAndCrossSell]);

  // File import (paperclip / "Import from a file" chip): the server parses the
  // document into structured assets; every one is added straight to the vault
  // (no row-by-row review). Plan caps are respected up front so the summary is
  // honest about anything that didn't fit.
  const importFile = useCallback((file) => {
    if (!file) return;
    if (file.size > 8 * 1024 * 1024) { toast("That file is over 8MB. Export a smaller version and try again.", "alert"); return; }
    pushMsg({ role: "user", text: `📎 ${file.name}` });
    setBusy(true);
    const reader = new FileReader();
    reader.onerror = () => { setBusy(false); pushMsg({ role: "assistant", text: "I couldn't read that file from your device. Try attaching it again." }); };
    reader.onload = () => {
      const b64 = String(reader.result || "").split(",")[1] || "";
      fetch("/api/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: file.name, mime: file.type, data: b64 }) })
        .then((r) => r.json().then((d) => ({ ok: r.ok, d })).catch(() => ({ ok: false, d: {} })))
        .then(({ ok, d }) => {
          setBusy(false);
          const found = ok && Array.isArray(d.assets) ? d.assets : [];
          if (!found.length) {
            pushMsg({ role: "assistant", text: (d && d.error) || `I read ${file.name} but couldn't find any assets in it. If it's a scan or photo-heavy, try the original document, or just paste the list right here.` });
            return;
          }
          const D = window.VC_DATA;
          const capMap = { free: 35, individual: 80, pro: 150, business: Infinity };
          const cap = capMap[plan] ?? 35;
          const room = Math.max(0, cap - assets.length);
          const toAdd = found.slice(0, room);
          runActions(toAdd.map((a, i) => ({
            type: "addAsset", flash: i === 0,
            asset: {
              id: "VA-" + Math.random().toString(36).slice(2, 8),
              name: a.name, category: a.category,
              purchase: a.purchase || a.replacement, replacement: a.replacement,
              sumInsured: 0, status: "uninsured", insurer: null, premium: 0, renews: null, bundle: null, note: "",
              brand: a.brand || "", condition: a.condition || "", location: a.location || "", serial: a.serial || "", place: a.place || "",
            },
          })));
          const total = toAdd.reduce((sum, a) => sum + (a.replacement || 0), 0);
          let text = `All done. I read **${file.name}** and added **${toAdd.length} asset${toAdd.length > 1 ? "s" : ""} worth ${D.fmtNaira(total)}** to your VaultLog.`;
          if (found.length > toAdd.length) text += ` Your plan had ${room === 0 ? "no" : `only ${room}`} asset slot${room === 1 ? "" : "s"} left, so ${found.length - toAdd.length} item${found.length - toAdd.length > 1 ? "s" : ""} couldn't fit. Upgrading in Settings raises the cap.`;
          pushMsg({ role: "assistant", text, chips: toAdd.length ? ["Open my VaultLog", "Group them into a place", "What's my risk score now?"] : ["See plans"] });
        })
        .catch(() => { setBusy(false); pushMsg({ role: "assistant", text: "I couldn't reach the vault to import that. Check your connection and try again." }); });
    };
    reader.readAsDataURL(file);
  }, [assets, plan, runActions]);

  const onAction = useCallback((act) => {
    if (act.type === "selectQuote") {
      toast(`Preparing your policy with ${act.quote.insurer}…`, "shield");
      setTimeout(() => setTab("insure"), 600);
    }
  }, [toast]);

  // open chat from anywhere
  const onChat = useCallback((text) => {
    setDockOpen(true); // deliberate: user asked for Cova, open the sheet even on mobile
    setTimeout(() => send(text), 250);
  }, [send]);

  // ---- guest-first flow: chat with Cova before signing up ----
  // Assets the visitor describes land in the SAME `assets` state the app reads, so when
  // they save, their vault is already built - nothing to migrate.
  const enterGuest = (msg) => {
    const prof = inferProfile(msg);
    setProfile(prof);
    setAssets([]);
    setClaims([]);
    setGraph(window.VC_DATA.emptyGraph());
    setPlan(prof === "business" ? "business" : "free");
    shownRecs.current = {}; nudgedUpgrade.current = false;
    setMessages([{
      role: "assistant",
      text: `Hi 👋 I'm **Cova**. Tell me what you own - a car, your home, equipment, stock - and I'll value it, score the risk and start building your vault. No account needed yet.`,
      chips: prof === "business" ? ["Add my fleet of 3 trucks", "Insure my warehouse", "Move my existing policy"] : ["Add my car worth ₦52M", "Insure my laptop & phone", "Am I covered?"],
    }]);
    pendingMsg.current = (typeof msg === "string" && msg.trim()) ? msg.trim() : null;
    setScreen("guest");
  };

  // visitor commits - carry the guest vault + conversation straight into the account
  const saveGuestVault = (prof) => {
    setProfile(prof);
    setPlan(prof === "business" ? "business" : "free");
    // The account was just created by /api/auth/signup (cookie set). Persist the
    // assets built as a guest into it, then swap in their real DB ids.
    api("/api/session").then((s) => { if (s && s.name) setUserName(s.name); loadNotifs(); loadPanel(); });
    const guestAssets = assets.filter((a) => String(a.id).startsWith("VA-"));
    Promise.all(guestAssets.map((a) => api("/api/assets", { name: a.name, category: a.category, replacement: a.replacement, purchase: a.purchase || a.replacement, place: a.place, location: a.location, brand: a.brand, condition: a.condition })))
      .then(() => api("/api/bootstrap"))
      .then((d) => {
        if (d && Array.isArray(d.assets) && d.assets.length) setAssets(d.assets);
        if (d && d.user) { setUserName(d.user.name); setUserAvatar(d.user.avatar || ""); setUserAddress(d.user.address || ""); setBusinessName(d.user.businessName || ""); setKycDone(!!d.user.kycStatus); }
        if (d && d.plan) setPlan(d.plan);
      })
      .catch(() => {});
    saveSession({ authed: true, role: "customer", profile: prof });
    setClaims([]);
    setFromGuest(false);
    setScreen("app");
    setTab("dashboard");
    setDockOpen(!isMobile); // desktop: open the dock; mobile: never cover the app on entry
    setMessages((ms) => [...ms, {
      role: "assistant",
      text: `Your vault is saved ✓ Everything we just went through is here - and it follows you on every screen. Want me to find cover for what's exposed?`,
      chips: ["Insure what's exposed", "Am I fully covered?", "Open Cova Insight"],
    }]);
  };

  // ---- onboarding ----
  const startApp = (prof) => {
    setProfile(prof);
    setAssets([]);
    setClaims([]);
    setGraph(window.VC_DATA.emptyGraph());
    setPlan(prof === "business" ? "business" : "free");
    shownRecs.current = {}; nudgedUpgrade.current = false;
    setScreen("app");
    setTab("dashboard");
    setDockOpen(!isMobile); // desktop: open the dock; mobile: never cover the app on entry
    // Cova's opening line depends on where the user actually is: a fresh vault
    // gets the build-your-VaultLog invitation; a returning user with assets gets
    // a summary of their vault and next steps (add more, insights, close gaps).
    const greet = (fullName, vault) => {
      const first = (fullName || "").trim().split(/\s+/)[0] || fullName;
      if (Array.isArray(vault) && vault.length > 0) {
        const D = window.VC_DATA;
        const total = vault.reduce((s, a) => s + (a.replacement || 0), 0);
        const gaps = vault.filter((a) => !a.insurer).length;
        setMessages([{
          role: "assistant",
          text: `Welcome back, ${first}! 👋 Your vault is holding **${vault.length} asset${vault.length > 1 ? "s" : ""} worth ${D.fmtNaira(total)}**${gaps > 0 ? `, and ${gaps === vault.length ? (vault.length > 1 ? "all of them are" : "it's") : `${gaps} of them ${gaps > 1 ? "are" : "is"}`} still uninsured` : ", all covered"}. What shall we look at today?`,
          chips: gaps > 0
            ? ["Close my coverage gaps", "What's my risk score?", "Add another asset", "Import from a file"]
            : ["What's my risk score?", "Add another asset", "Import from a file"],
        }]);
      } else {
        setMessages([{
          role: "assistant",
          text: `Welcome to VaultCova, ${first}! 👋 I'm **Cova**, right here on every screen. Let's build your VaultLog - tell me what you own (a car, your home, equipment, stock) and I'll value it, score the risk and spot any coverage gaps.`,
        }, {
          role: "assistant",
          text: `Add things in plain language - or import a whole list from a PDF, Excel or Word file with the paperclip below - and whenever you're ready I'll help you get cover. What would you like to add first?`,
          chips: prof === "business" ? ["Add my delivery van", "Add my shop premises", "Import from a file"] : ["Add my car", "Add my laptop & phone", "Add my home"],
        }]);
      }
    };
    saveSession({ authed: true, role: "customer", profile: prof });
    // The session cookie was set by /api/auth/signup or /api/auth/login — read
    // the authenticated account, load ITS vault, then greet based on what's there.
    api("/api/session").then((s) => {
      if (!s || s.role !== "customer") { signOutRef.current("Please log in again."); return; }
      setUserName(s.name);
      loadNotifs(); loadPanel();
      api("/api/bootstrap").then((d) => {
        if (d && d.user && d.user.name) setUserName(d.user.name);
        if (d && d.user) { setUserAvatar(d.user.avatar || ""); setUserAddress(d.user.address || ""); setBusinessName(d.user.businessName || ""); setKycDone(!!d.user.kycStatus); }
        if (d && Array.isArray(d.assets)) setAssets(d.assets);
        if (d && d.plan) setPlan(d.plan);
        greet((d && d.user && d.user.name) || s.name, (d && d.assets) || []);
      }).catch(() => greet(s.name, []));
      api("/api/claims").then((d) => { if (d && Array.isArray(d.claims)) setClaims(d.claims); });
    });
  };

  // A file picked on the PUBLIC site (hero / page CTAs dispatch vc-guest-import):
  // enter guest mode if we're not in a chat surface yet, then import it — the
  // visitor lands in the guest chat watching their list become a vault.
  useEffect(() => {
    const h = (e) => {
      const f = e && e.detail;
      if (!f) return;
      if (screen !== "guest" && screen !== "app") enterGuest("");
      importFile(f);
    };
    window.addEventListener("vc-guest-import", h);
    return () => window.removeEventListener("vc-guest-import", h);
  });

  // Lets earlier-declared flows (startApp) trigger sign-out without ordering issues.
  const signOutRef = useRef(() => {});

  // Real sign-out: clear the persisted session + backend cookie, wipe in-memory
  // state, and return to the public landing. `reason` optionally explains why.
  const signOut = useCallback((reason) => {
    saveSession(null);
    fetch("/api/session", { method: "DELETE" }).catch(() => {});
    setAssets([]); setClaims([]); setMessages([]);
    setGraph(window.VC_DATA.emptyGraph());
    setNotifs({ unread: 0, items: [] });
    setProfile("business"); setPlan("free"); setTab("dashboard"); setDockOpen(!isMobile); // desktop: open the dock; mobile: never cover the app on entry
    shownRecs.current = {}; nudgedUpgrade.current = false; pendingMsg.current = null;
    setScreen("landing");
    toast(typeof reason === "string" ? reason : "You've been signed out", "logout");
  }, [toast]);
  signOutRef.current = signOut;

  // Restore a persisted session on first load (re-establish the account + vault).
  useEffect(() => {
    const s = restored.current;
    if (!s || !s.authed) return;
    const prof = s.profile || "individual";
    setProfile(prof);
    setPlan(prof === "business" ? "business" : "free");
    setDockOpen(!isMobile); // desktop: open the dock; mobile: never cover the app on entry
    setMessages([{ role: "assistant", text: `Welcome back to VaultCova 👋 Your vault is loaded and I'm right here, just talk to me.`, chips: ["Am I fully covered?", "What needs attention?", "Open Cova Insight"] }]);
    api("/api/session").then((sv) => {
      // Cookie expired or was cleared server-side: drop the stale local session.
      if (!sv || sv.role !== "customer") { saveSession(null); setScreen("landing"); return; }
      setUserName(sv.name);
      loadNotifs(); loadPanel();
      api("/api/bootstrap").then((d) => {
        if (d && d.user && d.user.name) setUserName(d.user.name);
        if (d && d.user) { setUserAvatar(d.user.avatar || ""); setUserAddress(d.user.address || ""); setBusinessName(d.user.businessName || ""); setKycDone(!!d.user.kycStatus); }
        if (d && Array.isArray(d.assets)) setAssets(d.assets);
        if (d && d.plan) setPlan(d.plan);
      });
      api("/api/claims").then((d) => { if (d && Array.isArray(d.claims)) setClaims(d.claims); });
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Auto sign-out after IDLE_MS of no interaction, while signed in.
  useEffect(() => {
    if (screen !== "app") return;
    let timer;
    const reset = () => { clearTimeout(timer); timer = setTimeout(() => signOut("Signed out after 5 minutes of inactivity"), IDLE_MS); };
    const events = ["mousemove", "mousedown", "keydown", "touchstart", "scroll", "click"];
    events.forEach((e) => window.addEventListener(e, reset, { passive: true }));
    reset();
    return () => { clearTimeout(timer); events.forEach((e) => window.removeEventListener(e, reset)); };
  }, [screen, signOut]);

  // ---- insure flow done: bind through the REAL commission engine + ledger ----
  const applyInsured = (ids, quote, mode, serverAssets) => {
    setAssets((prev) => {
      const next = prev.map((a) => {
        if (!ids.includes(a.id)) return a;
        const s = serverAssets && serverAssets.find((x) => x.id === a.id);
        // server is the source of truth for the money; fall back to local if offline
        return s ? { ...a, ...s, bundle: mode === "bundle" ? (profile === "business" ? "fleet" : "household") : a.bundle }
          : { ...a, sumInsured: a.replacement, status: "insured", insurer: quote.insurer, premium: Math.round(quote.premium / ids.length), renews: "2027-06-10" };
      });
      setGraph((g) => {
        let g2 = g;
        const catFact = { vehicle: "vehicle", property: "property", equipment: "equipment", inventory: "inventory", marine: "transit", jewelry: "contents", electronics: "contents" };
        next.filter((a) => ids.includes(a.id)).forEach((a) => { const f = catFact[a.category]; if (f) g2 = window.VC_DATA.graphLearn(g2, f, 1.0, "Policy purchased"); });
        return g2;
      });
      return next;
    });
    setInsurePreselect(null);
    toast(`${ids.length} asset${ids.length > 1 ? "s" : ""} now insured with ${quote.insurer}`, "shield");
    setTab("vaultlog");
  };
  const onInsureDone = (ids, quote, mode) => {
    api("/api/bind", { assetIds: ids, insurer: quote.insurer, premium: quote.premium, mode })
      .then((res) => applyInsured(ids, quote, mode, res && res.assets));
  };

  // File a claim for REAL: Cova (Claude) triages it server-side — small clean
  // losses auto-settle, the rest lands on the ops claims desk. The optimistic
  // local claim is reconciled with the server's verdict + id when it returns.
  const onFileClaim = (claim) => {
    // No cover, no claim. Uninsured assets never open a claim — tell the
    // customer plainly and steer them to insuring the asset instead. This
    // mirrors the backend guard in /api/claims (defense in depth).
    const claimAsset = assets.find((a) => a.id === claim.assetId);
    const insured = !!claim.insurer || isAssetBound(claimAsset);
    if (!insured) {
      toast(`${claim.asset} isn't insured - there's no cover to claim. Let's protect it first.`, "shield");
      if (claimAsset) { setInsurePreselect(claimAsset); setTab("insure"); }
      return;
    }
    const localVerdict = window.VC_DATA.assessClaim(claim, insured);
    setClaims((prev) => [claim, ...prev]);
    setGraph((g) => window.VC_DATA.graphLearn(g, "interruption", 0.7, "Claim filed"));
    if (localVerdict.decision === "flag") toast(`Claim filed - routed to our team for review`, "flag");
    else toast(`Claim auto-approved by Cova - settlement queued`, "check");
    setTab("claims");
    api("/api/claims", { assetId: claim.assetId, type: claim.type, loss: claim.loss, note: claim.note })
      .then((res) => {
        if (!res || !res.claim) return; // offline -> keep the optimistic claim
        setClaims((prev) => prev.map((c) => (c.id === claim.id ? { ...c, ...res.claim, isNew: true } : c)));
        loadNotifs(); loadPanel();
      });
  };

  // =================== RENDER ===================
  if (screen === "landing") {
    return (
      <Root accent={t.accent} dark={t.dark}>
        <window.Landing tweaks={t} onStart={(msg) => { (typeof msg === "string" && msg.trim()) ? enterGuest(msg) : (setAuthMode("signup"), setScreen("auth")); }} onLogin={() => { setFromGuest(false); setAuthMode("login"); setScreen("auth"); }} onToggleTheme={() => setDark(!t.dark)} isDark={t.dark} />
      </Root>
    );
  }
  if (screen === "guest") {
    return (
      <Root accent={t.accent} dark={t.dark}>
        <window.GuestChat messages={messages} onSend={send} onAction={onAction} busy={busy} assets={assets} profile={profile} logoV={t.logoVariant} onImport={importFile}
          onSave={() => { setFromGuest(true); setAuthMode("signup"); setScreen("auth"); }} onExit={() => setScreen("landing")} voiceMode={t.voiceMode} />
      </Root>
    );
  }
  if (screen === "auth") {
    return (
      <Root accent={t.accent} dark={t.dark}>
        <window.Auth tweaks={t} onComplete={(prof, mode, account) => {
          resetTok.current = null; // a redeemed (or abandoned) reset link never re-renders the reset form
          // `account` is the authenticated user from /api/auth/* — its type is
          // authoritative (a login ignores whichever profile card was tapped).
          const type = (account && account.type) || prof;
          if (account && account.name) setUserName(account.name);
          if (fromGuest) { saveGuestVault(type); }
          else if (mode === "login") { startApp(type); } // returning user: straight into their vault
          else { setProfile(type); setScreen("onboarding"); } // new signup: meet Cova
        }} onBack={() => { resetTok.current = null; setScreen(fromGuest ? "guest" : "landing"); }} isDark={t.dark} onToggleTheme={() => setDark(!t.dark)} fromGuest={fromGuest} guestCount={assets.length} guestValue={assets.reduce((s, a) => s + (a.replacement || 0), 0)} guestProfile={profile} initialMode={authMode} resetToken={resetTok.current} />
      </Root>
    );
  }
  if (screen === "onboarding") {
    return (
      <Root accent={t.accent} dark={t.dark}>
        <window.Onboarding profile={profile} userName={userName} logoV={t.logoVariant} onComplete={startApp} onBack={() => setScreen("landing")} lang={lang} setLang={changeLang} langs={LANGS} />
      </Root>
    );
  }

  return (
    <Root accent={t.accent} dark={t.dark}>
      <div style={{ display: "flex", height: "100%", background: "var(--bg)" }}>
        {!isMobile && <RailNav tab={tab} setTab={setTab} profile={profile} logoV={t.logoVariant} userName={userName} userAvatar={userAvatar} isDark={t.dark} onToggleTheme={() => setDark(!t.dark)} onLogout={() => signOut()} flagCount={flags.filter((f) => f.status === "open").length} tabLocked={tabLocked} />}
        <div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", height: "100%" }}>
          <TopBar tab={tab} profile={profile} userName={userName} dockOpen={dockOpen} onToggleDock={() => setDockOpen(!dockOpen)} notifs={notifs} onMarkRead={markNotifsRead} onOpenAsset={(id) => { setOpenAssetId(id); setTab("vaultlog"); }} />
          <main className="vc-main" style={{ flex: 1, overflowY: "auto", position: "relative" }}>
            {tab === "dashboard" && <window.Dashboard assets={assets} claims={claims} profile={profile} userName={userName} onChat={onChat} onNav={setTab} />}
            {tab === "vaultlog" && <window.VaultLog assets={assets} profile={profile} userName={userName} tweaks={t} flashId={flashId} onChat={onChat} onInsure={(a) => { setInsurePreselect(a); setTab("insure"); }} onBundle={(a) => { setInsurePreselect(a); setTab("insure"); }} addToast={toast} onUpdate={onUpdateAsset} onDelete={onDeleteAsset} onLayout={(v) => setTweak("vaultLayout", v)} openAssetId={openAssetId} onAssetOpened={() => setOpenAssetId(null)} plan={plan} statementsOK={capOK("statements")} onUpgrade={goUpgrade} />}
            {tab === "insure" && (kycDone || !window.KycGate
              ? <window.Insure assets={assets} profile={profile} userName={userName} preselect={insurePreselect} tweaks={t} addToast={toast} onChat={onChat} onDone={onInsureDone} panel={panel} />
              : <window.KycGate profile={profile} userName={userName}
                  reason="One quick step before you buy cover: we need to verify who you are. It only takes a minute, and your details save straight to your profile."
                  onCancel={() => setTab("dashboard")}
                  onDone={({ address, status }) => { setUserAddress(address || ""); setKycDone(true); toast(status === "verified" ? "You're verified ✓ let's find you cover" : "Details saved, verification pending", status === "verified" ? "check" : "shield"); }} />)}
            {tab === "claims" && <window.Claims claims={claims} assets={assets} onFileClaim={onFileClaim} onChat={onChat} onInsure={(a) => { setInsurePreselect(a); setTab("insure"); }} />}
            {tab === "insight" && (capOK("cova_insights")
              ? <window.Insight assets={assets} claims={claims} profile={profile} onChat={onChat} onNav={setTab} />
              : <window.UpgradeGate title="Cova Insight & risk score" description="Your portfolio risk score, exposure breakdown and Cova's prioritised recommendations. Upgrade to unlock the full risk picture across everything you own." requiredTier={capTier("cova_insights")} onUpgrade={goUpgrade} />)}
            {tab === "rewards" && (capOK("rewards")
              ? <window.Rewards assets={assets} profile={profile} userName={userName} onChat={onChat} addToast={toast} claims={claims} />
              : <window.UpgradeGate title="VaultRewards cashback" description="Earn back a share of your premium for every claim-free year, accrued monthly. Upgrade to start building your rewards." requiredTier={capTier("rewards")} onUpgrade={goUpgrade} />)}
            {tab === "settings" && <window.Settings profile={profile} userName={userName} t={t} setTweak={setTweak} setDark={setDark} openSec={settingsOpenSec} assets={assets} addToast={toast} onPay={(amount, label, planCode) => setPayInfo({ amount, label, plan: planCode })} lang={lang} setLang={changeLang} langs={LANGS} plan={plan} setPlan={applyPlan} graph={graph} onProfileSaved={(d) => { if (d && d.name) setUserName(d.name); if (d && d.avatar !== undefined) setUserAvatar(d.avatar || ""); }} />}
          </main>
        </div>

        {/* Persistent Cova dock (desktop side panel / mobile full sheet) */}
        {!isMobile && <CovaDock open={dockOpen} setOpen={setDockOpen} messages={messages} onSend={send} onAction={onAction} busy={busy} chatStyle={t.chatStyle} onImport={importFile} />}
      </div>

      {isMobile && <MobileTabBar tab={tab} setTab={setTab} onCova={() => setDockOpen(true)} onLogout={() => signOut()} isDark={t.dark} onToggleTheme={() => setDark(!t.dark)} profile={profile} flagCount={flags.filter((f) => f.status === "open").length} tabLocked={tabLocked} />}
      {isMobile && dockOpen && <MobileCovaSheet onClose={() => setDockOpen(false)} messages={messages} onSend={send} onAction={onAction} busy={busy} chatStyle={t.chatStyle} onImport={importFile} />}

      <window.ToastHost toasts={toasts} />
      <window.MigrateWizard open={migrateOpen} profile={profile} onClose={() => setMigrateOpen(false)} onDone={(asset) => {
        setMigrateOpen(false);
        setAssets((prev) => [asset, ...prev]);
        setGraph((g) => {
          const catFact = { vehicle: "vehicle", property: "property", equipment: "equipment", inventory: "inventory", marine: "transit", jewelry: "contents", electronics: "contents" }[asset.category];
          return catFact ? window.VC_DATA.graphLearn(g, catFact, 1.0, "Policy migrated") : g;
        });
        flash(asset.id);
        toast(`${asset.insurer} policy now managed in VaultCova`, "shield");
        setTab("vaultlog");
      }} />
      <window.Payment open={!!payInfo} amount={payInfo?.amount || 0} label={payInfo?.label || ""} onClose={() => setPayInfo(null)} onSuccess={() => { const info = payInfo; setPayInfo(null); if (info && info.plan) applyPlan(info.plan); else toast("Payment successful - receipt sent", "check"); }} />
    </Root>
  );
}

function Root({ children, accent, dark }) {
  const tr = window.makeT(__vcLang);
  window.__t = tr;
  return (
    <window.LangContext.Provider value={{ lang: __vcLang, t: tr }}>
      <div className="theme-anim" style={{ height: "100%", ...accentVars(accent, dark) }}>{children}</div>
    </window.LangContext.Provider>
  );
}

/* ---------------- Compact rail nav ---------------- */
function RailNav({ tab, setTab, profile, logoV, userName, userAvatar, isDark, onToggleTheme, onLogout, onAdmin, flagCount, tabLocked }) {
  const { t } = window.useT();
  const RailBtn = ({ id, icon, label, onClick, active, badge, locked }) => (
    <button onClick={onClick} title={locked ? label + " (upgrade to unlock)" : label} style={{
      position: "relative", display: "flex", flexDirection: "column", alignItems: "center", gap: 4, width: "100%",
      padding: "10px 4px", borderRadius: 13, transition: "all .2s var(--ease)",
      background: active ? "var(--accent-soft)" : "transparent",
      color: active ? "var(--accent-ink)" : "var(--text-faint)",
    }} onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = "var(--surface-3)"; }} onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = "transparent"; }}>
      <window.Icon name={icon} size={21} />
      <span style={{ fontSize: 9.5, fontWeight: 700, letterSpacing: ".01em" }}>{label}</span>
      {!!badge && <span style={{ position: "absolute", top: 5, right: 10, minWidth: 15, height: 15, padding: "0 4px", borderRadius: 999, background: "var(--danger)", color: "#fff", fontSize: 9, fontWeight: 700, display: "grid", placeItems: "center" }}>{badge}</span>}
      {locked && <span style={{ position: "absolute", top: 6, right: 12, display: "grid", placeItems: "center", width: 15, height: 15, borderRadius: 999, background: "var(--surface)", color: "var(--text-faint)", boxShadow: "var(--shadow-sm)" }}><window.Icon name="lock" size={9} stroke={2.4} /></span>}
    </button>
  );
  return (
    <aside style={{ width: 86, flexShrink: 0, height: "100%", display: "flex", flexDirection: "column", alignItems: "center", background: "var(--surface)", borderRight: "1px solid var(--border)", padding: "16px 10px" }}>
      <div title="VaultCova" style={{ marginBottom: 14 }}><window.LogoMark variant={logoV} size={38} /></div>
      <nav style={{ display: "grid", gap: 3, width: "100%", flex: 1 }}>
        {NAV.map((n) => <RailBtn key={n.id} id={n.id} icon={n.icon} label={t(n.tkey)} active={tab === n.id} onClick={() => setTab(n.id)} locked={tabLocked && tabLocked(n.id)} />)}
      </nav>
      <div style={{ display: "grid", gap: 3, width: "100%", marginBottom: 10 }}>
        <RailBtn id="settings" icon="cog" label={t("nav.settings")} active={tab === "settings"} onClick={() => setTab("settings")} />
      </div>
      <span title={userName} style={{ display: "block", marginBottom: 8 }}><window.Avatar avatar={userAvatar} name={userName} size={38} radius={12} /></span>
      <div style={{ display: "flex", gap: 4 }}>
        <button onClick={onToggleTheme} title="Theme" className="vc-focus" style={{ display: "grid", placeItems: "center", width: 30, height: 30, borderRadius: 9, color: "var(--text-faint)" }}><window.Icon name={isDark ? "sun" : "moon"} size={15} /></button>
        <button onClick={onLogout} title="Log out" className="vc-focus" style={{ display: "grid", placeItems: "center", width: 30, height: 30, borderRadius: 9, color: "var(--text-faint)" }}><window.Icon name="logout" size={15} /></button>
      </div>
    </aside>
  );
}

/* ---------------- Top bar ---------------- */
function TopBar({ tab, profile, userName, dockOpen, onToggleDock, notifs, onMarkRead, onOpenAsset }) {
  const { t } = window.useT();
  const [open, setOpen] = useState(false);
  const bellRef = useRef(null); // anchor for the portalled panel
  const isMobile = useIsMobile();
  const items = (notifs && notifs.items) || [];
  const unread = (notifs && notifs.unread) || 0;
  const KIND_ICON = { info_request: "doc", inspection: "eye", quote_ready: "shield", declined: "alert", general: "bell" };
  const toggle = () => { const next = !open; setOpen(next); if (next && unread > 0) onMarkRead(); };
  return (
    <header className="vc-topbar" style={{ flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, padding: "12px 24px", borderBottom: "1px solid var(--border)", background: "var(--glass)", backdropFilter: "blur(12px)" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0, flex: "0 1 auto" }}>
        <h2 className="display" style={{ fontSize: 17, flexShrink: 0 }}>{t("title." + tab)}</h2>
        {/* Business shows the account name here, which can be long — truncate so it never widens the row on mobile. */}
        <span className="vc-topbar-badge" style={{ fontSize: 11.5, fontWeight: 700, padding: "3px 10px", borderRadius: 999, background: "var(--surface-3)", color: "var(--text-muted)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 }}>{profile === "business" ? userName : t("topbar.personalVault")}</span>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
        <div style={{ position: "relative" }} ref={bellRef}>
          <button onClick={toggle} className="vc-focus" title="Notifications" style={{ position: "relative", display: "grid", placeItems: "center", width: 38, height: 38, borderRadius: 999, color: "var(--text-muted)", border: "1px solid var(--border)" }}>
            <window.Icon name="bell" size={17} />
            {unread > 0 && <span style={{ position: "absolute", top: 5, right: 5, minWidth: 15, height: 15, padding: "0 3px", display: "grid", placeItems: "center", borderRadius: 999, background: "var(--danger)", color: "#fff", fontSize: 9, fontWeight: 700, border: "2px solid var(--surface)" }}>{unread}</span>}
          </button>
          {open && (() => {
            const panelBody = (
              <>
                <div style={{ padding: "13px 16px", borderBottom: "1px solid var(--border)", fontWeight: 700, fontSize: 14 }}>Notifications</div>
                {items.length === 0 && <div style={{ padding: 24, textAlign: "center", fontSize: 13, color: "var(--text-faint)" }}>You're all caught up.</div>}
                {items.map((n) => (
                  <div key={n.id} onClick={() => { if (n.assetId && onOpenAsset) { onOpenAsset(n.assetId); setOpen(false); } }}
                    style={{ display: "flex", gap: 11, padding: "13px 16px", borderBottom: "1px solid var(--border)", background: n.read ? "transparent" : "var(--accent-soft)", cursor: n.assetId ? "pointer" : "default" }}>
                    <span style={{ flexShrink: 0, display: "grid", placeItems: "center", width: 32, height: 32, borderRadius: 10, background: "var(--surface-3)", color: "var(--accent)" }}><window.Icon name={KIND_ICON[n.kind] || "bell"} size={16} /></span>
                    <div style={{ minWidth: 0 }}>
                      <div style={{ fontWeight: 700, fontSize: 13 }}>{n.title}</div>
                      <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 2, lineHeight: 1.45 }}>{n.body}</div>
                      {n.assetId && <div style={{ fontSize: 11.5, fontWeight: 700, color: "var(--accent)", marginTop: 4 }}>Open asset →</div>}
                    </div>
                  </div>
                ))}
              </>
            );
            if (isMobile) {
              // A transformed/backdrop-filtered ancestor (the topbar) traps a
              // fixed child in its own stacking context, so page content bled
              // THROUGH the panel. Portal the whole overlay to <body> so it
              // escapes that context and reliably sits above everything.
              return window.ReactDOM.createPortal(
                <div style={{ position: "fixed", inset: 0, zIndex: 1000 }}>
                  <div onClick={() => setOpen(false)} style={{ position: "absolute", inset: 0, background: "rgba(8,9,26,.5)", backdropFilter: "blur(4px)", WebkitBackdropFilter: "blur(4px)", animation: "vc-fade-in .2s var(--ease) both" }} />
                  <div style={{ position: "absolute", top: "calc(58px + env(safe-area-inset-top))", left: 12, right: 12, maxHeight: "70dvh", overflowY: "auto", background: "var(--surface)", borderRadius: 18, border: "1px solid var(--border)", boxShadow: "var(--shadow-xl)", animation: "vc-fade-up .25s var(--ease) both" }}>
                    {panelBody}
                  </div>
                </div>,
                document.body
              );
            }
            // Desktop: also portal to <body>. The topbar's backdrop-filter
            // creates a stacking context that trapped the old absolute panel, so
            // it slipped BEHIND the page content. Portalled + fixed, anchored
            // just under the bell, it always sits above everything.
            const r = bellRef.current ? bellRef.current.getBoundingClientRect() : null;
            const top = r ? Math.round(r.bottom + 8) : 58;
            const right = r ? Math.max(12, Math.round(window.innerWidth - r.right)) : 24;
            return window.ReactDOM.createPortal(
              <div style={{ position: "fixed", inset: 0, zIndex: 1000 }}>
                <div onClick={() => setOpen(false)} style={{ position: "absolute", inset: 0 }} />
                <div style={{ position: "absolute", top, right, width: 340, maxWidth: "calc(100vw - 24px)", maxHeight: 420, overflowY: "auto", background: "var(--surface)", borderRadius: 16, border: "1px solid var(--border)", boxShadow: "var(--shadow-xl)", animation: "vc-fade-up .2s var(--ease) both" }}>
                  {panelBody}
                </div>
              </div>,
              document.body
            );
          })()}
        </div>
        {!dockOpen && <window.Btn icon="sparkle" size="sm" onClick={onToggleDock}>Cova</window.Btn>}
      </div>
    </header>
  );
}

/* ---------------- Persistent Cova dock ---------------- */
function CovaDock({ open, setOpen, messages, onSend, onAction, busy, chatStyle, onImport }) {
  const { t } = window.useT();
  if (!open) {
    return (
      <div style={{ width: 54, flexShrink: 0, height: "100%", display: "flex", flexDirection: "column", alignItems: "center", background: "var(--surface)", borderLeft: "1px solid var(--border)", padding: "16px 0", gap: 10 }}>
        <button onClick={() => setOpen(true)} title="Open Cova" style={{ display: "grid", placeItems: "center", width: 38, height: 38, borderRadius: 12, background: "linear-gradient(135deg,var(--accent-2),var(--accent))", color: "#fff", boxShadow: "var(--shadow-glow)", animation: "vc-pulse-ring 2.6s infinite" }}>
          <window.Icon name="sparkle" size={19} />
        </button>
        <span style={{ writingMode: "vertical-rl", fontSize: 11, fontWeight: 700, color: "var(--text-faint)", letterSpacing: ".06em" }}>{t("dock.copilot")}</span>
      </div>
    );
  }
  return (
    <div style={{ width: 372, flexShrink: 0, height: "100%", display: "flex", flexDirection: "column", background: "var(--bg-tint)", borderLeft: "1px solid var(--border)" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "13px 16px", borderBottom: "1px solid var(--border)", background: "var(--glass)", backdropFilter: "blur(10px)", flexShrink: 0 }}>
        <span style={{ display: "grid", placeItems: "center", width: 32, height: 32, borderRadius: 999, background: "linear-gradient(135deg,var(--accent-2),var(--accent))", color: "#fff" }}><window.Icon name="sparkle" size={17} /></span>
        <div style={{ flex: 1 }}>
          <div style={{ fontWeight: 700, fontSize: 14 }}>Cova</div>
          <div style={{ fontSize: 10.5, color: "var(--success)", fontWeight: 700 }}>{t("dock.online")}</div>
        </div>
        <button onClick={() => setOpen(false)} title="Collapse" style={{ display: "grid", placeItems: "center", width: 32, height: 32, borderRadius: 999, color: "var(--text-muted)" }}><window.Icon name="chevR" size={17} /></button>
      </div>
      <div style={{ flex: 1, minHeight: 0 }}>
        <window.ChatPanel messages={messages} onSend={onSend} onAction={onAction} busy={busy} chatStyle={chatStyle} compact placeholder="Tell Cova anything…" onImport={onImport} />
      </div>
    </div>
  );
}

/* ---------------- Mobile: bottom tab bar + Cova sheet ---------------- */
function MobileTabBar({ tab, setTab, onCova, onAdmin, onLogout, isDark, onToggleTheme, profile, flagCount, tabLocked }) {
  const { t } = window.useT();
  const [more, setMore] = useState(false);
  const go = (id) => { setTab(id); setMore(false); };
  const Item = ({ id, icon, label, onClick, active, badge }) => (
    <button onClick={onClick} style={{ position: "relative", display: "flex", flexDirection: "column", alignItems: "center", gap: 3, padding: "8px 2px", borderRadius: 12, color: active ? "var(--accent)" : "var(--text-faint)", minHeight: 48 }}>
      <window.Icon name={icon} size={22} />
      <span style={{ fontSize: 10, fontWeight: 700 }}>{label}</span>
      {!!badge && <span style={{ position: "absolute", top: 4, right: "22%", minWidth: 15, height: 15, padding: "0 4px", borderRadius: 999, background: "var(--danger)", color: "#fff", fontSize: 9, fontWeight: 700, display: "grid", placeItems: "center" }}>{badge}</span>}
    </button>
  );
  return (
    <>
      {more && (
        <div onClick={() => setMore(false)} style={{ position: "fixed", inset: 0, zIndex: 280, background: "rgba(8,9,26,.45)", backdropFilter: "blur(4px)", animation: "vc-fade-in .25s var(--ease) both" }}>
          <div onClick={(e) => e.stopPropagation()} style={{ position: "absolute", bottom: 86, left: 12, right: 12, background: "var(--surface)", borderRadius: 22, border: "1px solid var(--border)", boxShadow: "var(--shadow-xl)", padding: 14, animation: "vc-fade-up .3s var(--ease) both" }}>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(2,1fr)", gap: 8 }}>
              {[["insight", "eye", t("title.insight")], ["rewards", "trend", t("nav.rewards")], ["insure", "shield", t("nav.insure")], ["settings", "cog", t("nav.settings")]].map(([id, ic, label]) => {
                const locked = tabLocked && tabLocked(id);
                return (
                <button key={id} onClick={() => go(id)} style={{ position: "relative", display: "flex", alignItems: "center", gap: 10, padding: "13px 14px", borderRadius: 14, background: tab === id ? "var(--accent-soft)" : "var(--surface-2)", color: tab === id ? "var(--accent-ink)" : "var(--text)", fontWeight: 700, fontSize: 13.5, minHeight: 48 }}><window.Icon name={ic} size={19} />{label}{locked && <window.Icon name="lock" size={13} stroke={2.3} style={{ marginLeft: "auto", color: "var(--text-faint)" }} />}</button>
                );
              })}
              <button onClick={onToggleTheme} style={{ display: "flex", alignItems: "center", gap: 10, padding: "13px 14px", borderRadius: 14, background: "var(--surface-2)", fontWeight: 700, fontSize: 13.5, minHeight: 48 }}><window.Icon name={isDark ? "sun" : "moon"} size={19} />{isDark ? t("mobile.lightMode") : t("mobile.darkMode")}</button>
            </div>
            <div style={{ marginTop: 8 }}>
              <button onClick={onLogout} style={{ width: "100%", display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8, padding: "13px 14px", borderRadius: 14, background: "var(--surface-2)", color: "var(--text-muted)", fontWeight: 700, fontSize: 13.5, minHeight: 48 }}><window.Icon name="logout" size={18} />Log out</button>
            </div>
          </div>
        </div>
      )}
      <nav style={{ position: "fixed", bottom: 0, left: 0, right: 0, zIndex: 290, display: "grid", gridTemplateColumns: "repeat(5,1fr)", alignItems: "end", background: "var(--glass)", backdropFilter: "blur(18px) saturate(160%)", WebkitBackdropFilter: "blur(18px) saturate(160%)", borderTop: "1px solid var(--border)", padding: "4px 6px calc(6px + env(safe-area-inset-bottom))" }}>
        <Item id="dashboard" icon="dashboard" label={t("mobile.home")} active={tab === "dashboard"} onClick={() => go("dashboard")} />
        <Item id="vaultlog" icon="vault" label={t("mobile.vault")} active={tab === "vaultlog"} onClick={() => go("vaultlog")} />
        <div style={{ display: "grid", placeItems: "center" }}>
          <button onClick={onCova} title={t("mobile.askCova")} style={{ width: 56, height: 56, marginTop: -26, borderRadius: 999, background: "linear-gradient(135deg,var(--accent-2),var(--accent))", color: "#fff", display: "grid", placeItems: "center", boxShadow: "var(--shadow-glow)", border: "4px solid var(--bg)" }}>
            <window.Icon name="sparkle" size={26} />
          </button>
        </div>
        <Item id="claims" icon="claim" label={t("mobile.claims")} active={tab === "claims"} onClick={() => go("claims")} />
        <Item id="more" icon="grid" label={t("mobile.more")} active={more || ["insight", "rewards", "insure", "settings"].includes(tab)} onClick={() => setMore(!more)} badge={flagCount} />
      </nav>
    </>
  );
}

function MobileCovaSheet({ onClose, messages, onSend, onAction, busy, chatStyle, onImport }) {
  const { t } = window.useT();
  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 400, display: "flex", flexDirection: "column", background: "var(--bg)", animation: "vc-fade-up .3s var(--ease) both" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 16px", borderBottom: "1px solid var(--border)", background: "var(--glass)", backdropFilter: "blur(10px)", flexShrink: 0 }}>
        <span style={{ display: "grid", placeItems: "center", width: 34, height: 34, borderRadius: 999, background: "linear-gradient(135deg,var(--accent-2),var(--accent))", color: "#fff" }}><window.Icon name="sparkle" size={18} /></span>
        <div style={{ flex: 1 }}>
          <div style={{ fontWeight: 700, fontSize: 15 }}>Cova</div>
          <div style={{ fontSize: 10.5, color: "var(--success)", fontWeight: 700 }}>{t("dock.online")}</div>
        </div>
        <button onClick={onClose} style={{ display: "grid", placeItems: "center", width: 44, height: 44, borderRadius: 999, color: "var(--text-muted)", background: "var(--surface-3)" }}><window.Icon name="chevD" size={20} /></button>
      </div>
      <div style={{ flex: 1, minHeight: 0 }}>
        <window.ChatPanel messages={messages} onSend={onSend} onAction={onAction} busy={busy} chatStyle={chatStyle} compact placeholder="Tell Cova anything…" onImport={onImport} />
      </div>
    </div>
  );
}

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