/* global React, ReactDOM */
const { useState, useEffect, useMemo, useRef, useCallback } = React;

// Lazy-load the 139KB founders-enriched.js bundle on first AI use so the
// initial mobile page parse stays light.
let __enrichedLoadPromise = null;
function loadEnrichedFounders() {
  if (typeof window === "undefined") return Promise.resolve();
  if (window.FOUNDERS_ENRICHED) return Promise.resolve();
  if (__enrichedLoadPromise) return __enrichedLoadPromise;
  __enrichedLoadPromise = new Promise((resolve) => {
    const s = document.createElement("script");
    s.src = "founders-enriched.js";
    s.async = true;
    s.onload = () => resolve();
    s.onerror = () => resolve(); // graceful degradation — AI just lacks prior/edu fields
    document.head.appendChild(s);
  });
  return __enrichedLoadPromise;
}

// ───────────────────── Prime VP Logo ─────────────────────
function PrimeLogo() {
  // Self-hosted official Prime VP wordmark (1371×441) — fetched from primevp.in.
  // Falls back to a stylized P-mark if the asset ever fails to load.
  const [failed, setFailed] = React.useState(false);
  if (failed) {
    return (
      <svg viewBox="0 0 32 32" width="100%" height="100%" aria-hidden="true">
        <rect width="32" height="32" rx="7" fill="#0E0E0E" />
        <path d="M9 23 L9 9 L16 9 C19.3 9 21.5 11 21.5 14 C21.5 17 19.3 19 16 19 L13 19 L13 23 Z M13 16 L15.6 16 C17 16 17.7 15.3 17.7 14 C17.7 12.7 17 12 15.6 12 L13 12 Z" fill="#FF5A1F" />
      </svg>);
  }
  return (
    <img
      src="/prime-logo.png"
      alt="Prime Venture Partners"
      onError={() => setFailed(true)}
      style={{ width: "100%", height: "100%", objectFit: "contain", display: "block" }} />);
}

// ───────────────────── helpers ─────────────────────
function initials(name) {
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0]).join("").toUpperCase();
}
function avatarColor(seed) {
  const colors = ["#FF5A1F", "#0E0E0E", "#7A4BFF", "#0EA672", "#E0457B", "#1F4FFF", "#FF8A00", "#5A2DEE"];
  let h = 0;
  for (let i = 0; i < seed.length; i++) h = h * 31 + seed.charCodeAt(i) >>> 0;
  return colors[h % colors.length];
}
function logoColor(seed) {
  const palette = ["#FF5A1F", "#0E0E0E", "#1F4FFF", "#0EA672", "#7A4BFF", "#E0457B", "#FF8A00"];
  let h = 0;
  for (let i = 0; i < seed.length; i++) h = h * 31 + seed.charCodeAt(i) >>> 0;
  return palette[h % palette.length];
}
function statusLabel(s) {
  return {
    Attending: "Attending",
    NotAttending: "Not attending",
    Active: "Active",
    Acquired: "Acquired",
    IPO: "Public",
    ShutDown: "Shut down",
  }[s] || s;
}

// Per-founder attending: simple yes/no. Badge shows "Attending" or "Not attending".
function attendingShort(a) {
  return a === "yes" ? "Attending" : "Not attending";
}

// Count attending founders for a company
function attendingSummary(c) {
  const attended = (c.founders || []).filter((f) => f.attending === "yes").length;
  const total = (c.founders || []).length;
  return { attended, total };
}
function formatFunding(m) {
  if (!m && m !== 0) return "—";
  if (m >= 1000) return `$${(m / 1000).toFixed(1)}B`;
  if (m >= 100) return `$${Math.round(m)}M`;
  if (m >= 10) return `$${Math.round(m)}M`;
  if (m >= 1) return `$${m.toFixed(1).replace(/\.0$/, '')}M`;
  return `$${Math.round(m * 1000)}K`;
}
function isRecentNews(news) {
  if (!news) return false;
  // Tighten "in the news" to specific 2026 month references — most companies
  // now have *some* 2025/2026 mention, so we filter to genuinely fresh items.
  return /\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+2026\b/i.test(news);
}

// ───────────────────── Avatar (LinkedIn photo with initial-monogram fallback) ─────────────────────
const PersonAvatar = React.memo(function PersonAvatar({ name, linkedin, className = "avatar", size }) {
  const [failed, setFailed] = useState(false);
  const styleColor = { background: avatarColor(name) };
  const useImg = linkedin && !failed;
  const sizeStyle = size ? { width: size, height: size } : null;
  if (useImg) {
    return (
      <span className={`${className} has-photo`} style={sizeStyle}>
        <img
          src={`/founders/${linkedin}.jpg`}
          alt={name}
          loading="lazy"
          onError={() => setFailed(true)}
        />
      </span>
    );
  }
  return (
    <span className={className} style={{ ...styleColor, ...(sizeStyle || {}) }}>
      {initials(name)}
    </span>
  );
});

// ───────────────────── Logo ─────────────────────
// Prefers the local /logos/<id>.{png|svg} override, falls back to Google's
// favicon service, then to a colored monogram.
const LOCAL_LOGOS = {
  aquaairx:"png", aurm:"png", basil:"png", boltearth:"png", digii:"png",
  dozee:"png", durlabhdarshan:"png", elchemy:"svg", empirical:"png", finagg:"png",
  freo:"svg", frinks:"png", gallabox:"png", hackerearth:"png", hiration:"png",
  hitwicket:"png", illumine:"png", inamo:"png", klaar:"png", knightfintech:"svg",
  kredx:"png", metafin:"png", mygate:"png", navadhan:"png", nivasa:"png",
  niyo:"png", oto:"svg", oyela:"png", planetspark:"png", poshn:"png",
  punch:"png", strainx:"png", styldod:"png", sunstone:"png", surveysparrow:"png",
  synup:"png", tranzact:"png", waggle:"png", wayground:"png", wheelseye:"png",
  winuall:"png", zuai:"png", zuper:"png",
};
const Logo = React.memo(function Logo({ company, size = "row" }) {
  const [failed, setFailed] = useState(false);
  const cls = size === "detail" ? "logo" : "row-logo";
  // 1. Local override (highest priority)
  const localExt = LOCAL_LOGOS[company.id];
  // 2. Favicon service (skip for LinkedIn-only domains so we don't get LinkedIn's logo)
  const isFallbackDomain = !company.domain || /linkedin\.com\b/i.test(company.domain);
  const url = localExt
    ? `/logos/${company.id}.${localExt}`
    : (isFallbackDomain ? null : `https://www.google.com/s2/favicons?domain=${company.domain}&sz=128`);

  if (failed || !url) {
    return (
      <div className={cls}>
        <div className="mono" style={{ background: logoColor(company.id) }}>
          {company.name[0]}
        </div>
      </div>);
  }
  return (
    <div className={cls}>
      <img
        src={url}
        alt={company.name}
        loading="lazy"
        onError={() => setFailed(true)} />
    </div>);

});

// ───────────────────── Chip multi-select ─────────────────────
function ChipMulti({ label, options, selected, onChange, counts, formatOption }) {
  const fmt = formatOption || ((x) => x);
  const [open, setOpen] = useState(false);
  const ref = useRef();

  // Click-outside dismiss. Since the menu is now portaled to document.body,
  // we explicitly preserve clicks INSIDE the menu (any `.chip-menu` element)
  // — otherwise the option's own click would never fire because we'd close
  // the sheet first.
  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => {
      // Click inside the menu — let it through (option toggle, Done, Clear)
      if (e.target.closest(".chip-menu")) return;
      // Click on the overlay — overlay's onClick handles closing
      if (e.target.closest(".sheet-overlay")) return;
      // Click on the chip itself — let it toggle
      if (ref.current && ref.current.contains(e.target)) return;
      setOpen(false);
    };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open]);

  // Body scroll-lock when filter sheet is open on mobile (CSS media query
  // governs whether it's a sheet — JS just locks scroll defensively).
  useEffect(() => {
    if (!open) return;
    const isMobile = window.matchMedia("(max-width: 760px)").matches;
    if (!isMobile) return;
    document.body.classList.add("no-scroll");
    return () => document.body.classList.remove("no-scroll");
  }, [open]);

  // Esc closes
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [open]);

  // Page scroll closes the desktop dropdown — its `position: fixed` would
  // detach from the chip otherwise (which moves with the sticky filter row).
  // Mobile uses a bottom-sheet so this doesn't apply there.
  useEffect(() => {
    if (!open) return;
    if (window.matchMedia("(max-width: 760px)").matches) return;
    const onScroll = (e) => {
      if (e.target.closest && e.target.closest(".chip-menu")) return;
      setOpen(false);
    };
    window.addEventListener("scroll", onScroll, { passive: true, capture: true });
    return () => window.removeEventListener("scroll", onScroll, { capture: true });
  }, [open]);

  const active = selected.length > 0;
  const toggle = (opt) => {
    if (selected.includes(opt)) onChange(selected.filter((s) => s !== opt));
    else onChange([...selected, opt]);
  };

  // Render the dropdown/sheet. On every viewport we portal it to document.body
  // so no ancestor with `position: sticky`, `transform`, `contain`, etc. can
  // trap a `position: fixed` sheet inside a smaller box (iOS Safari quirk).
  // CSS `@media (max-width: 760px)` decides whether it's a bottom sheet or a
  // floating dropdown, and JS measures the chip's position so the dropdown
  // appears next to it on desktop.
  const [chipRect, setChipRect] = React.useState(null);
  React.useEffect(() => {
    if (!open) return;
    const btn = ref.current && ref.current.querySelector("button.chip");
    if (btn) setChipRect(btn.getBoundingClientRect());
  }, [open]);

  const isMobile = typeof window !== "undefined" && window.matchMedia("(max-width: 760px)").matches;

  const desktopStyle = (chipRect && !isMobile) ? {
    position: "fixed",
    top: Math.round(chipRect.bottom + 6) + "px",
    left: Math.round(chipRect.left) + "px",
    bottom: "auto",
    right: "auto",
    width: "max-content",
    minWidth: "240px",
    maxWidth: "320px",
    maxHeight: "360px",
    borderRadius: "12px",
    zIndex: 50,
  } : undefined;

  const sheet = open ? (
    <>
      <div className="sheet-overlay" onClick={() => setOpen(false)}></div>
      <div className="chip-menu" style={desktopStyle}>
        <div className="sheet-head">
          <span className="sheet-title">{label}{active ? ` · ${selected.length}` : ""}</span>
          <div className="sheet-actions">
            {active && <button className="sheet-clear" onClick={() => onChange([])}>Clear</button>}
            <button className="sheet-done" onClick={() => setOpen(false)}>Done</button>
          </div>
        </div>
        <div className="chip-menu-list">
          {options.map((opt) =>
            <div key={opt} className={`opt ${selected.includes(opt) ? "selected" : ""}`} onClick={() => toggle(opt)}>
              <span className="opt-label">{fmt(opt)}</span>
              <span className="opt-right">
                {counts && <span className="ct">{counts[opt] || 0}</span>}
                <span className="opt-check" aria-hidden="true">
                  <svg width="14" height="14" viewBox="0 0 14 14"><path d="M3 7 L6 10 L11 4" stroke="currentColor" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
                </span>
              </span>
            </div>
          )}
        </div>
      </div>
    </>
  ) : null;

  return (
    <div className="chip-select" ref={ref}>
      <button className={`chip ${active ? "active" : ""}`} onClick={() => setOpen((o) => !o)}>
        {label}{active ? ` · ${selected.length}` : ""}
        {active ?
          <span className="clear" onClick={(e) => {e.stopPropagation();onChange([]);}}>×</span> :
          <svg className="caret" viewBox="0 0 10 10"><path d="M2 4 L5 7 L8 4" stroke="currentColor" fill="none" strokeWidth="1.4" strokeLinecap="round" /></svg>
        }
      </button>
      {sheet && ReactDOM.createPortal(sheet, document.body)}
      {/* legacy in-tree (disabled — portal approach handles all viewports) */}
      {false && open && (
        <>
          <div className="sheet-overlay" onClick={() => setOpen(false)}></div>
          <div className="chip-menu">
            <div className="sheet-head">
              <span className="sheet-title">{label}{active ? ` · ${selected.length}` : ""}</span>
              <div className="sheet-actions">
                {active && <button className="sheet-clear" onClick={() => onChange([])}>Clear</button>}
                <button className="sheet-done" onClick={() => setOpen(false)}>Done</button>
              </div>
            </div>
            <div className="chip-menu-list">
              {options.map((opt) =>
                <div key={opt} className={`opt ${selected.includes(opt) ? "selected" : ""}`} onClick={() => toggle(opt)}>
                  <span className="opt-label">{fmt(opt)}</span>
                  <span className="opt-right">
                    {counts && <span className="ct">{counts[opt] || 0}</span>}
                    <span className="opt-check" aria-hidden="true">
                      <svg width="14" height="14" viewBox="0 0 14 14"><path d="M3 7 L6 10 L11 4" stroke="currentColor" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
                    </span>
                  </span>
                </div>
              )}
            </div>
          </div>
        </>
      )}
    </div>);

}

// ───────────────────── ChipSingle: single-select chip matching ChipMulti chrome ─────────────────────
function ChipSingle({ label, options, selected, onChange, formatOption }) {
  const fmt = formatOption || ((x) => x.label || x);
  const [open, setOpen] = useState(false);
  const ref = useRef();
  const [chipRect, setChipRect] = React.useState(null);

  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => {
      if (e.target.closest(".chip-menu")) return;
      if (e.target.closest(".sheet-overlay")) return;
      if (ref.current && ref.current.contains(e.target)) return;
      setOpen(false);
    };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open]);

  useEffect(() => {
    if (!open) return;
    const isMobile = window.matchMedia("(max-width: 760px)").matches;
    if (!isMobile) return;
    document.body.classList.add("no-scroll");
    return () => document.body.classList.remove("no-scroll");
  }, [open]);

  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [open]);

  // Close on page scroll outside the menu (desktop only — mobile is a sheet)
  useEffect(() => {
    if (!open) return;
    if (window.matchMedia("(max-width: 760px)").matches) return;
    const onScroll = (e) => {
      if (e.target.closest && e.target.closest(".chip-menu")) return;
      setOpen(false);
    };
    window.addEventListener("scroll", onScroll, { passive: true, capture: true });
    return () => window.removeEventListener("scroll", onScroll, { capture: true });
  }, [open]);

  React.useEffect(() => {
    if (!open) return;
    const btn = ref.current && ref.current.querySelector("button.chip");
    if (btn) setChipRect(btn.getBoundingClientRect());
  }, [open]);

  const isMobile = typeof window !== "undefined" && window.matchMedia("(max-width: 760px)").matches;
  const desktopStyle = (chipRect && !isMobile) ? {
    position: "fixed",
    top: Math.round(chipRect.bottom + 6) + "px",
    left: "auto",
    right: Math.round(window.innerWidth - chipRect.right) + "px",
    bottom: "auto",
    width: "max-content",
    minWidth: "220px",
    maxWidth: "320px",
    maxHeight: "360px",
    borderRadius: "12px",
    zIndex: 50,
  } : undefined;

  const current = options.find((o) => (o.value || o) === selected) || options[0];
  const currentLabel = current ? fmt(current) : "";

  const sheet = open ? (
    <>
      <div className="sheet-overlay" onClick={() => setOpen(false)}></div>
      <div className="chip-menu" style={desktopStyle}>
        <div className="sheet-head">
          <span className="sheet-title">{label}</span>
          <div className="sheet-actions">
            <button className="sheet-done" onClick={() => setOpen(false)}>Done</button>
          </div>
        </div>
        <div className="chip-menu-list">
          {options.map((opt) => {
            const val = opt.value || opt;
            const isSel = val === selected;
            return (
              <div key={val} className={`opt ${isSel ? "selected" : ""}`} onClick={() => { onChange(val); setOpen(false); }}>
                <span className="opt-label">{fmt(opt)}</span>
                <span className="opt-right">
                  <span className="opt-check" aria-hidden="true">
                    <svg width="14" height="14" viewBox="0 0 14 14"><path d="M3 7 L6 10 L11 4" stroke="currentColor" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
                  </span>
                </span>
              </div>
            );
          })}
        </div>
      </div>
    </>
  ) : null;

  return (
    <div className="chip-select" ref={ref}>
      <button className="chip" onClick={() => setOpen((o) => !o)}>
        {label}: {currentLabel}
        <svg className="caret" viewBox="0 0 10 10"><path d="M2 4 L5 7 L8 4" stroke="currentColor" fill="none" strokeWidth="1.4" strokeLinecap="round" /></svg>
      </button>
      {sheet && ReactDOM.createPortal(sheet, document.body)}
    </div>
  );
}

// ───────────────────── Tiny toast (in-app feedback) ─────────────────────
let __toastId = 0;
const __toastListeners = new Set();
function showToast(message, opts = {}) {
  const t = { id: ++__toastId, message, action: opts.action || null, duration: opts.duration || 3200 };
  __toastListeners.forEach((fn) => fn({ type: "add", t }));
  setTimeout(() => __toastListeners.forEach((fn) => fn({ type: "remove", id: t.id })), t.duration);
}
function ToastHost() {
  const [items, setItems] = useState([]);
  useEffect(() => {
    const onMsg = (e) => {
      if (e.type === "add") setItems((x) => [...x, e.t]);
      if (e.type === "remove") setItems((x) => x.filter((y) => y.id !== e.id));
    };
    __toastListeners.add(onMsg);
    return () => __toastListeners.delete(onMsg);
  }, []);
  if (!items.length) return null;
  return (
    <div className="toast-host" aria-live="polite">
      {items.map((t) => (
        <div key={t.id} className="toast">
          <span className="toast-msg">{t.message}</span>
          {t.action && (
            <button className="toast-action" onClick={() => { t.action.fn(); }}>{t.action.label}</button>
          )}
        </div>
      ))}
    </div>
  );
}

// ───────────────────── useBookmarks (localStorage-backed, no account needed) ─────────────────────
const BOOKMARK_KEY = "pvp_bookmarks_v1";
function useBookmarks() {
  const [ids, setIds] = useState(() => {
    try {
      const raw = localStorage.getItem(BOOKMARK_KEY);
      if (!raw) return [];
      const arr = JSON.parse(raw);
      return Array.isArray(arr) ? arr.filter((x) => typeof x === "string") : [];
    } catch (_) { return []; }
  });
  // Persist
  useEffect(() => {
    try { localStorage.setItem(BOOKMARK_KEY, JSON.stringify(ids)); } catch (_) {}
  }, [ids]);
  // Sync across tabs (so a bookmark added on a second tab shows up here too)
  useEffect(() => {
    const onStorage = (e) => {
      if (e.key !== BOOKMARK_KEY) return;
      try {
        const arr = e.newValue ? JSON.parse(e.newValue) : [];
        if (Array.isArray(arr)) setIds(arr);
      } catch (_) {}
    };
    window.addEventListener("storage", onStorage);
    return () => window.removeEventListener("storage", onStorage);
  }, []);
  const set = useMemo(() => new Set(ids), [ids]);
  const toggle = useCallback((id, displayName) => {
    setIds((cur) => {
      const has = cur.includes(id);
      const next = has ? cur.filter((x) => x !== id) : [...cur, id];
      // Visible feedback so the user knows the action took effect
      if (typeof showToast === "function") {
        showToast(
          has
            ? `Removed ${displayName || "company"} from bookmarks`
            : `${displayName || "Company"} bookmarked. View all in the Bookmarks filter.`,
        );
      }
      return next;
    });
  }, []);
  const has = useCallback((id) => set.has(id), [set]);
  const clear = useCallback(() => setIds([]), []);
  return { ids, set, toggle, has, clear, count: ids.length };
}

// ───────────────────── useIsMobile ─────────────────────
function useIsMobile(maxWidth = 760) {
  const [m, setM] = useState(() =>
    typeof window !== "undefined" && window.matchMedia(`(max-width: ${maxWidth}px)`).matches
  );
  useEffect(() => {
    const mq = window.matchMedia(`(max-width: ${maxWidth}px)`);
    const onChange = () => setM(mq.matches);
    if (mq.addEventListener) mq.addEventListener("change", onChange);
    else mq.addListener(onChange);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener("change", onChange);
      else mq.removeListener(onChange);
    };
  }, [maxWidth]);
  return m;
}

// ───────────────────── News Ticker (CSS marquee, glitch-free) ─────────────────────
function NewsTicker({ companies, onClickCompany }) {
  const items = companies.filter((c) => isRecentNews(c.news));
  const [paused, setPaused] = useState(false);
  if (items.length === 0) return null;
  // Render the list twice, each occupying exactly 50% of the track width so
  // the -50% translate animation produces a perfectly seamless loop.
  const Track = ({ keyPrefix }) =>
  <div className="ticker-row">
      {items.map((c, i) =>
    <button
      className="ticker-item"
      key={`${keyPrefix}-${i}`}
      onClick={() => {
        // Tap = navigate. Hover/Live-label still pauses the marquee for
        // people who want to read; tapping never leaves the ticker paused.
        onClickCompany && onClickCompany(c.id);
      }}>

          <span className="dot"></span>
          <span className="name">{c.name}</span>
          <span className="text">{c.news}</span>
        </button>
    )}
    </div>;

  return (
    <div
      className={`ticker ${paused ? "paused" : ""}`}
      onClick={(e) => {
        // Tapping the label (not an item) toggles pause/play
        if (e.target.closest(".ticker-label")) setPaused((p) => !p);
      }}>
      <div className="ticker-label" role="button" aria-label={paused ? "Resume ticker" : "Pause ticker"}>
        <span className="pulse"></span>
        {paused ? "Paused · tap to resume" : `Live · ${items.length} updates`}
      </div>
      <div className="ticker-viewport">
        <div className="ticker-track">
          <Track keyPrefix="a" />
          <Track keyPrefix="b" />
        </div>
      </div>
    </div>);

}

// ───────────────────── RAG-style LLM Search ─────────────────────
// Builds a compact JSON corpus, sends to Claude Haiku, returns ranked company IDs + reasoning.
function buildCorpus(companies) {
  const enriched = window.FOUNDERS_ENRICHED || {};
  return companies.map((c) => ({
    id: c.id,
    name: c.name,
    sector: c.sector,
    stage: c.stage,
    status: c.status,
    invested: c.invested,
    funding: c.funding,
    quirk: c.quirk,
    description: c.description,
    news: c.news,
    founders: c.founders.map((f) => {
      const ex = enriched[c.id]?.[f.name];
      const parts = [`${f.name} (${f.role}) [${f.attending === "yes" ? "ATTENDING" : "NOT ATTENDING"}]`];
      if (ex?.prior) parts.push(`Prior: ${ex.prior}`);
      if (ex?.pedigree) parts.push(`Education: ${ex.pedigree}`);
      return parts.join(" | ");
    }).join("; "),
    teamAttendees: (c.teamAttendees || []).map((t) =>
      `${t.name} (${t.designation || "team"}) [ATTENDING]`
    ).join("; "),
    coInvestors: c.coInvestors.slice(0, 6).join(", ")
  }));
}

async function semanticSearch(query, companies) {
  const corpus = buildCorpus(companies);
  const prompt = `You are a portfolio-search assistant for Prime Venture Partners.
The user asked: "${query}"

Below is the full Prime portfolio as JSON. Return ONLY valid JSON in this exact shape (no markdown, no prose):
{"matches":[{"id":"<company id>","why":"<one sentence on why this matches>"}],"summary":"<2-sentence summary of the result set>"}

Rules:
- Return up to 10 most relevant companies, most relevant first
- "why" must be specific to the user's query (cite founder names, sectors, news, co-investors as relevant)
- If nothing matches, return {"matches":[],"summary":"No portfolio companies match this query."}
- Use ONLY ids from the corpus

PORTFOLIO:
${JSON.stringify(corpus)}`;

  try {
    const raw = await callClaude(prompt);
    const cleaned = raw.replace(/^```(?:json)?/i, "").replace(/```$/, "").trim();
    return JSON.parse(cleaned);
  } catch (err) {
    console.error("Semantic search failed", err);
    return { matches: [], summary: "Search assistant is unavailable right now. Try keyword search instead." };
  }
}

// Calls Vercel serverless function which proxies to Claude Haiku
async function callClaude(prompt) {
  const resp = await fetch("/api/claude", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt })
  });
  if (!resp.ok) {
    const text = await resp.text().catch(() => "");
    throw new Error(`Claude API ${resp.status}: ${text}`);
  }
  const data = await resp.json();
  return data.text || "";
}

// Streaming variant. `onChunk(deltaText)` is called as each text fragment arrives.
async function streamClaude(prompt, onChunk) {
  const resp = await fetch("/api/claude", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt, stream: true }),
  });
  if (!resp.ok || !resp.body) {
    const text = await resp.text().catch(() => "");
    throw new Error(`Claude API ${resp.status}: ${text}`);
  }
  const reader = resp.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let full = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let idx;
    // SSE events terminated by a blank line ("\n\n")
    while ((idx = buffer.indexOf("\n\n")) !== -1) {
      const raw = buffer.slice(0, idx);
      buffer = buffer.slice(idx + 2);
      let evt = "";
      let dataStr = "";
      for (const line of raw.split("\n")) {
        if (line.startsWith("event:")) evt = line.slice(6).trim();
        else if (line.startsWith("data:")) dataStr = line.slice(5).trim();
      }
      if (!dataStr) continue;
      if (evt === "content_block_delta") {
        try {
          const obj = JSON.parse(dataStr);
          if (obj.delta && obj.delta.type === "text_delta") {
            const piece = obj.delta.text || "";
            full += piece;
            onChunk(piece, full);
          }
        } catch (_) { /* ignore malformed event */ }
      } else if (evt === "message_stop") {
        return full;
      }
    }
  }
  return full;
}

function AISearchResults({ results, companies, onPick, loading, query, onDismiss }) {
  if (!results && !loading) return null;
  return (
    <div className="ai-results">
      <div className="ai-results-head">
        <span className="ai-pill">AI search</span>
        <span className="ai-query">"{query}"</span>
        {onDismiss &&
        <button className="ai-dismiss" onClick={onDismiss} aria-label="Dismiss">
            <svg width="11" height="11" viewBox="0 0 14 14"><path d="M2 2 L12 12 M12 2 L2 12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /></svg>
          </button>
        }
      </div>
      {loading &&
      <div className="ai-loading-block">
          <div className="ai-loading-hint">
            <span className="spinner"></span>
            Reading the full portfolio…
          </div>
          <div className="ai-matches">
            {[0, 1, 2].map((k) =>
            <div className="ai-match skeleton" key={k}>
                <span className="rank skel-shape"></span>
                <span className="row-logo skel-shape skel-square"></span>
                <div className="match-body">
                  <div className="match-name skel-shape skel-line"></div>
                  <div className="match-why skel-shape skel-line short"></div>
                </div>
              </div>
            )}
          </div>
        </div>
      }
      {!loading && results &&
      <>
          {results.summary && <p className="ai-summary">{results.summary}</p>}
          {results.matches.length > 0 ?
        <div className="ai-matches">
              {results.matches.map(({ id, why }, i) => {
            const c = companies.find((x) => x.id === id);
            if (!c) return null;
            return (
              <button className="ai-match" key={id} onClick={() => onPick(c.id)}>
                    <span className="rank">{i + 1}</span>
                    <Logo company={c} />
                    <div className="match-body">
                      <div className="match-name">{c.name}<span className="match-sector">· {c.sector}</span></div>
                      <div className="match-why">{why}</div>
                    </div>
                    <span className="match-arrow">
                      <svg width="14" height="14" viewBox="0 0 14 14"><path d="M3 7 L11 7 M7 3 L11 7 L7 11" stroke="currentColor" strokeWidth="1.6" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
                    </span>
                  </button>);

          })}
            </div> :

        <div className="ai-empty">No companies matched. Try rephrasing or use the filters below.</div>
        }
        </>
      }
    </div>);

}

// ───────────────────── Ask the Portfolio (chat-style) ─────────────────────
// Tiny inline markdown renderer: **bold**, *italic*, `code`, links, paragraphs, line breaks.
// Strips out [Companies: ...] tags so they don't show in the bubble.
function renderMarkdown(text) {
  if (typeof text !== "string" || !text) return null;
  try {
    // Strip the inline tags we use for cards/follow-ups (rendered separately).
    // Use a tolerant regex that also matches *unclosed* tags (e.g. when a
    // streaming response runs out of tokens mid-tag and never emits the `]`).
    const cleaned = text
      .replace(/\[Companies:[^\]]*(?:\]|$)/gi, "")
      .replace(/\[Founders:[^\]]*(?:\]|$)/gi, "")
      .replace(/\[Followups:[^\]]*(?:\]|$)/gi, "")
      .trim();

    // Pass 1: split into structural blocks (paragraphs, bullet lists, headings)
    const blocks = [];
    const lines = cleaned.split(/\n/);
    let listItems = null;
    let paraLines = [];
    const flushPara = () => {
      if (paraLines.length) { blocks.push({ kind: "p", lines: paraLines }); paraLines = []; }
    };
    const flushList = () => {
      if (listItems && listItems.length) { blocks.push({ kind: "ul", items: listItems }); }
      listItems = null;
    };
    for (const raw of lines) {
      const line = raw.replace(/\s+$/, "");
      if (!line.trim()) { flushPara(); flushList(); continue; }
      // Bullet: -, –, —, •, *, ·, or "1. "
      const bullet = line.match(/^\s*(?:[-–—•*·]|\d+[.)])\s+(.*)$/);
      if (bullet) {
        flushPara();
        if (!listItems) listItems = [];
        listItems.push(bullet[1]);
        continue;
      }
      // Markdown header (## or ###) → small uppercase eyebrow style
      const head = line.match(/^\s*#{1,6}\s+(.*)$/);
      if (head) {
        flushPara(); flushList();
        blocks.push({ kind: "h", text: head[1] });
        continue;
      }
      // Bold-only line ending with a colon → treat as a sub-heading
      const subHead = line.match(/^\s*\*\*([^*]+):\*\*\s*$/);
      if (subHead) {
        flushPara(); flushList();
        blocks.push({ kind: "h", text: subHead[1] });
        continue;
      }
      flushList();
      paraLines.push(line);
    }
    flushPara(); flushList();

    return blocks.map((b, i) => {
      if (b.kind === "p") {
        return (
          <p key={i} className="md-p">
            {b.lines.map((line, li) => (
              <React.Fragment key={li}>
                {renderInline(line)}
                {li < b.lines.length - 1 && <br />}
              </React.Fragment>
            ))}
          </p>
        );
      }
      if (b.kind === "h") {
        return <h4 key={i} className="md-h">{renderInline(b.text)}</h4>;
      }
      if (b.kind === "ul") {
        return (
          <ul key={i} className="md-ul">
            {b.items.map((it, j) => <li key={j}>{renderInline(it)}</li>)}
          </ul>
        );
      }
      return null;
    });
  } catch (_) {
    return <p className="md-p">{String(text)}</p>;
  }
}

function renderInline(text) {
  // Tokenize for **bold**, *italic*, `code`. Keep it simple and non-greedy.
  const parts = [];
  let rest = text;
  let key = 0;
  // Pattern: **x** | *x* | `x`
  const re = /\*\*([^*]+)\*\*|\*([^*]+)\*|`([^`]+)`/;
  while (rest) {
    const m = rest.match(re);
    if (!m) { parts.push(rest); break; }
    if (m.index > 0) parts.push(rest.slice(0, m.index));
    if (m[1] !== undefined) parts.push(<strong key={key++}>{m[1]}</strong>);
    else if (m[2] !== undefined) parts.push(<em key={key++}>{m[2]}</em>);
    else if (m[3] !== undefined) parts.push(<code key={key++} className="md-code">{m[3]}</code>);
    rest = rest.slice(m.index + m[0].length);
  }
  return parts;
}

function AskPanel({ companies, onClose, onPickCompany, initialQuery }) {
  const STORAGE_KEY = "pvp_ask_history_v1";
  // Prefetch the enriched founders bundle as soon as the panel mounts, so
  // by the time the user finishes their first question it's ready.
  useEffect(() => { loadEnrichedFounders(); }, []);
  const [messages, setMessages] = useState(() => {
    try {
      const saved = localStorage.getItem(STORAGE_KEY);
      if (!saved) return [];
      const parsed = JSON.parse(saved);
      if (!Array.isArray(parsed)) return [];
      // Sanitize: only keep entries with required shape; cap to last 60 turns
      // so a runaway history doesn't blow iOS Safari's memory budget.
      return parsed
        .filter((m) => m && typeof m.text === "string" && (m.role === "user" || m.role === "assistant"))
        .slice(-60);
    } catch (_) { return []; }
  });
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const [confirmClear, setConfirmClear] = useState(false);
  const bodyRef = useRef();
  const inputRef = useRef();
  const sentInitial = useRef(false);

  // Pool of suggestion prompts; show 6 chosen at random per panel mount so
  // the empty state feels fresh rather than static.
  const suggestionPool = useMemo(() => [
    "Who's attending the dinner tonight?",
    "Which founders worked at Flipkart before?",
    "IIT alumni in the portfolio",
    "Ex-Google or Microsoft founders",
    "Show me the AI-native bets",
    "Founders building in fintech",
    "Female founders attending tonight",
    "Repeat founders we've backed",
    "Companies that raised in 2026",
    "Stealth or pre-Series A bets",
    "Ex-McKinsey or BCG founders",
    "Ex-Razorpay or Cred founders",
    "Climate or cleantech bets",
    "B2B SaaS founders here tonight",
    "Founders on their second startup",
    "Who has the deepest fintech track record?",
  ], []);
  const suggestions = useMemo(() => {
    const pool = [...suggestionPool];
    for (let i = pool.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const t = pool[i]; pool[i] = pool[j]; pool[j] = t;
    }
    return pool.slice(0, 6);
  }, [suggestionPool]);

  // Persist history to localStorage whenever it changes
  useEffect(() => {
    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(messages)); } catch (_) {}
  }, [messages]);

  useEffect(() => {
    if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
  }, [messages, loading]);

  // Escape closes the panel
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  const send = async (q) => {
    const question = (q ?? input).trim();
    if (!question || loading) return;
    setInput("");
    // Append the user message AND a placeholder assistant message we'll
    // mutate as the stream comes in.
    setMessages((m) => [
      ...m,
      { role: "user", text: question },
      { role: "assistant", text: "", streaming: true, companyIds: [], founderRefs: [], followups: [] },
    ]);
    setLoading(true);

    await loadEnrichedFounders();
    const corpus = buildCorpus(companies);
    // Cap to last 4 user-assistant pairs (8 messages) so prompt size stays bounded.
    const historySrc = messages.slice(-8);
    const history = historySrc.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.text}`).join("\n");
    const prompt = `You are an analyst for Prime Venture Partners' portfolio. Use ONLY what's in the JSON below — never invent facts.

DATA SHAPE:
- Each company has: id, name, sector, stage, status, invested, funding, quirk, description, news, founders, teamAttendees, coInvestors.
- "founders" is "Name (Role) [ATTENDING|NOT ATTENDING] | Prior: <work> | Education: <schools>" segments separated by "; ". The bracket tag is authoritative.
- "teamAttendees" is "Name (Designation) [ATTENDING]" segments — additional people coming.
- ~48 founders have Prior on file; ~99 have Education. Substring-match those fields for background questions.

FORMATTING RULES — STRICT (output is rendered as markdown):
- Lead with one short summary sentence (≤25 words).
- ALWAYS use bullet lists for any answer that names 2 or more founders/companies. NEVER return a comma-separated paragraph instead. The bullet character MUST be "- " (hyphen + space).
- For each bullet about a founder: \`- **<Full Name>** (<Company>) — <one-line detail>\`.
- For each bullet about a company (when no specific founder is the focus): \`- **<Company>** — <one-line detail>\`.
- Use short section headings (only when grouping ≥4 bullets by sector/category): write as \`**Heading:**\` on its own line above the bullets.
- For "who is attending" / guest-list questions, you MAY exceed 8 bullets — list every attending person, grouped by sector heading. The 8-bullet cap does NOT apply to roster questions.
- For all other questions, cap at 8 bullets — quality over quantity.
- Never write a flat list separated by commas. Never use "1. " numbering. Always use "- ".
- Do NOT invent founders, education, or prior work that isn't in the data.

CONTEXT-AWARENESS RULES:
- The PORTFOLIO JSON below is the SOLE source of truth — facts in it ALWAYS win over anything in "Previous turns". If a prior assistant message contradicts the JSON (e.g. an outdated attending status, an old role, a stale company name), silently use the JSON's current value.
- If the question can't be fully answered from the data, give the partial answer and end with one suggested rephrase.

CARD TAGS — append at the very end on their own lines (these are stripped from the visible reply and rendered as cards/chips):
1. \`[Companies: id1, id2, ...]\` — 1–6 company ids relevant to the answer. Omit only when answer is purely conversational.
2. \`[Founders: <founder name>@<company id>; <founder name>@<company id>; ...]\` — every founder you named in your answer that the user would want a contact card for, in the order they appear. Use the exact name and company id from the data. Cap at 12.
3. \`[Followups: <q1>; <q2>; <q3>]\` — exactly three short, naturally-curious follow-up questions the user might ask next, given this answer. Each ≤7 words.

PORTFOLIO JSON:
${JSON.stringify(corpus)}

${history ? "Previous turns:\n" + history + "\n" : ""}User just asked: "${question}"

Reply now.`;

    let buffer = "";
    let renderTimer = null;
    const flushRender = () => {
      const text = buffer; // closure copy
      setMessages((arr) => {
        const copy = arr.slice();
        // Update the last assistant message in-place
        for (let i = copy.length - 1; i >= 0; i--) {
          if (copy[i].role === "assistant" && copy[i].streaming) {
            copy[i] = { ...copy[i], text };
            break;
          }
        }
        return copy;
      });
    };

    try {
      const full = await streamClaude(prompt, (delta) => {
        buffer += delta;
        // throttle re-renders to ~30fps
        if (!renderTimer) {
          renderTimer = setTimeout(() => { renderTimer = null; flushRender(); }, 35);
        }
      });
      // Final flush + parse out tags
      const reply = full || buffer;
      const compTag = reply.match(/\[Companies:\s*([^\]]+)\]/i);
      const fndTag  = reply.match(/\[Founders:\s*([^\]]+)\]/i);
      const fupTag  = reply.match(/\[Followups:\s*([^\]]+)\]/i);
      const companyIds = compTag
        ? compTag[1].split(",").map((s) => s.trim()).filter((id) => companies.find((c) => c.id === id))
        : [];
      const founderRefs = fndTag
        ? fndTag[1].split(/[;,]/).map((s) => s.trim()).filter(Boolean).map((entry) => {
            const [name, cid] = entry.split("@").map((x) => x.trim());
            const company = companies.find((c) => c.id === cid);
            if (!company) return null;
            const founder = (company.founders || []).find((f) => f.name === name)
                          || (company.teamAttendees || []).find((t) => t.name === name);
            if (!founder) return null;
            return { name, companyId: cid, ...founder };
          }).filter(Boolean).slice(0, 12)
        : [];
      const followups = fupTag
        ? fupTag[1].split(/[;]/).map((s) => s.trim()).filter(Boolean).slice(0, 3)
        : [];
      const cleanText = reply
        .replace(/\[Companies:[^\]]+\]/i, "")
        .replace(/\[Founders:[^\]]+\]/i, "")
        .replace(/\[Followups:[^\]]+\]/i, "")
        .trim();

      if (renderTimer) { clearTimeout(renderTimer); renderTimer = null; }
      setMessages((arr) => {
        const copy = arr.slice();
        for (let i = copy.length - 1; i >= 0; i--) {
          if (copy[i].role === "assistant" && copy[i].streaming) {
            copy[i] = { role: "assistant", text: cleanText, companyIds, founderRefs, followups };
            break;
          }
        }
        return copy;
      });
    } catch (err) {
      if (renderTimer) { clearTimeout(renderTimer); renderTimer = null; }
      setMessages((arr) => {
        const copy = arr.slice();
        for (let i = copy.length - 1; i >= 0; i--) {
          if (copy[i].role === "assistant" && copy[i].streaming) {
            copy[i] = { role: "assistant", text: "Sorry — couldn't reach the AI service. Tap Retry to try again.", companyIds: [], founderRefs: [], followups: [], failed: true, retryQuestion: question };
            break;
          }
        }
        return copy;
      });
    } finally {
      setLoading(false);
    }
  };

  // Auto-send the seeded query when the panel opens via the search bar
  useEffect(() => {
    if (initialQuery && !sentInitial.current && !loading) {
      sentInitial.current = true;
      send(initialQuery);
    }
  }, [initialQuery]);

  const clearHistory = () => {
    setMessages([]);
    try { localStorage.removeItem(STORAGE_KEY); } catch (_) {}
  };

  // Auto-resize textarea on input.
  const resizeInput = () => {
    const ta = inputRef.current;
    if (!ta) return;
    ta.style.height = "auto";
    ta.style.height = Math.min(ta.scrollHeight, 140) + "px";
  };
  useEffect(() => { resizeInput(); }, [input]);

  return (
    <>
      <div className="ai-overlay" onClick={onClose}></div>
      <aside className="ask-panel">
        <header className="ask-head">
          <div className="ask-head-text">
            <div className="ask-title">Ask the Portfolio</div>
            <div className="ask-sub">Powered by Claude · {companies.length} companies</div>
          </div>
          <div className="ask-head-actions">
            <button
              className="ask-clear"
              onClick={() => {
                if (messages.length === 0) clearHistory();
                else setConfirmClear(true);
              }}
              title="Start a new chat (clears current conversation)"
              aria-label="New chat">
              <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
                <path d="M11 3 V11 M3 7 L8 7 M5 5 L3 7 L5 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
              <span className="ask-clear-label">New chat</span>
            </button>
            <button className="drawer-close" onClick={onClose} aria-label="Close">
              <svg width="14" height="14" viewBox="0 0 14 14"><path d="M2 2 L12 12 M12 2 L2 12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /></svg>
            </button>
          </div>
        </header>
        {confirmClear &&
          <div className="confirm-modal" onClick={() => setConfirmClear(false)}>
            <div className="confirm-card" onClick={(e) => e.stopPropagation()}>
              <h4>Start a new chat?</h4>
              <p>This clears the current conversation. You can't undo this.</p>
              <div className="confirm-actions">
                <button className="confirm-cancel" onClick={() => setConfirmClear(false)}>Cancel</button>
                <button className="confirm-confirm" onClick={() => { clearHistory(); setConfirmClear(false); }}>Start new chat</button>
              </div>
            </div>
          </div>
        }
        <div className="ask-body" ref={bodyRef}>
          {messages.length === 0 &&
          <div className="ask-welcome">
              <p>Ask anything about Prime's {companies.length} portfolio companies. The AI sees every founder, sector, stage, news headline, co-investor, and who's attending tonight.</p>
              <div className="suggest-chips">
                {suggestions.map((s) =>
              <button key={s} className="suggest" onClick={() => send(s)}>{s}</button>
              )}
              </div>
            </div>
          }
          {messages.map((m, i) => {
            const isLastAssistant = i === messages.length - 1 && m.role === "assistant";
            return (
              <div key={i} className={`bubble ${m.role} ${m.streaming ? "streaming" : ""}`}>
                {m.role === "assistant" ? (
                  m.streaming && !m.text ? (
                    <div className="bubble-typing">
                      <span className="dots"><span></span><span></span><span></span></span>
                      Thinking…
                    </div>
                  ) : (
                    <div className="bubble-text">
                      {renderMarkdown(m.text)}
                      {m.streaming && <span className="stream-cursor">▍</span>}
                    </div>
                  )
                ) : (
                  <div className="bubble-text">{m.text}</div>
                )}

                {!m.streaming && m.founderRefs && m.founderRefs.length > 0 &&
                  <div className="bubble-founders">
                    <div className="bubble-section-label">Founders mentioned</div>
                    <div className="founder-cards">
                      {m.founderRefs.map((f, idx) => {
                        const co = companies.find((c) => c.id === f.companyId);
                        return (
                          <div
                            key={idx}
                            className="founder-card"
                            onClick={(e) => {
                              // Only navigate if the click was on the body, not on action icons
                              if (e.target.closest(".fc-actions")) return;
                              if (co) onPickCompany(co.id);
                            }}>
                            <div className="fc-main">
                              <PersonAvatar name={f.name} linkedin={f.linkedin} className="fc-avatar" />
                              <span className="fc-info">
                                <span className="fc-name">{f.name}</span>
                                <span className="fc-role">{f.role || f.designation || ""}{co ? ` · ${co.name}` : ""}</span>
                              </span>
                            </div>
                            <span className="fc-actions">
                              {f.email && <a className="fc-act" href={`mailto:${f.email}`} title="Email">
                                <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="3" width="11" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.2" /><path d="M2 4 L7 8 L12 4" stroke="currentColor" strokeWidth="1.2" fill="none" /></svg>
                              </a>}
                              {f.linkedin && <a className="fc-act" href={`https://linkedin.com/in/${f.linkedin}`} target="_blank" rel="noopener noreferrer" title="LinkedIn">
                                <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M4.98 3.5C4.98 4.88 3.87 6 2.5 6S0 4.88 0 3.5 1.13 1 2.5 1s2.48 1.12 2.48 2.5zM.22 8h4.56v14H.22V8zm7.61 0h4.36v1.92h.06c.61-1.15 2.1-2.36 4.32-2.36 4.62 0 5.47 3.04 5.47 6.99V22h-4.56v-6.6c0-1.57-.03-3.6-2.19-3.6-2.2 0-2.54 1.71-2.54 3.48V22H7.83V8z" /></svg>
                              </a>}
                            </span>
                          </div>
                        );
                      })}
                    </div>
                  </div>
                }

                {!m.streaming && m.failed &&
                  <div className="bubble-retry">
                    <button className="retry-btn" onClick={() => send(m.retryQuestion)} disabled={loading}>
                      <svg width="12" height="12" viewBox="0 0 14 14" fill="none"><path d="M11 4 A4 4 0 1 0 11 10 M11 1 V5 H7" stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>
                      Retry
                    </button>
                  </div>
                }

                {!m.streaming && m.companyIds && m.companyIds.length > 0 &&
                  <div className="bubble-cards">
                    {m.companyIds.slice(0, 6).map((id) => {
                      const c = companies.find((x) => x.id === id);
                      if (!c) return null;
                      return (
                        <button key={id} className="mini-card" onClick={() => onPickCompany(c.id)}>
                          <Logo company={c} />
                          <span className="mc-name">{c.name}</span>
                        </button>);
                    })}
                  </div>
                }

                {!m.streaming && isLastAssistant && m.followups && m.followups.length > 0 &&
                  <div className="bubble-followups">
                    <div className="bubble-section-label">Follow-ups</div>
                    <div className="followup-chips">
                      {m.followups.map((q, idx) =>
                        <button key={idx} className="followup-chip" onClick={() => send(q)} disabled={loading}>
                          {q}
                          <svg width="11" height="11" viewBox="0 0 14 14" fill="none"><path d="M3 7 L11 7 M7 3 L11 7 L7 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
                        </button>
                      )}
                    </div>
                  </div>
                }
              </div>
            );
          })}
        </div>
        <div className="ask-input">
          <textarea
            ref={inputRef}
            rows={1}
            placeholder="Ask anything…"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={(e) => {
              // Enter sends; Shift+Enter inserts a newline
              if (e.key === "Enter" && !e.shiftKey) {
                e.preventDefault();
                send();
              }
            }}
            disabled={loading} />
          <button className="ask-send" onClick={() => send()} disabled={loading || !input.trim()} aria-label="Send">
            <svg width="14" height="14" viewBox="0 0 14 14"><path d="M2 7 L12 7 M8 3 L12 7 L8 11" stroke="currentColor" strokeWidth="1.7" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
          </button>
        </div>
      </aside>
    </>);

}

// ───────────────────── Auto-intro generator ─────────────────────
function IntroGenerator({ company, onClose }) {
  const [tone, setTone] = useState("warm");
  const [angle, setAngle] = useState("");
  const [output, setOutput] = useState("");
  const [loading, setLoading] = useState(false);
  const [copied, setCopied] = useState(false);

  const generate = async () => {
    setLoading(true);
    setCopied(false);
    const founderList = company.founders.map((f) => f.prior ? `${f.name} (${f.role}, ${f.prior})` : `${f.name} (${f.role})`).join(", ");
    const prompt = `Draft a short, ${tone} intro email from a Prime Venture Partners team member to ${founderList} of ${company.name}.

Company context: ${company.description}
Latest news: ${company.news}
Sector: ${company.sector}, ${company.stage}

${angle ? `Reason for the intro: ${angle}` : "It's a warm catch-up / check-in."}

Constraints:
- Subject line first, then 3-4 short paragraphs
- Sound human, not LinkedIn-formal. No "I hope this finds you well."
- Reference one specific thing from their company
- Under 120 words

Format:
Subject: ...

[email body]`;
    try {
      const reply = await callClaude(prompt);
      setOutput(reply);
    } catch (err) {
      setOutput("Couldn't reach AI service. Try again.");
    } finally {
      setLoading(false);
    }
  };

  const copy = () => {
    navigator.clipboard?.writeText(output);
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  };

  return (
    <div className="intro-gen">
      <div className="intro-head">
        <div>
          <div className="intro-title">AI Intro Drafter</div>
          <div className="intro-sub">Draft an email to {company.name}'s founders</div>
        </div>
        <button className="ico-btn" onClick={onClose} aria-label="Close">
          <svg width="12" height="12" viewBox="0 0 14 14"><path d="M2 2 L12 12 M12 2 L2 12" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /></svg>
        </button>
      </div>
      <div className="intro-controls">
        <div className="ctl">
          <label>Tone</label>
          <div className="seg">
            {["warm", "punchy", "formal"].map((t) =>
            <button key={t} className={tone === t ? "on" : ""} onClick={() => setTone(t)}>{t}</button>
            )}
          </div>
        </div>
        <div className="ctl">
          <label>What's the ask? (optional)</label>
          <input
            type="text"
            placeholder="e.g. catch up on Q2 metrics, intro to a hire, pre-IPO check-in"
            value={angle}
            onChange={(e) => setAngle(e.target.value)} />
          
        </div>
      </div>
      <button className="btn-orange intro-btn" onClick={generate} disabled={loading}>
        {loading ? "Drafting…" : output ? "Regenerate" : "Draft email"}
      </button>
      {output &&
      <div className="intro-output">
          <pre>{output}</pre>
          <button className="copy-btn" onClick={copy}>{copied ? "Copied" : "Copy"}</button>
        </div>
      }
    </div>);

}

// ───────────────────── Drawer ─────────────────────
// ───────────────────── Confetti (mounted briefly on featured-drawer open) ─────────────────────
function Confetti({ companyId }) {
  const PALETTE = ['#FF5A1F', '#0E0E0E', '#1F4FFF', '#0EA672', '#7A4BFF', '#E0457B', '#FF8A00', '#F7C948'];
  // Re-shuffle particles whenever a different company opens
  const particles = useMemo(() => {
    const N = 90;
    return Array.from({ length: N }, (_, i) => ({
      key: i,
      color: PALETTE[Math.floor(Math.random() * PALETTE.length)],
      left: Math.random() * 100,
      delay: Math.random() * 0.45,
      duration: 1.7 + Math.random() * 1.5,
      rotate: (Math.random() * 720 - 360) | 0,
      size: 6 + Math.round(Math.random() * 8),
      drift: ((Math.random() - 0.5) * 280) | 0,
      shape: Math.random() < 0.4 ? "rect" : (Math.random() < 0.5 ? "round" : "rect-tall"),
    }));
  }, [companyId]);
  const [active, setActive] = useState(true);
  useEffect(() => {
    setActive(true);
    const t = setTimeout(() => setActive(false), 4200); // stop animating after first burst
    return () => clearTimeout(t);
  }, [companyId]);
  if (!active) return null;
  return (
    <div className="confetti-layer" aria-hidden="true">
      {particles.map((p) => (
        <span
          key={p.key}
          className={`confetti-piece confetti-${p.shape}`}
          style={{
            left: p.left + "%",
            background: p.color,
            width: (p.shape === "rect-tall" ? p.size * 0.55 : p.size) + "px",
            height: (p.shape === "rect-tall" ? p.size * 1.6 : p.size) + "px",
            animationDelay: p.delay + "s",
            animationDuration: p.duration + "s",
            ['--rotate']: p.rotate + "deg",
            ['--drift']: p.drift + "px",
          }} />
      ))}
    </div>
  );
}

function Drawer({ company, onClose, onPrev, onNext, bookmarked, onToggleBookmark }) {
  const drawerRef = useRef();
  const touchRef = useRef({ startX: 0, startY: 0, dx: 0, dy: 0, swiping: false });

  useEffect(() => {
    if (!company) return;
    const onKey = (e) => {
      if (e.key === "Escape") onClose();else
      if (e.key === "ArrowDown" || e.key === "ArrowRight") onNext();else
      if (e.key === "ArrowUp" || e.key === "ArrowLeft") onPrev();
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [company, onClose, onPrev, onNext]);

  // Swipe gestures on mobile:
  //   - Horizontal swipe (>70px, mostly horizontal) → close drawer
  //   - Vertical swipe on the drawer-head (>60px) → next/prev company
  // The vertical handler is gated to the .drawer-head element so it doesn't
  // fight with the body's natural scrolling.
  const onTouchStart = (e) => {
    const t = e.touches[0];
    const onHead = !!e.target.closest(".drawer-head, .drawer-handle");
    touchRef.current = {
      startX: t.clientX, startY: t.clientY,
      dx: 0, dy: 0,
      swiping: true, onHead,
    };
  };
  const onTouchMove = (e) => {
    if (!touchRef.current.swiping) return;
    const t = e.touches[0];
    const dx = t.clientX - touchRef.current.startX;
    const dy = t.clientY - touchRef.current.startY;
    touchRef.current.dx = dx;
    touchRef.current.dy = dy;
    if (Math.abs(dx) > 10 && Math.abs(dx) > Math.abs(dy) * 1.4 && drawerRef.current) {
      // Horizontal swipe: follow finger
      drawerRef.current.style.transform = `translateX(${Math.max(0, dx)}px)`;
      drawerRef.current.classList.add("swiping");
    }
  };
  const onTouchEnd = () => {
    if (!touchRef.current.swiping) return;
    const { dx, dy, onHead } = touchRef.current;
    touchRef.current.swiping = false;
    if (drawerRef.current) {
      drawerRef.current.classList.remove("swiping");
      drawerRef.current.style.transform = "";
    }
    const horizontal = Math.abs(dx) > Math.abs(dy) * 1.4;
    const vertical = Math.abs(dy) > Math.abs(dx) * 1.4;
    if (horizontal && Math.abs(dx) > 70) {
      onClose();
    } else if (onHead && vertical && Math.abs(dy) > 60) {
      // Swipe up (negative dy) → next, swipe down (positive dy) → prev
      if (dy < 0) onNext();
      else onPrev();
    }
  };

  const featured = !!company?.featured;

  return (
    <>
      <div className={`drawer-overlay ${company ? "open" : ""} ${featured ? "featured" : ""}`} onClick={onClose}></div>
      {featured && company && <Confetti companyId={company.id} />}
      <aside
        ref={drawerRef}
        className={`drawer ${company ? "open" : ""} ${featured ? "featured" : ""}`}
        onTouchStart={onTouchStart}
        onTouchMove={onTouchMove}
        onTouchEnd={onTouchEnd}>
        {company &&
        <>
            <span className="drawer-handle" aria-hidden="true"></span>
            <div className="drawer-head">
              {featured && (
                <span className="featured-ribbon">
                  <svg width="11" height="11" viewBox="0 0 14 14" fill="none"><path d="M7 1 L8.6 5.4 L13 7 L8.6 8.6 L7 13 L5.4 8.6 L1 7 L5.4 5.4 Z" fill="currentColor" /></svg>
                  Latest investment
                </span>
              )}
              <div className="drawer-head-actions">
                <button
                  className="drawer-share"
                  title="Share / save this company"
                  aria-label="Share or save link"
                  onClick={async () => {
                    const url = new URL(window.location.href);
                    url.searchParams.set("c", company.id);
                    const link = url.toString();
                    const title = `${company.name} — Prime Venture Partners`;
                    const text = `${company.name}: ${company.quirk || company.description || ""}`;
                    // Try Web Share API first (mobile + Safari + macOS Chrome)
                    let sheetOpened = false;
                    if (typeof navigator.share === "function") {
                      try {
                        await navigator.share({ title, text, url: link });
                        sheetOpened = true;
                      } catch (err) {
                        // AbortError = user dismissed the sheet — that's fine, no further action.
                        if (err && err.name === "AbortError") return;
                        // Otherwise (NotAllowedError, etc) fall through to clipboard
                      }
                    }
                    if (sheetOpened) return;
                    // Fallback: copy URL + show explicit hint
                    try {
                      await navigator.clipboard.writeText(link);
                      showToast(`Link copied. Press ⌘D / Ctrl+D to bookmark in your browser, or paste anywhere.`, { duration: 5000 });
                    } catch (_) {
                      showToast(`Copy this URL: ${link}`, { duration: 8000 });
                    }
                  }}>
                  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                    {/* Universally-recognizable share / upload arrow */}
                    <path d="M8 2 V11" />
                    <path d="M5 5 L8 2 L11 5" />
                    <path d="M3 9 V13 a1 1 0 0 0 1 1 H12 a1 1 0 0 0 1-1 V9" />
                  </svg>
                  <span className="drawer-share-label">Save</span>
                </button>
                <BookmarkBtn active={!!bookmarked} onToggle={onToggleBookmark} className="drawer-bookmark" />
                <button className="drawer-close" onClick={onClose} aria-label="Close">
                  <svg width="14" height="14" viewBox="0 0 14 14"><path d="M2 2 L12 12 M12 2 L2 12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /></svg>
                </button>
              </div>
            </div>
            <div className="drawer-body">
              <div className="detail-hero">
                <Logo company={company} size="detail" />
                <div className="detail-title">
                  <h2>{company.name}</h2>
                  <a className="domain" href={`https://${company.domain}`} target="_blank" rel="noopener noreferrer">
                    {company.domain} ↗
                  </a>
                  <div className="tags-row">
                    <span className={`tag status ${company.status}`}>{statusLabel(company.status)}</span>
                    <span className="tag">
                      <span className="swatch" style={{ background: (window.SECTOR_COLORS && window.SECTOR_COLORS[company.sector]) || "#777" }}></span>
                      {company.sector}
                    </span>
                    <span className="tag">{company.stage}</span>
                    {/* Invested year removed per user */}
                    {/* funding tag removed — data preserved for AI use */}
                  </div>
                </div>
              </div>

              <div className="detail-quirk">{company.quirk}</div>

              <div className="detail-section">
                <h4>About</h4>
                <p>{company.description}</p>
              </div>

              {company.news &&
            <div className="detail-section">
                  <h4>Latest</h4>
                  <div className="news-card">
                    <span className="news-tag">News</span>
                    <span>{company.news}</span>
                  </div>
                </div>
            }

              <div className="detail-section">
                <div className="section-head">
                  <h4>Founders & Contact</h4>
                </div>
                <div className="founders-grid">
                  {company.founders.map((f, i) =>
                <div className={`founder rsvp-${f.attending === "yes" ? "yes" : "no"}`} key={i}>
                      <PersonAvatar name={f.name} linkedin={f.linkedin} className="avatar" />
                      <div className="founder-info">
                        <div className="nm">
                          {f.name}
                          <span className={`rsvp-badge rsvp-${f.attending === "yes" ? "yes" : "no"}`}>{attendingShort(f.attending)}</span>
                        </div>
                        <div className="role">{f.role}</div>
                        {f.prior && <div className="prior">{f.prior}</div>}
                        {f.email && <span className="em">{f.email}</span>}
                      </div>
                      <div className="contact-actions">
                        <a className={`ico-btn ${f.email ? "" : "disabled"}`} href={f.email ? `mailto:${f.email}` : "#"} title="Email">
                          <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="3" width="11" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.2" /><path d="M2 4 L7 8 L12 4" stroke="currentColor" strokeWidth="1.2" fill="none" /></svg>
                        </a>
                        <a className={`ico-btn ${f.linkedin ? "" : "disabled"}`} href={f.linkedin ? `https://linkedin.com/in/${f.linkedin}` : "#"} target="_blank" rel="noopener noreferrer" title="LinkedIn">
                          <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M4.98 3.5C4.98 4.88 3.87 6 2.5 6S0 4.88 0 3.5 1.13 1 2.5 1s2.48 1.12 2.48 2.5zM.22 8h4.56v14H.22V8zm7.61 0h4.36v1.92h.06c.61-1.15 2.1-2.36 4.32-2.36 4.62 0 5.47 3.04 5.47 6.99V22h-4.56v-6.6c0-1.57-.03-3.6-2.19-3.6-2.2 0-2.54 1.71-2.54 3.48V22H7.83V8z" /></svg>
                        </a>
                        <a className={`ico-btn ${f.x ? "" : "disabled"}`} href={f.x ? `https://x.com/${f.x}` : "#"} target="_blank" rel="noopener noreferrer" title="X">
                          <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2H21l-6.51 7.44L22 22h-6.93l-4.74-6.2L4.8 22H2l7-8.01L1.76 2h7.07l4.3 5.69L18.24 2zm-1.21 18h1.83L7.07 4H5.13l11.91 16z" /></svg>
                        </a>
                      </div>
                    </div>
                )}
                </div>
                {company.teamAttendees && company.teamAttendees.length > 0 && (
                  <div className="team-attendees">
                    <div className="team-head">Other team members attending</div>
                    {company.teamAttendees.map((t, i) =>
                      <div className="team-attendee rsvp-yes" key={i}>
                        <PersonAvatar name={t.name} linkedin={t.linkedin} className="avatar small" />
                        <div className="ta-info">
                          <div className="nm">{t.name} <span className="rsvp-badge rsvp-yes">Attending</span></div>
                          {t.designation && <div className="role">{t.designation}</div>}
                          {t.email && <span className="em">{t.email}</span>}
                        </div>
                        <div className="contact-actions">
                          {t.email && <a className="ico-btn" href={`mailto:${t.email}`} title="Email"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="3" width="11" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.2" /><path d="M2 4 L7 8 L12 4" stroke="currentColor" strokeWidth="1.2" fill="none" /></svg></a>}
                          {t.linkedin && <a className="ico-btn" href={`https://linkedin.com/in/${t.linkedin}`} target="_blank" rel="noopener noreferrer" title="LinkedIn"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M4.98 3.5C4.98 4.88 3.87 6 2.5 6S0 4.88 0 3.5 1.13 1 2.5 1s2.48 1.12 2.48 2.5zM.22 8h4.56v14H.22V8zm7.61 0h4.36v1.92h.06c.61-1.15 2.1-2.36 4.32-2.36 4.62 0 5.47 3.04 5.47 6.99V22h-4.56v-6.6c0-1.57-.03-3.6-2.19-3.6-2.2 0-2.54 1.71-2.54 3.48V22H7.83V8z" /></svg></a>}
                        </div>
                      </div>
                    )}
                  </div>
                )}
              </div>

              {company.coInvestors.length > 0 &&
            <div className="detail-section">
                  <h4>Co-investors ({company.coInvestors.length})</h4>
                  <div className="coinv-list">
                    {company.coInvestors.map((ci, i) =>
                <span className="coinv" key={i}>{ci}</span>
                )}
                  </div>
                </div>
            }
            </div>
          </>
        }
      </aside>
    </>);

}

// ───────────────────── Bookmark icon button ─────────────────────
function BookmarkBtn({ active, onToggle, className = "" }) {
  return (
    <button
      type="button"
      className={`bookmark-btn ${active ? "on" : ""} ${className}`}
      aria-label={active ? "Remove bookmark" : "Bookmark"}
      title={active ? "Remove bookmark" : "Bookmark this company"}
      onClick={(e) => { e.stopPropagation(); onToggle(); }}>
      <svg width="14" height="14" viewBox="0 0 16 16" fill={active ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round">
        <path d="M3.5 2.5 H12.5 V13.5 L8 10.6 L3.5 13.5 Z" />
      </svg>
    </button>
  );
}

// ───────────────────── Memoized Row (skips re-render when company unchanged) ─────────────────────
const CompanyRow = React.memo(function CompanyRow({ c, onPick, bookmarked, onToggleBookmark }) {
  return (
    <div
      className={`row ${c.status === "ShutDown" ? "shutdown" : ""} ${isRecentNews(c.news) ? "has-news" : ""}`}
      onClick={() => onPick(c.id)}>
      <span className="news-halo" title="Recently in the news"></span>
      <Logo company={c} />
      <div className="row-name">
        <div className="nm">
          {c.name}
          <span className={`tag status ${c.status}`}>{statusLabel(c.status)}</span>
          {c.featured && (
            <span className="tag tag-new" title="Latest investment">
              <svg width="9" height="9" viewBox="0 0 14 14" fill="none" style={{ marginRight: 4 }}><path d="M7 1 L8.6 5.4 L13 7 L8.6 8.6 L7 13 L5.4 8.6 L1 7 L5.4 5.4 Z" fill="currentColor" /></svg>
              NEW
            </span>
          )}
        </div>
        <div className="qk">{c.quirk}</div>
      </div>
      <div className="row-meta-block col-sector">
        <span className="k">Sector</span>
        <span className="v">
          <span className="tag">
            <span className="swatch" style={{ background: (window.SECTOR_COLORS && window.SECTOR_COLORS[c.sector]) || "#777" }}></span>
            {c.sector}
          </span>
        </span>
      </div>
      <div className="row-meta-block col-founders">
        <span className="k">Founders / Attendees</span>
        <span className="v">{(() => {
          // Combine founders + team attendees, dedupe by name, prefer attending
          // people first so the most relevant names show up before truncation.
          const all = [...(c.founders || []), ...(c.teamAttendees || [])];
          const seen = new Set();
          const dedup = all.filter((p) => {
            if (!p?.name || seen.has(p.name)) return false;
            seen.add(p.name);
            return true;
          });
          dedup.sort((a, b) => (b.attending === "yes" ? 1 : 0) - (a.attending === "yes" ? 1 : 0));
          const firsts = dedup.map((p) => p.name.split(" ")[0]);
          const shown = firsts.slice(0, 3).join(", ");
          const extra = firsts.length > 3 ? ` +${firsts.length - 3}` : "";
          return shown + extra;
        })()}</span>
      </div>
      <div className="row-meta-block col-stage">
        <span className="k">Stage</span>
        <span className="v">{c.stage}</span>
      </div>
      <div className="row-meta-block col-bookmark">
        <BookmarkBtn active={!!bookmarked} onToggle={onToggleBookmark} />
      </div>
    </div>
  );
});

// ───────────────────── App ─────────────────────
function App() {
  // Hidden companies (e.g. Zomint) are excluded from the visible list and
  // the AI corpus by default. They surface only via an exact-name search
  // (the easter egg). Keep the full set on the side for that lookup.
  const allWithHidden = window.PORTFOLIO;
  const all = useMemo(() => allWithHidden.filter((c) => !c.hidden), [allWithHidden]);
  const [query, setQuery] = useState("");
  const [aiQuery, setAiQuery] = useState("");
  const [aiResults, setAiResults] = useState(null);
  const [aiLoading, setAiLoading] = useState(false);
  const [searchMode, setSearchMode] = useState("keyword"); // smart | keyword
  const [sectors, setSectors] = useState([]);
  const [stages, setStages] = useState([]);
  const [statuses, setStatuses] = useState([]);
  // invested + fundingBuckets removed
  const [coInvestors, setCoInvestors] = useState([]);
  const [recentOnly, setRecentOnly] = useState(false);
  const [bookmarkedOnly, setBookmarkedOnly] = useState(false);
  const bookmarks = useBookmarks();
  const [sort, setSort] = useState("shuffle");
  // Stable per-page-load shuffle. Recomputed only if the company set changes
  // (it doesn't), so the order stays consistent while the user filters/searches.
  const shuffleOrder = useMemo(() => {
    const ids = all.map((c) => c.id);
    for (let i = ids.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const t = ids[i]; ids[i] = ids[j]; ids[j] = t;
    }
    const map = {};
    ids.forEach((id, idx) => { map[id] = idx; });
    return map;
  }, [all]);
  // Initialize from URL (?c=companyId) synchronously so the URL-sync effect
  // doesn't strip the deeplink before we read it.
  const [activeId, setActiveId] = useState(() => {
    try {
      const t = new URLSearchParams(window.location.search).get("c");
      return t && window.PORTFOLIO.some((c) => c.id === t) ? t : null;
    } catch (_) { return null; }
  });
  const [askOpen, setAskOpen] = useState(false);
  const [askSeed, setAskSeed] = useState(null);
  const searchRef = useRef();
  const debounceRef = useRef();
  const isMobile = useIsMobile(760);

  // Cycling AI placeholder examples — typewriter effect (only smart mode + empty)
  const aiPlaceholders = useMemo(() => isMobile ? [
    "Who's attending?",
    "Ex-Google founders",
    "IIT alumni",
    "AI-native bets",
    "Fintech founders",
    "Ex-Flipkart founders",
    "Female founders",
    "Repeat founders",
    "Raised in 2025",
    "Pre-Series A bets",
    "IIT Bombay founders",
    "Ex-McKinsey founders",
    "Ex-Razorpay founders",
    "Cleantech bets",
    "B2B SaaS here tonight",
    "Second-time founders",
  ] : [
    "Who's attending tonight?",
    "Which founders worked at Flipkart?",
    "IIT alumni in the portfolio",
    "Ex-Google or Microsoft founders",
    "Show me the AI-native bets",
    "Fintech founders attending tonight",
    "Female founders in the portfolio",
    "Repeat founders we've backed",
    "Which companies raised in 2025?",
    "Stealth or pre-Series A bets",
    "Founders from IIT Bombay",
    "Ex-McKinsey or BCG founders",
    "Ex-Razorpay or Cred founders",
    "Climate or cleantech bets",
    "B2B SaaS founders here tonight",
    "Founders on their second startup",
  ], [isMobile]);

  const [typedPlaceholder, setTypedPlaceholder] = useState("");
  const [aiPhIdx, setAiPhIdx] = useState(0);

  useEffect(() => {
    if (searchMode !== "smart" || query) {
      setTypedPlaceholder("");
      return;
    }
    const phrase = aiPlaceholders[aiPhIdx % aiPlaceholders.length];
    let cancelled = false;
    let timer;
    const schedule = (fn, ms) => { timer = setTimeout(() => { if (!cancelled) fn(); }, ms); };
    const TYPING = 48;     // ms per char while typing
    const DELETING = 28;   // ms per char while deleting
    const HOLD_FULL = 1300; // pause when phrase fully typed
    const HOLD_EMPTY = 220; // pause before next phrase
    const type = (i) => {
      setTypedPlaceholder(phrase.slice(0, i));
      if (i < phrase.length) schedule(() => type(i + 1), TYPING);
      else schedule(() => del(phrase.length), HOLD_FULL);
    };
    const del = (i) => {
      setTypedPlaceholder(phrase.slice(0, i));
      if (i > 0) schedule(() => del(i - 1), DELETING);
      else schedule(() => setAiPhIdx((p) => (p + 1) % aiPlaceholders.length), HOLD_EMPTY);
    };
    type(0);
    return () => { cancelled = true; clearTimeout(timer); };
  }, [aiPhIdx, searchMode, query, aiPlaceholders]);

  // Lock body scroll while a fullscreen overlay (drawer/ask) is open — keeps
  // iOS Safari from rubber-banding the underlying page.
  useEffect(() => {
    const locked = !!activeId || askOpen;
    document.body.classList.toggle("no-scroll", locked);
    return () => document.body.classList.remove("no-scroll");
  }, [activeId, askOpen]);

  // Auto-hide the Ask FAB while the user is scrolling down so it stops
  // covering content; show again on scroll-up or after they pause.
  const [fabHidden, setFabHidden] = useState(false);
  useEffect(() => {
    let lastY = window.scrollY;
    let ticking = false;
    let restoreT;
    const onScroll = () => {
      const y = window.scrollY;
      if (!ticking) {
        requestAnimationFrame(() => {
          const dy = y - lastY;
          // Below 200px from the top, never hide
          if (y < 200) setFabHidden(false);
          else if (dy > 6) setFabHidden(true);
          else if (dy < -6) setFabHidden(false);
          lastY = y;
          ticking = false;
        });
        ticking = true;
      }
      // Reveal again after the user pauses scrolling
      clearTimeout(restoreT);
      restoreT = setTimeout(() => setFabHidden(false), 800);
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => {
      window.removeEventListener("scroll", onScroll);
      clearTimeout(restoreT);
    };
  }, []);

  // Click outside the search bar / AI results closes the dropdown
  useEffect(() => {
    if (!aiResults && !aiQuery && !aiLoading) return;
    const onClick = (e) => {
      const inSearch = e.target.closest(".header-search");
      const inResults = e.target.closest(".ai-results");
      if (!inSearch && !inResults) {
        setAiResults(null);
        setAiQuery("");
      }
    };
    document.addEventListener("mousedown", onClick);
    document.addEventListener("touchstart", onClick);
    return () => {
      document.removeEventListener("mousedown", onClick);
      document.removeEventListener("touchstart", onClick);
    };
  }, [aiResults, aiQuery, aiLoading]);


  // Smart-search: fires on Enter or after a long pause
  const runAISearch = useCallback(async (q) => {
    if (!q || q.length < 3) {setAiResults(null);setAiQuery("");return;}
    setAiQuery(q);
    setAiLoading(true);
    const res = await semanticSearch(q, all);
    setAiResults(res);
    setAiLoading(false);
  }, [all]);

  // Auto-trigger AI search disabled — AI mode now opens the unified Ask Panel
  // on Enter (debounced inline dropdown deprecated).
  useEffect(() => {
    setAiResults(null); setAiQuery("");
  }, [searchMode]);

  // Build option lists
  // Sectors are stored as "Top / Subtag" (e.g. "Fintech / Auto"). Collapse to
  // top-level for the filter so users see "Fintech" instead of 7 sub-buckets.
  const sectorTop = useCallback((s) => (s || "").split(" / ")[0].trim(), []);
  const sectorOpts = useMemo(() => [...new Set(all.map((c) => sectorTop(c.sector)))].sort(), [all, sectorTop]);
  // Stage = Early / Growth, plus a synthetic "New" option that filters by
  // the `featured` flag so users can quickly spotlight latest investments.
  const stageOpts = ["New", "Early", "Growth"];
  const statusOpts = ["Attending", "NotAttending"];

  const coInvOpts = useMemo(() => {
    const map = {};
    all.forEach((c) => c.coInvestors.forEach((ci) => {map[ci] = (map[ci] || 0) + 1;}));
    return Object.entries(map).sort((a, b) => b[1] - a[1]).map(([k]) => k);
  }, [all]);

  // Hoist each count to its own useMemo at component top-level (Rules of Hooks)
  const sectorCounts = useMemo(
    () => Object.fromEntries(sectorOpts.map((s) => [s, all.filter((c) => sectorTop(c.sector) === s).length])),
    [sectorOpts, all, sectorTop]);
  const stageCounts = useMemo(
    () => Object.fromEntries(stageOpts.map((s) => [
      s,
      s === "New" ? all.filter((c) => c.featured).length
                  : all.filter((c) => c.stage === s).length
    ])),
    [stageOpts, all]);
  const statusCounts = useMemo(
    () => Object.fromEntries(statusOpts.map((s) => [s, all.filter((c) => c.status === s).length])),
    [all]);
  const coInvCounts = useMemo(() => {
    const m = {};
    all.forEach((c) => (c.coInvestors || []).forEach((ci) => { m[ci] = (m[ci] || 0) + 1; }));
    return m;
  }, [all]);
  const counts = {
    sectors: sectorCounts,
    stages: stageCounts,
    statuses: statusCounts,
    coInv: coInvCounts,
  };

  const filtered = useMemo(() => {
    // Easter egg: if the keyword query is an exact prefix match for a hidden
    // company's name, surface it alongside visible results.
    let list = all;
    if (searchMode === "keyword" && query.trim()) {
      const q = query.trim().toLowerCase();
      const hiddenMatches = allWithHidden.filter(
        (c) => c.hidden && c.name.toLowerCase().startsWith(q)
      );
      if (hiddenMatches.length) list = [...list, ...hiddenMatches];
    }
    // Keyword fallback search
    if (searchMode === "keyword" && query.trim()) {
      const q = query.toLowerCase();
      const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
      // Strip whitespace + punctuation so "peakxv" matches "Peak XV Partners"
      // and "kunalvarma" matches "Kunal Varma".
      const qNorm = q.replace(/[\s.\-_/]+/g, "");
      const norm = (s) => (s || "").toLowerCase().replace(/[\s.\-_/]+/g, "");
      // Word-boundary regex: matches "Accel" but NOT "Accelerator"; "Peak" but
      // NOT "speak". Used for co-investor names where partial-token matches
      // produce noisy results.
      const wordRe = new RegExp("\\b" + escapeRegex(q) + "\\b", "i");
      // Prefix-from-word-start regex: matches "kun" → "Kunal", "ank" → "Ankit".
      const prefixRe = new RegExp("\\b" + escapeRegex(q), "i");
      // Normalized matchers (only kick in for longer queries to avoid noise)
      const useNorm = qNorm.length >= 4;
      // Stage shortcut: typing "early", "growth", or "new" exactly = stage-only filter
      // (skip the broader keyword matchers so "new" doesn't also catch "new campus" etc.)
      const stageQuery = q === "new" ? "New" : q === "early" ? "Early" : q === "growth" ? "Growth" : null;
      if (stageQuery) {
        list = list.filter((c) => stageQuery === "New" ? !!c.featured : c.stage === stageQuery);
      } else {
        list = list.filter((c) =>
          // Company name + sector use plain substring (short, controlled fields)
          c.name.toLowerCase().includes(q) ||
          (useNorm && norm(c.name).includes(qNorm)) ||
          c.sector.toLowerCase().includes(q) ||
          // Description + quirk use word-boundary prefix to avoid noise
          prefixRe.test(c.description) ||
          prefixRe.test(c.quirk) ||
          // Founder + team names: word-boundary prefix (lets "kun" match "Kunal")
          c.founders.some((f) => prefixRe.test(f.name)) ||
          (useNorm && c.founders.some((f) => norm(f.name).includes(qNorm))) ||
          (c.teamAttendees || []).some((t) => prefixRe.test(t.name || "")) ||
          (useNorm && (c.teamAttendees || []).some((t) => norm(t.name).includes(qNorm))) ||
          // Co-investors: full word-boundary on both sides
          c.coInvestors.some((ci) => wordRe.test(ci)) ||
          (useNorm && c.coInvestors.some((ci) => norm(ci).startsWith(qNorm)))
        );
      }
    }
    if (sectors.length) list = list.filter((c) => sectors.includes(sectorTop(c.sector)));
    if (stages.length) {
      list = list.filter((c) => stages.some((s) =>
        s === "New" ? !!c.featured : c.stage === s
      ));
    }
    if (statuses.length) list = list.filter((c) => statuses.includes(c.status));
    if (coInvestors.length) list = list.filter((c) => coInvestors.some((ci) => c.coInvestors.includes(ci)));
    if (recentOnly) list = list.filter((c) => isRecentNews(c.news));
    if (bookmarkedOnly) list = list.filter((c) => bookmarks.has(c.id));

    const sorted = [...list];
    const yr = (c) => Number(c.invested) || 0;
    if (sort === "shuffle") sorted.sort((a, b) => (shuffleOrder[a.id] ?? 0) - (shuffleOrder[b.id] ?? 0));else
    if (sort === "alpha") sorted.sort((a, b) => a.name.localeCompare(b.name));else
    if (sort === "newest") sorted.sort((a, b) => yr(b) - yr(a) || a.name.localeCompare(b.name));else
    if (sort === "oldest") sorted.sort((a, b) => yr(a) - yr(b) || a.name.localeCompare(b.name));else
    if (sort === "attending") sorted.sort((a, b) => {
      const order = (s) => s === "Attending" ? 0 : 1;
      return order(a.status) - order(b.status) || a.name.localeCompare(b.name);
    });else
    if (sort === "sector") sorted.sort((a, b) => a.sector.localeCompare(b.sector) || a.name.localeCompare(b.name));
    return sorted;
  }, [all, query, searchMode, sectors, stages, statuses, coInvestors, recentOnly, bookmarkedOnly, bookmarks.set, sort, shuffleOrder]);

  // Stats: react to the filtered set so the numbers stay coherent with the
  // visible list. When no filter is active, this equals the portfolio total.
  const hasAnyFilter = !!query.trim() || sectors.length > 0 || stages.length > 0 ||
                       statuses.length > 0 || coInvestors.length > 0 || recentOnly || bookmarkedOnly;
  const stats = useMemo(() => {
    const set = hasAnyFilter ? filtered : all;
    const total = set.length;
    const sectorCount = new Set(set.map((c) => (c.sector || "").split(" / ")[0])).size;
    const founders = set.reduce((sum, c) => sum + c.founders.length, 0);
    return { total, sectors: sectorCount, founders };
  }, [filtered, all, hasAnyFilter]);

  // Look up the active company in the full set (incl. hidden) so featured
  // hidden companies can still open their drawer once surfaced.
  const activeCompany = activeId ? allWithHidden.find((c) => c.id === activeId) : null;

  // Deeplink + tab-title sync: keep the URL AND document.title in sync with
  // the open drawer. The title sync is what makes Cmd+D / Ctrl+D bookmarks
  // pre-fill with the company name instead of the generic site title.
  const SITE_TITLE = "Prime Portfolio — Companies & Founders";
  useEffect(() => {
    const url = new URL(window.location.href);
    if (activeId) {
      const c = allWithHidden.find((x) => x.id === activeId);
      if (url.searchParams.get("c") !== activeId) {
        url.searchParams.set("c", activeId);
        window.history.replaceState({}, "", url.toString());
      }
      if (c) document.title = `${c.name} — Prime Venture Partners`;
    } else {
      if (url.searchParams.has("c")) {
        url.searchParams.delete("c");
        window.history.replaceState({}, "", url.toString());
      }
      document.title = SITE_TITLE;
    }
  }, [activeId, allWithHidden]);

  // Browser back/forward navigation respects ?c=... deeplinks
  useEffect(() => {
    const onPop = () => {
      const t = new URLSearchParams(window.location.search).get("c");
      setActiveId(t && allWithHidden.find((c) => c.id === t) ? t : null);
    };
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);
  const activeIdx = activeCompany ? filtered.findIndex((c) => c.id === activeId) : -1;

  const surprise = () => {
    const pool = filtered.length > 0 ? filtered : all;
    const c = pool[Math.floor(Math.random() * pool.length)];
    setActiveId(c.id);
  };

  const clearAll = () => {
    setQuery("");setSectors([]);setStages([]);setStatuses([]);
    setCoInvestors([]);setRecentOnly(false);
    setAiResults(null);setAiQuery("");
  };

  const activeFilterCount =
    sectors.length + stages.length + statuses.length +
    coInvestors.length +
    (recentOnly ? 1 : 0);
  const hasFilter = !!query || activeFilterCount > 0;
  const showAIResults = searchMode === "smart" && (aiLoading || aiResults && aiQuery);

  return (
    <>
      <header className="app-header">
        <div className="header-inner">
          <a
            href="https://www.primevp.in"
            target="_blank"
            rel="noopener noreferrer"
            className="brand"
            onClick={(e) => {
              // If page is scrolled, intercept the tap and scroll to top first.
              // A second tap (now at top) follows the link as normal.
              if (window.scrollY > 80) {
                e.preventDefault();
                window.scrollTo({ top: 0, behavior: "smooth" });
              }
            }}>
            <div className="brand-mark">
              <PrimeLogo />
            </div>
            <div className="brand-text">
              <span className="b1">Prime Portfolio</span>
              <span className="b2">Founders &amp; Companies, A–Z</span>
            </div>
          </a>
          <div className={`header-search mode-${searchMode}`}>
            <svg className="search-icon" viewBox="0 0 16 16" fill="none">
              <circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.4" />
              <path d="M11 11 L14 14" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
            </svg>
            <input
              ref={searchRef}
              type="text"
              placeholder={
                searchMode === "smart"
                  ? (typedPlaceholder + (typedPlaceholder ? "▌" : ""))
                  : (isMobile ? "Search company or founder…" : "Search company, founder, sector…")
              }
              value={query}
              onChange={(e) => {
                const v = e.target.value;
                setQuery(v);
                // Clear active filters as soon as the user starts typing —
                // the portfolio is small enough that the search alone narrows
                // sufficiently, and combining the two often gives a confusing
                // empty/"X of 0" state.
                if (v.trim() && (sectors.length || stages.length || statuses.length || coInvestors.length || recentOnly)) {
                  setSectors([]); setStages([]); setStatuses([]); setCoInvestors([]); setRecentOnly(false); setBookmarkedOnly(false);
                }
              }}
              onKeyDown={(e) => {
                if (e.key === "Enter" && searchMode === "smart") {
                  e.preventDefault();
                  const q = query.trim();
                  if (q.length >= 2) {
                    setAskSeed(q);
                    setAskOpen(true);
                    setQuery("");
                    e.target.blur();
                  }
                }
                if (e.key === "Escape") {
                  setQuery("");
                  setAiResults(null);
                  setAiQuery("");
                  e.target.blur();
                }
              }} />
            {(query || aiQuery) && (
              <button
                className="search-clear"
                onClick={() => {
                  setQuery("");
                  setAiResults(null);
                  setAiQuery("");
                  searchRef.current && searchRef.current.blur();
                }}
                aria-label="Clear search">
                <svg width="12" height="12" viewBox="0 0 14 14"><path d="M2 2 L12 12 M12 2 L2 12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /></svg>
              </button>
            )}
            <div
              className="search-mode toggle"
              data-mode={searchMode}
              role="switch"
              aria-checked={searchMode === "smart"}
              tabIndex={0}
              title="Tap anywhere to switch between AI and Text search"
              onClick={() => setSearchMode((m) => m === "smart" ? "keyword" : "smart")}
              onKeyDown={(e) => {
                if (e.key === " " || e.key === "Enter") {
                  e.preventDefault();
                  setSearchMode((m) => m === "smart" ? "keyword" : "smart");
                }
              }}>
              <span className="toggle-thumb" aria-hidden="true"></span>
              <span className={`mode-pill mode-ai ${searchMode === "smart" ? "on" : ""}`}>
                <span className="ai-spark" aria-hidden="true"></span>
                <svg width="11" height="11" viewBox="0 0 14 14"><path d="M7 1 L8.6 5.4 L13 7 L8.6 8.6 L7 13 L5.4 8.6 L1 7 L5.4 5.4 Z" fill="currentColor" /></svg>
                AI
              </span>
              <span className={`mode-pill ${searchMode === "keyword" ? "on" : ""}`}>Text</span>
            </div>
            {showAIResults &&
            <AISearchResults
              results={aiResults}
              companies={all}
              loading={aiLoading}
              query={aiQuery}
              onPick={(id) => {setActiveId(id);setAiResults(null);setAiQuery("");}}
              onDismiss={() => {setAiResults(null);setAiQuery("");}} />

            }
          </div>
          <div className="header-actions">
            <button className="btn-orange ask-btn" onClick={() => setAskOpen(true)}>
              <svg width="11" height="11" viewBox="0 0 14 14"><path d="M7 1 L8.6 5.4 L13 7 L8.6 8.6 L7 13 L5.4 8.6 L1 7 L5.4 5.4 Z" fill="currentColor" /></svg>
              Ask
            </button>
          </div>
        </div>
      </header>

      <section className="hero">
        <h1>Meet the companies we're <em>privileged to partner</em> with.</h1>
        <p className="hero-sub">{isMobile
          ? "Every company in the Prime portfolio, every founder you can meet tonight."
          : "Every company in the Prime portfolio, every founder you can meet tonight. Browse, search, or ask the AI to surface a contact before the next coffee."}


        </p>
        <div className="stat-grid">
          <div className="stat"><div className="num">{stats.total}</div><div className="lbl">Companies</div></div>
          <div className="stat"><div className="num">{stats.founders}</div><div className="lbl">Founders</div></div>
          <div className="stat"><div className="num">{stats.sectors}</div><div className="lbl">Sectors</div></div>
          <div className="stat"><div className="num">2011</div><div className="lbl">Backing since</div></div>
        </div>
      </section>

      <NewsTicker companies={all} onClickCompany={(id) => setActiveId(id)} />

      <section className="filters">
        <span className="label">
          Filter
          {activeFilterCount > 0 && <span className="filter-badge">{activeFilterCount}</span>}
        </span>
        <ChipMulti label="Sector" options={sectorOpts} selected={sectors} onChange={setSectors} counts={counts.sectors} />
        <ChipMulti label="Stage" options={stageOpts} selected={stages} onChange={setStages} counts={counts.stages} />
        <ChipMulti label="Attending" options={statusOpts} selected={statuses} onChange={setStatuses} formatOption={statusLabel} />
        {/* Invested filter removed per user */}
        {/* Funding filter hidden from UI per request — AI agent still uses funding data */}
        <ChipMulti label="Co-investor" options={coInvOpts} selected={coInvestors} onChange={setCoInvestors} counts={counts.coInv} />
        <button className={`chip ${recentOnly ? "active" : ""}`} onClick={() => setRecentOnly((r) => !r)}>
          <span className="dot-orange"></span>
          In the news
        </button>
        <button
          className={`chip chip-bookmarks ${bookmarkedOnly ? "active" : ""}`}
          onClick={() => setBookmarkedOnly((b) => !b)}
          title="Show only the companies you've bookmarked">
          <svg width="11" height="11" viewBox="0 0 16 16" fill={bookmarkedOnly ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round">
            <path d="M3.5 2.5 H12.5 V13.5 L8 10.6 L3.5 13.5 Z" />
          </svg>
          Bookmarks{bookmarks.count > 0 ? ` · ${bookmarks.count}` : ""}
        </button>
        <div className="filters-right">
          <ChipSingle
            label="Sort"
            selected={sort}
            onChange={setSort}
            options={[
              { value: "shuffle", label: "Shuffle (default)" },
              { value: "alpha", label: "A–Z" },
              { value: "newest", label: "Newest investment" },
              { value: "oldest", label: "Oldest investment" },
              { value: "attending", label: "Attending first" },
              { value: "sector", label: "Sector" },
            ]} />
        </div>
      </section>

      <div className="result-meta">
        <span>
          {hasFilter
            ? <><strong>{filtered.length}</strong> of {all.length} companies</>
            : <><strong>{all.length}</strong> companies</>}
        </span>
        {hasFilter && <button className="clear-all" onClick={clearAll}>Clear all filters</button>}
      </div>

      <main className="list">
        {filtered.length === 0 ?
        <div className="empty">
            <h3>No matches.</h3>
            <p>Try removing a filter or switching to AI search above.</p>
          </div> :

        filtered.map((c) => (
          <CompanyRow
            key={c.id}
            c={c}
            onPick={setActiveId}
            bookmarked={bookmarks.has(c.id)}
            onToggleBookmark={() => bookmarks.toggle(c.id, c.name)} />
        ))
        }
      </main>

      {/* Desktop: Surprise me — random company picker */}
      <button className="surprise-fab desktop-only" onClick={surprise} title="Pick a random portfolio company">
        <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
          <rect x="1.5" y="1.5" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="1.4" />
          <circle cx="4.5" cy="4.5" r="0.9" fill="currentColor" />
          <circle cx="9.5" cy="4.5" r="0.9" fill="currentColor" />
          <circle cx="7" cy="7" r="0.9" fill="currentColor" />
          <circle cx="4.5" cy="9.5" r="0.9" fill="currentColor" />
          <circle cx="9.5" cy="9.5" r="0.9" fill="currentColor" />
        </svg>
        Surprise me
      </button>

      {/* Persistent bottom-left chat-style FAB — hides on scroll-down */}
      <button
        className={`ask-fab ${fabHidden ? "hidden" : ""}`}
        onClick={() => setAskOpen(true)}
        title="Ask the portfolio AI">
        <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
          <path d="M7 1 L8.6 5.4 L13 7 L8.6 8.6 L7 13 L5.4 8.6 L1 7 L5.4 5.4 Z" fill="currentColor" />
        </svg>
        <span className="ask-fab-label">Ask AI</span>
      </button>

      <Drawer
        company={activeCompany}
        bookmarked={activeCompany ? bookmarks.has(activeCompany.id) : false}
        onToggleBookmark={() => activeCompany && bookmarks.toggle(activeCompany.id, activeCompany.name)}
        onClose={() => setActiveId(null)}
        onPrev={() => {
          if (activeIdx > 0) setActiveId(filtered[activeIdx - 1].id);
        }}
        onNext={() => {
          if (activeIdx >= 0 && activeIdx < filtered.length - 1) setActiveId(filtered[activeIdx + 1].id);
        }} />
      

      {askOpen &&
      <AskPanel
        companies={allWithHidden}
        initialQuery={askSeed}
        onClose={() => { setAskOpen(false); setAskSeed(null); }}
        onPickCompany={(id) => setActiveId(id)} />

      }
    </>);

}

// ErrorBoundary so a runtime error doesn't leave the user staring at a blank page.
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { err: null }; }
  static getDerivedStateFromError(err) { return { err }; }
  componentDidCatch(err, info) { console.error("App crashed:", err, info); }
  render() {
    if (this.state.err) {
      return (
        <div style={{ padding: "60px 24px", maxWidth: 560, margin: "0 auto", fontFamily: "Geist, sans-serif" }}>
          <h2 style={{ fontFamily: "Fraunces, serif", fontSize: 24, marginBottom: 12 }}>Something went sideways.</h2>
          <p style={{ color: "#6B6258", lineHeight: 1.5, marginBottom: 16 }}>
            The portfolio directory hit an unexpected error. The team has been notified — please refresh in a moment.
          </p>
          <pre style={{ background: "#F1EBDF", padding: 12, borderRadius: 8, fontSize: 12, overflow: "auto", color: "#6B6258" }}>
            {String(this.state.err && this.state.err.message || this.state.err)}
          </pre>
          <button onClick={() => window.location.reload()} style={{ marginTop: 16, padding: "10px 18px", borderRadius: 999, border: "none", background: "#FF5A1F", color: "#fff", fontWeight: 600, cursor: "pointer" }}>
            Reload
          </button>
        </div>);
    }
    return this.props.children;
  }
}

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