/* =========================================================================
   Veto · Overlays — search everywhere + help launcher (intercom)
   Mounted once by AppShell. Communicate via window events:
     veto:cmdk  → open search
     veto:help  → open help launcher

   Search is built for escrow officers, not engineers: you search the things
   that live in a file — money movements, payoffs, holds, receipts, parties —
   in plain language ("disbursements over 500k", "Tanaka payoff"). Category
   tabs scope the search; an empty query browses suggestions and recents.
   ========================================================================= */

/* status → tone color, matching the rest of the app's grammar */
function statusTone(status) {
  if (status === "Recorded" || status === "Ready to sign" || status === "Signed" || status === "Cleared") return "ok";
  if (status === "In review" || status === "Awaiting evidence" || status === "Open" || status === "In cure") return "warn";
  return "muted";
}
const TONE_TEXT = { ok: "text-[var(--ok)]", warn: "text-[var(--warn)]", muted: "text-ink-soft" };
const TONE_DOT = { ok: "bg-[var(--ok)]", warn: "bg-[var(--warn)]", muted: "bg-[var(--ink-soft)]/40" };

/* Build the full searchable index from the live escrow fixtures. */
function buildSearchIndex() {
  const items = [];
  const push = (it) => { it.run = it.run || (() => navigate(it.to, it.params)); items.push(it); };

  // ---- Pages: navigation + actions an officer runs ----
  const pages = [
    { label: "Open a new file", icon: "new-file", hint: "⌘N", to: "/files/new", kw: "create start new file open" },
    { label: "All files", icon: "file-text", hint: "G F", to: "/files", kw: "files list browse" },
    { label: "Tasks", icon: "check-circle", hint: "G T", to: "/tasks", kw: "tasks action queue needs you to do desk policy generated work" },
    { label: "Funds", icon: "banknote", hint: "G M", to: "/funds", kw: "funds money control center funding payoffs proceeds release accounts matching overview" },
    { label: "Funding In", icon: "buyer-funding", hint: "G B", to: "/funds/funding-in", kw: "buyer funding money in queue review source of funds deposit" },
    { label: "Payoffs", icon: "payoff-demand", hint: "G P", to: "/funds/payoffs", kw: "payoff demand lien queue review beneficiary obligations out" },
    { label: "Proceeds", icon: "seller-proceeds", hint: "G S", to: "/funds/proceeds", kw: "seller proceeds entitlement money out queue review" },
    { label: "Release Requests", icon: "arrow-right", hint: "G R", to: "/funds/release-requests", kw: "release request disbursement outgoing money gate wire" },
    { label: "Accounts", icon: "trust-account", hint: "G A", to: "/funds/accounts", kw: "accounts trust destination funding source bank" },
    { label: "Matching", icon: "check-circle", to: "/funds/matching", kw: "matching reconciliation post action outflow match wire" },
    { label: "Records", icon: "file-text", hint: "G C", to: "/records", kw: "records proof artifacts review change exception hold immutable" },
    { label: "Start a seller proceeds review", icon: "plus", to: "/seller-proceeds/new", kw: "new seller proceeds review start" },
    { label: "Start a buyer funding review", icon: "plus", to: "/buyer-funding/new", kw: "new buyer funding review start" },
    { label: "Start a payoff demand review", icon: "plus", to: "/payoff/new", kw: "new payoff demand review start" },
    { label: "Create v2 Seller Proceeds Record", icon: "file-text", to: "/tasks", kw: "seller proceeds destination change create v2 record SP-0214 stale blocked" },
    { label: "Open SP-0214 destination change", icon: "alert-circle", to: "/funds/changes", kw: "SP-0214 destination change material change after reliance seller proceeds" },
    { label: "Request manager exception", icon: "shield", to: "/tasks", kw: "manager exception seller proceeds limitation SP-0214" },
    { label: "Open seller proceeds destination policy", icon: "scale", to: "/settings/policy", kw: "policy control seller proceeds destination v2 record POL-SP-DEST" },
    { label: "Open current Seller Proceeds Record", icon: "file-text", to: "/file/$id/change-impact", params: { id: "SP-0214" }, kw: "current seller proceeds record stale v1 draft v2 SP-0214" },
    { label: "Alerts", icon: "bell", hint: "G N", to: "/notifications", kw: "notifications alerts signals" },
    { label: "Settings", icon: "settings", hint: "⌘,", to: "/settings", kw: "settings preferences office policy" },
  ];
  pages.forEach((p, i) => push({ id: "page-" + i, type: "page", label: p.label, icon: p.icon, hint: p.hint, to: p.to, params: p.params, keywords: p.kw }));

  // ---- Files: keyed by property address ----
  allFilesRows().forEach((r) => push({
    id: "file-" + r.id, type: "file", fileId: r.id,
    label: r.street, sub: `${r.id} · ${r.party}`,
    icon: "file-text", status: r.status, closing: r.closing,
    to: "/file/$id", params: { id: r.id },
    keywords: `${r.id} ${r.party} ${r.street} ${r.city} ${r.lineLabel} ${r.status} closing ${r.closing}`,
  }));

  // ---- Money movements: deposits (in) + disbursements (out) ----
  Object.keys(CASE_FILES).forEach((id) => {
    const cf = CASE_FILES[id];
    const street = streetOf(cf.property);
    const mv = fileMovements(id);
    const all = [
      ...mv.deposits.map((m) => ({ ...m, dir: "in" })),
      ...mv.disbursements.map((m) => ({ ...m, dir: "out" })),
    ];
    all.forEach((m) => {
      const isPayoff = m.line === "payoff";
      const rec = {
        id: `mv-${id}-${m.id}`, type: isPayoff ? "payoff" : "movement", fileId: id,
        label: m.label, sub: `${street} · ${m.party}`,
        icon: isPayoff ? "scale" : m.dir === "in" ? "arrow-down-left" : "arrow-up-right",
        amount: m.amount, dir: m.dir, status: m.status, line: m.line,
        to: "/file/$id", params: { id },
        keywords: `${m.label} ${m.party} ${street} ${id} ${m.line || ""} ${m.status} ${m.dir === "in" ? "deposit money in incoming received" : "disbursement payment money out outgoing wire"} ${isPayoff ? "payoff demand lien lender beneficiary servicer" : ""}`,
      };
      push(rec);
    });
  });

  // ---- Holds: open blockers to cure ----
  Object.keys(CASE_FILES).forEach((id) => {
    const r = getReadiness(id);
    if (!r) return;
    const cf = CASE_FILES[id];
    const street = streetOf(cf.property);
    r.holds.forEach((h) => push({
      id: "hold-" + h.id, type: "hold", fileId: id,
      label: h.cure, sub: `${h.id} · ${street} · ${LINE_LABEL[h.line]}`,
      icon: "alert-triangle", status: h.state, owner: h.owner,
      to: "/file/$id", params: { id },
      keywords: `${h.cure} ${h.id} ${street} ${LINE_LABEL[h.line]} ${h.owner} hold blocker cure ${h.state}`,
    }));
  });

  // ---- Receipts: signed line receipts ----
  Object.values(CASE_FILES).forEach((cf) => {
    Object.entries(cf.lines).forEach(([key, l]) => {
      if (l.status !== "Signed" || !l.receiptHref) return;
      const street = streetOf(cf.property);
      push({
        id: `rcpt-${cf.id}-${key}`, type: "receipt", fileId: cf.id,
        label: `${LINE_LABEL[key]} receipt`, sub: `${street} · ${cf.party}`,
        icon: "shield-check", when: l.when,
        to: l.receiptHref, params: { id: cf.id },
        keywords: `${LINE_LABEL[key]} receipt ${street} ${cf.party} ${cf.id} signed recorded ${l.when}`,
      });
    });
  });

  // ---- Parties: principals on files + the office's reviewers ----
  const seenParty = new Set();
  Object.values(CASE_FILES).forEach((cf) => {
    const k = cf.party.toLowerCase();
    if (seenParty.has(k)) return;
    seenParty.add(k);
    const files = Object.values(CASE_FILES).filter((x) => x.party === cf.party);
    const fid = files[0].id;
    push({
      id: "party-" + k.replace(/\s+/g, "-"), type: "party",
      label: cf.party, sub: files.length > 1 ? `Principal · ${files.length} files` : `Principal · ${streetOf(files[0].property)}`,
      icon: "user", to: "/file/$id", params: { id: fid },
      keywords: `${cf.party} principal seller buyer party ${files.map((f) => f.id).join(" ")}`,
    });
  });
  Object.values(REVIEWERS).forEach((rv) => push({
    id: "rev-" + rv.initials, type: "party",
    label: rv.name, sub: rv.roleLabel,
    icon: "shield", to: "/reviewer/$initials", params: { initials: rv.initials },
    keywords: `${rv.name} ${rv.roleLabel} ${rv.office} reviewer officer escrow staff team`,
  }));

  // counterparties: lenders, servicers, beneficiaries an officer calls back
  const CP_ROLE = { payoff: "Lienholder", buyer: "Funding source" };
  const cpMap = {};
  Object.keys(CASE_FILES).forEach((id) => {
    const cf = CASE_FILES[id];
    const mv = fileMovements(id);
    [...mv.deposits, ...mv.disbursements].forEach((m) => {
      const role = CP_ROLE[m.line];
      if (!role || m.party === cf.party) return;          // skip principals' own legs
      const name = m.party.split("\u00b7")[0].trim();
      if (!name || /^(servicer of record|brokers|listing)/i.test(name)) return;
      const key = name.toLowerCase();
      (cpMap[key] = cpMap[key] || { name, role, files: new Set() }).files.add(id);
    });
  });
  Object.entries(cpMap).forEach(([key, v]) => {
    const fid = [...v.files][0];
    const n = v.files.size;
    push({
      id: "cp-" + key.replace(/[^a-z0-9]+/g, "-"), type: "party",
      label: v.name, sub: n > 1 ? `${v.role} \u00b7 ${n} files` : `${v.role} \u00b7 ${streetOf(CASE_FILES[fid].property)}`,
      icon: v.role === "Lienholder" ? "landmark" : "building",
      to: "/file/$id", params: { id: fid },
      keywords: `${v.name} ${v.role} lender servicer beneficiary lienholder payoff counterparty ${[...v.files].join(" ")}`,
    });
  });

  return items;
}

/* Parse a plain-language amount filter: "over 500k", "under 1.5m", "> 250000". */
function parseAmountFilter(q) {
  const m = q.match(/(over|above|greater than|more than|>|under|below|less than|<)\s*\$?\s*([\d][\d,.]*)\s*([kmb])?/i);
  if (!m) return null;
  const op = /over|above|greater|more|>/i.test(m[1]) ? ">" : "<";
  let n = parseFloat(m[2].replace(/,/g, ""));
  const unit = (m[3] || "").toLowerCase();
  if (unit === "k") n *= 1e3; else if (unit === "m") n *= 1e6; else if (unit === "b") n *= 1e9;
  if (!isFinite(n)) return null;
  return { op, value: n, raw: m[0] };
}

/* substring match that tolerates singular/plural (disbursement(s), payment(s)). */
function tokenMatches(hay, t) {
  if (hay.includes(t)) return true;
  if (t.length > 3 && t.endsWith("s") && hay.includes(t.slice(0, -1))) return true;
  if (t.length > 2 && hay.includes(t + "s")) return true;
  return false;
}

function scoreItem(item, tokens) {
  const hay = (item.label + " " + (item.sub || "") + " " + (item.keywords || "")).toLowerCase();
  const lbl = item.label.toLowerCase();
  let score = 0;
  for (const t of tokens) {
    if (!tokenMatches(hay, t)) return -1;
    if (lbl.startsWith(t)) score += 4;
    else if (lbl.includes(t)) score += 2;
    else score += 1;
  }
  return score;
}

/* Words that signal a money direction rather than a text match. */
const INTENT_DIR = {
  disbursement: "out", disbursements: "out", payment: "out", payments: "out",
  wire: "out", "out": "out", outgoing: "out", paid: "out",
  deposit: "in", deposits: "in", incoming: "in", "in": "in", received: "in",
};

const SEARCH_CATS = [
  { key: "all", label: "All" },
  { key: "file", label: "Files" },
  { key: "movement", label: "Movements" },
  { key: "payoff", label: "Payoffs" },
  { key: "hold", label: "Holds" },
  { key: "receipt", label: "Receipts" },
  { key: "party", label: "Parties" },
  { key: "page", label: "Pages" },
];
const GROUP_ORDER = ["page", "file", "movement", "payoff", "hold", "receipt", "party"];
const GROUP_LABEL = { page: "Pages", file: "Files", movement: "Money movements", payoff: "Payoffs", hold: "Holds", receipt: "Receipts", party: "Parties" };

const SEARCH_PLACEHOLDERS = [
  "Disbursements over 500k",
  "Tanaka payoff demand",
  "Open holds on Westcliff",
  "Priya Mehta",
  "Seller proceeds receipts",
];

/* Inject the entrance keyframes once. Kept short and gentle on purpose. */
function ensureSearchAnim() {
  if (document.getElementById("veto-search-anim")) return;
  const s = document.createElement("style");
  s.id = "veto-search-anim";
  s.textContent = ":root{--veto-row-hover:oklch(0.905 0.006 270)}@keyframes vetoScrim{from{opacity:0}to{opacity:1}}@keyframes vetoPop{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}@media (prefers-reduced-motion:reduce){[data-veto-pop]{animation:none!important}}";
  document.head.appendChild(s);
}

function CommandPalette() {
  const [open, setOpen] = useState(false);
  const [q, setQ] = useState("");
  const [cat, setCat] = useState("all");
  const [sel, setSel] = useState(0);
  const [ph, setPh] = useState(0);
  const [tabEdge, setTabEdge] = useState({ left: false, right: true });
  const inputRef = useRef(null);
  const tabsRef = useRef(null);
  const index = useMemo(() => buildSearchIndex(), []);

  // group results into the ordered, flat list the keyboard navigates
  const { groups, flat, isBrowse } = useMemo(() => {
    const raw = q.trim().toLowerCase();
    const amt = parseAmountFilter(raw);
    const inCat = (i) => cat === "all" || i.type === cat;

    let pool;
    let browse = false;
    if (amt) {
      const rest = raw.replace(amt.raw, "").trim();
      let words = rest ? rest.split(/\s+/) : [];
      // an amount query is direction/intent-led: "disbursements"/"payments" → out, "deposits" → in.
      let dir = null;
      words = words.filter((w) => {
        if (INTENT_DIR[w]) { dir = INTENT_DIR[w]; return false; }
        return true;
      });
      // remaining words are a soft text hint (name/address); drop any that match nothing
      pool = index.filter((i) => (i.type === "movement" || i.type === "payoff") && i.amount != null
        && (amt.op === ">" ? i.amount > amt.value : i.amount < amt.value) && inCat(i)
        && (dir == null || i.dir === dir)
        && (words.length === 0 || scoreItem(i, words) >= 0));
      // if a name hint zeroed everything, fall back to amount(+direction) only
      if (pool.length === 0 && words.length > 0) {
        pool = index.filter((i) => (i.type === "movement" || i.type === "payoff") && i.amount != null
          && (amt.op === ">" ? i.amount > amt.value : i.amount < amt.value) && inCat(i)
          && (dir == null || i.dir === dir));
      }
      pool = pool.slice().sort((a, b) => b.amount - a.amount);
    } else if (!raw) {
      browse = true;
      pool = cat === "all" ? [] : index.filter((i) => i.type === cat);
    } else {
      const tokens = raw.split(/\s+/);
      pool = index.map((i) => ({ i, s: inCat(i) ? scoreItem(i, tokens) : -1 }))
        .filter((x) => x.s >= 0).sort((a, b) => b.s - a.s).map((x) => x.i);
    }

    // order into groups for display
    const byType = {};
    pool.forEach((i) => { (byType[i.type] = byType[i.type] || []).push(i); });
    const gs = [];
    const flatArr = [];
    GROUP_ORDER.forEach((t) => {
      if (!byType[t] || !byType[t].length) return;
      const items = byType[t].slice(0, 6);
      gs.push({ type: t, label: GROUP_LABEL[t], items });
      items.forEach((it) => flatArr.push(it));
    });
    return { groups: gs, flat: flatArr, isBrowse: browse && cat === "all" };
  }, [q, cat, index]);

  const holdCount = useMemo(() => index.filter((i) => i.type === "hold").length, [index]);

  // a single, curated jump list shown when the query is empty (recents already live in the sidebar)
  const suggestions = useMemo(() => ([
    { id: "sg-week", icon: "calendar2", label: "Files closing this week", rightTop: "View all", run: () => navigate("/files") },
    { id: "sg-holds", icon: "alert-triangle", label: "Open holds to cure", rightTop: String(holdCount), run: () => navigate("/notifications") },
    { id: "sg-payoff", icon: "scale", label: "Payoff demand queue", run: () => navigate("/payoff") },
    { id: "sg-new", icon: "plus", label: "Open a new file", run: () => navigate("/files/new") },
  ]), [holdCount]);
  const browseFlat = isBrowse ? suggestions : flat;

  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setOpen((o) => !o); }
      else if (e.key === "Escape") setOpen(false);
    };
    const onOpen = () => setOpen(true);
    window.addEventListener("keydown", onKey);
    window.addEventListener("veto:cmdk", onOpen);
    return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("veto:cmdk", onOpen); };
  }, []);
  useEffect(() => { if (open) { ensureSearchAnim(); setQ(""); setCat("all"); setSel(0); setPh(0); setTimeout(() => inputRef.current?.focus(), 30); } }, [open]);
  useEffect(() => { setSel(0); }, [q, cat]);
  // slow, gentle rotation of ghost-suggestion placeholders while the field is empty
  useEffect(() => {
    if (!open || q) return;
    const t = setInterval(() => setPh((p) => (p + 1) % SEARCH_PLACEHOLDERS.length), 4600);
    return () => clearInterval(t);
  }, [open, q]);
  // measure how far the category strip can scroll, so the edge arrows only show when useful
  const measureTabs = useCallback(() => {
    const el = tabsRef.current; if (!el) return;
    setTabEdge({ left: el.scrollLeft > 4, right: el.scrollLeft + el.clientWidth < el.scrollWidth - 4 });
  }, []);
  useEffect(() => { if (open) { const id = setTimeout(measureTabs, 60); return () => clearTimeout(id); } }, [open, cat, measureTabs]);

  if (!open) return null;
  const nav = isBrowse ? browseFlat : flat;
  const go = (i, newTab) => {
    if (!i) return;
    if (newTab && i.to) { window.open(hrefFor(i.to, i.params), "_blank"); return; }
    setOpen(false); i.run ? i.run() : i.to && navigate(i.to, i.params);
  };
  const cycleCat = (dir) => {
    const idx = SEARCH_CATS.findIndex((c) => c.key === cat);
    const next = (idx + dir + SEARCH_CATS.length) % SEARCH_CATS.length;
    setCat(SEARCH_CATS[next].key);
  };

  return (
    <div className="fixed inset-0 z-[100] flex items-start justify-center px-4 pt-[10vh]" onMouseDown={() => setOpen(false)}>
      <div className="flex h-[600px] max-h-[82vh] w-full max-w-[720px] flex-col overflow-hidden rounded-2xl border border-line bg-background shadow-[0_32px_70px_-24px_rgba(0,0,0,0.32),0_10px_28px_-16px_rgba(0,0,0,0.14)]"
        data-veto-pop style={{ animation: "vetoPop .17s cubic-bezier(.2,.7,.2,1)" }}
        onMouseDown={(e) => e.stopPropagation()}>

        {/* search field */}
        <div className="flex items-center gap-3.5 px-6 pt-1">
          <Icon name="search" size={22} className="shrink-0 text-ink-soft" />
          <input ref={inputRef} value={q} onChange={(e) => setQ(e.target.value)}
            placeholder={SEARCH_PLACEHOLDERS[ph]}
            autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} name="veto-search"
            onKeyDown={(e) => {
              if (e.key === "ArrowDown") { e.preventDefault(); setSel((s) => Math.min(s + 1, nav.length - 1)); }
              else if (e.key === "ArrowUp") { e.preventDefault(); setSel((s) => Math.max(s - 1, 0)); }
              else if (e.key === "Tab") { e.preventDefault(); cycleCat(e.shiftKey ? -1 : 1); }
              else if (e.key === "Enter") { e.preventDefault(); go(nav[sel], e.metaKey || e.ctrlKey); }
            }}
            className="h-[62px] flex-1 bg-transparent text-[16.5px] text-ink outline-none placeholder:text-muted-foreground/55" />
        </div>

        {/* category tabs */}
        <div className="relative px-4">
          <div ref={tabsRef} onScroll={measureTabs}
            className="flex items-center gap-1 overflow-x-auto py-2.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
            style={{ paddingLeft: tabEdge.left ? 28 : 0, paddingRight: tabEdge.right ? 28 : 0 }}>
            {SEARCH_CATS.map((c) => {
              const active = c.key === cat;
              return (
                <button key={c.key} type="button" onClick={() => setCat(c.key)}
                  className={cn("shrink-0 whitespace-nowrap rounded-lg px-3 py-1.5 text-[13px] transition-colors duration-100",
                    active ? "bg-surface-2 font-medium text-ink" : "text-ink-soft hover:bg-surface hover:text-ink")}>
                  {c.label}
                </button>
              );
            })}
          </div>
          {tabEdge.left && (
            <div className="absolute left-4 top-0 bottom-0 flex items-center">
              <div className="pointer-events-none absolute left-0 h-full w-10 bg-gradient-to-r from-background to-transparent" />
              <button type="button" aria-label="Scroll filters left" onMouseDown={(e) => e.preventDefault()}
                onClick={() => tabsRef.current?.scrollBy({ left: -200, behavior: "smooth" })}
                className="relative grid h-7 w-7 place-items-center rounded-lg text-ink-soft transition-colors hover:bg-surface hover:text-ink">
                <Icon name="arrow-left" size={14} />
              </button>
            </div>
          )}
          {tabEdge.right && (
            <div className="absolute right-4 top-0 bottom-0 flex items-center justify-end">
              <div className="pointer-events-none absolute right-0 h-full w-10 bg-gradient-to-l from-background to-transparent" />
              <button type="button" aria-label="Scroll filters right" onMouseDown={(e) => e.preventDefault()}
                onClick={() => tabsRef.current?.scrollBy({ left: 200, behavior: "smooth" })}
                className="relative grid h-7 w-7 place-items-center rounded-lg text-ink-soft transition-colors hover:bg-surface hover:text-ink">
                <Icon name="arrow-right" size={14} />
              </button>
            </div>
          )}
        </div>

        {/* results */}
        <div className="min-h-0 flex-1 overflow-y-auto border-t border-line px-3 py-2">
          {/* empty-state: a single curated jump list (one decision, not two) */}
          {isBrowse ? (
            <Section label="Suggestions">
              {suggestions.map((it) => {
                const idx = browseFlat.indexOf(it);
                return <ResultRow key={it.id} it={it} active={idx === sel} onHover={() => setSel(idx)} onClick={() => go(it)} />;
              })}
            </Section>
          ) : nav.length === 0 ? (
            <div className="px-4 py-10 text-center">
              <div className="text-[13px] text-ink">No matches for “{q.trim()}”.</div>
              <div className="mt-1 text-[12px] text-ink-soft">Try a name, an address, an amount, or a file number.</div>
            </div>
          ) : (
            groups.map((g) => (
              <Section key={g.type} label={g.label}>
                {g.items.map((it) => {
                  const idx = flat.indexOf(it);
                  return <ResultRow key={it.id} it={it} active={idx === sel} onHover={() => setSel(idx)} onClick={() => go(it)} />;
                })}
              </Section>
            ))
          )}
        </div>

        {/* footer hints */}
        <div className="flex items-center justify-center gap-x-8 border-t border-line px-6 py-3 text-[12px] text-muted-foreground">
          <FootHint keys={["↑", "↓"]} label="Navigate" />
          <FootHint keys={["↵"]} label="Open" />
          <FootHint keys={["⌘", "↵"]} label="Open in new tab" />
        </div>
      </div>
    </div>
  );
}

function Section({ label, children }) {
  return (
    <div className="mb-1.5">
      <div className="px-3 pb-1.5 pt-3 text-[11px] text-muted-foreground/75">{label}</div>
      {children}
    </div>
  );
}

function FootHint({ keys, label }) {
  return (
    <span className="flex shrink-0 items-center gap-2 whitespace-nowrap">
      <span className="flex items-center gap-1">
        {keys.map((k, i) => (
          <kbd key={i} className="inline-flex h-[28px] min-w-[28px] items-center justify-center rounded-md border border-line bg-surface px-1.5 font-medium text-ink-soft">
            <span className="block leading-none" style={{ fontSize: k === "⌘" ? "19px" : "18px", transform: k === "↵" ? "translateY(-4px)" : "translateY(-0.5px)" }}>{k}</span>
          </kbd>
        ))}
      </span>
      <span className="text-[12px]">{label}</span>
    </span>
  );
}

/* One result row — leading glyph is borderless; right meta adapts to type. */
function ResultRow({ it, active, onHover, onClick }) {
  const tone = it.status ? statusTone(it.status) : "muted";
  return (
    <button type="button" onMouseEnter={onHover} onClick={onClick}
      className={cn("flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors duration-75", active ? "bg-[var(--veto-row-hover)]" : "hover:bg-[var(--veto-row-hover)]")}>
      <span className={cn("grid h-5 w-5 shrink-0 place-items-center transition-colors", active ? "text-ink" : "text-ink-soft")}>
        <Icon name={it.icon} size={16} />
      </span>
      <span className="min-w-0 flex-1">
        <span className="block truncate text-[13.5px] text-ink">{it.label}</span>
        {it.sub && <span className="mt-0.5 block truncate text-[12px] text-muted-foreground">{it.sub}</span>}
      </span>

      {/* right meta by type — kept quiet */}
      {(it.type === "movement" || it.type === "payoff") && it.amount != null ? (
        <span className="flex shrink-0 items-center gap-2.5 pl-3">
          <span className={cn("h-1.5 w-1.5 rounded-full", TONE_DOT[tone])} />
          <span className="w-[92px] text-right font-mono text-[12.5px] tabular-nums text-ink">{it.dir === "out" ? "−" : "+"}{fmtUSD(it.amount)}</span>
        </span>
      ) : it.type === "file" ? (
        <span className={cn("inline-flex shrink-0 items-center gap-1.5 pl-3 text-[11.5px]", TONE_TEXT[tone])}>
          <span className={cn("h-1.5 w-1.5 rounded-full", TONE_DOT[tone])} />{it.status}
        </span>
      ) : it.type === "hold" ? (
        <span className="shrink-0 pl-3 text-[11.5px] font-medium text-[var(--warn)]">{it.status}</span>
      ) : it.type === "receipt" ? (
        <span className="shrink-0 pl-3 font-mono text-[11.5px] text-muted-foreground">{it.when}</span>
      ) : it.rightTop ? (
        <span className="shrink-0 pl-3 text-[12px] text-ink-soft">{it.rightTop}</span>
      ) : it.hint ? (
        <kbd className="shrink-0 rounded bg-surface-2 px-1.5 py-0.5 font-mono text-[10px] text-ink-soft">{it.hint}</kbd>
      ) : null}
    </button>
  );
}

/* --------------------------------------------------------- HelpLauncher */
/* Mercury-model: the bubble opens straight INTO a conversation (greeting +
   suggested prompts + input), never a menu you have to learn. */
function HelpLauncher({ reviewer = { name: "Madeline Lane" } }) {
  const [open, setOpen] = useState(false);
  const [msgs, setMsgs] = useState([]);
  const [input, setInput] = useState("");
  const [busy, setBusy] = useState(false);
  const scrollRef = useRef(null);
  const inputRef = useRef(null);
  const first = (reviewer?.name || "there").split(" ")[0];

  useEffect(() => {
    const onOpen = () => setOpen(true);
    window.addEventListener("veto:help", onOpen);
    return () => window.removeEventListener("veto:help", onOpen);
  }, []);
  useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [msgs, busy]);
  useEffect(() => { if (open && inputRef.current) inputRef.current.focus(); }, [open]);

  const send = async (text) => {
    const q = (text != null ? text : input).trim();
    if (!q || busy) return;
    setInput("");
    setMsgs((m) => [...m, { role: "user", text: q }]);
    setBusy(true);
    let reply = "";
    try {
      const ai = window.claude && window.claude.complete;
      if (ai) {
        const prompt = "You are Veto's in-app assistant for an escrow operations console. Domain: files, signed receipts/records, and money lines (buyer funding in, payoff demands, seller proceeds), plus reviews, exceptions, and holds. Answer in 2-3 short, plain, neutral sentences. Never overclaim — say \"matched by API, not live bank confirmation\" rather than \"verified.\" Question: " + q;
        reply = await Promise.race([
          ai(prompt),
          new Promise((res) => setTimeout(() => res(""), 14000)),
        ]);
      }
    } catch (e) { reply = ""; }
    if (!reply) reply = "I can point you to a file, explain what a receipt records, or walk through a money line. Try a file number, a closing date, or \"what's blocking my closings today?\"";
    setMsgs((m) => [...m, { role: "assistant", text: reply }]);
    setBusy(false);
  };

  const suggestions = [
    { icon: "tasks", label: "What needs me before closing today?" },
    { icon: "receipt", label: "What does a review record prove?" },
    { icon: "message-circle", label: "Get support" },
  ];

  return (
    <div className="no-print fixed bottom-5 right-5 z-[90] hidden flex-col items-end gap-3 sm:flex" data-print-hide="true">
      {open && (
        <div role="dialog" aria-label="Veto assistant"
          className="flex h-[460px] w-[360px] max-w-[calc(100vw-2.5rem)] flex-col overflow-hidden rounded-2xl border border-line bg-background shadow-[0_24px_64px_-20px_rgba(0,0,0,0.35)]"
          style={{ animation: "hintIn .16s cubic-bezier(0.16,1,0.3,1)", transformOrigin: "right bottom" }}>
          <div className="flex items-center justify-between border-b border-line px-4 py-3">
            <div className="flex items-center gap-2">
              <span className="text-[13px] font-semibold text-ink">Veto Assistant</span>
              <span className="rounded-full bg-surface-2 px-1.5 py-0.5 text-[9.5px] font-semibold uppercase tracking-[0.06em] text-ink-soft">Beta</span>
            </div>
            <button type="button" onClick={() => setOpen(false)} aria-label="Close assistant"
              className="grid h-7 w-7 cursor-pointer place-items-center rounded-md text-ink-soft transition-colors hover:bg-surface hover:text-ink"><Icon name="x" size={15} /></button>
          </div>

          <div ref={scrollRef} className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
            {msgs.length === 0 ? (
              <div className="flex h-full flex-col justify-end">
                <p className="text-[16px] font-semibold tracking-[-0.01em] text-ink">Hey, {first}. How can I help?</p>
                <p className="mt-1.5 text-[12.5px] leading-relaxed text-ink-soft">Ask about a file, a receipt, or a money line — or start with one of these.</p>
                <div className="mt-4 flex flex-col gap-2">
                  {suggestions.map((s) => (
                    <button key={s.label} type="button" onClick={() => send(s.label)}
                      className="flex cursor-pointer items-center gap-2.5 rounded-xl border border-line bg-surface/50 px-3 py-2.5 text-left text-[12.5px] text-ink transition hover:border-ink/20 hover:bg-surface">
                      <Icon name={s.icon} size={15} className="shrink-0 text-ink-soft" />
                      <span className="flex-1">{s.label}</span>
                      <Icon name="arrow-up-right" size={13} className="shrink-0 text-muted-foreground" />
                    </button>
                  ))}
                </div>
              </div>
            ) : (
              <div className="flex flex-col gap-2.5">
                {msgs.map((m, i) => (
                  <div key={i} className={cn("max-w-[86%] rounded-2xl px-3.5 py-2 text-[13px] leading-relaxed",
                    m.role === "user" ? "self-end bg-ink text-background" : "self-start bg-surface text-ink")}>{m.text}</div>
                ))}
                {busy && (
                  <div className="self-start rounded-2xl bg-surface px-3.5 py-3">
                    <span className="flex items-center gap-1">
                      {[0, 1, 2].map((d) => (
                        <span key={d} className="h-1.5 w-1.5 rounded-full bg-ink-soft" style={{ animation: "vtTyping 1.1s ease-in-out infinite", animationDelay: d * 0.16 + "s" }} />
                      ))}
                    </span>
                  </div>
                )}
              </div>
            )}
          </div>

          <form onSubmit={(e) => { e.preventDefault(); send(); }} className="border-t border-line p-2.5">
            <div className="flex items-center gap-2 rounded-xl border border-line bg-background px-3 py-2 transition-colors focus-within:border-ink/30">
              <input ref={inputRef} value={input} onChange={(e) => setInput(e.target.value)} placeholder="Ask a question or give a command"
                className="min-w-0 flex-1 bg-transparent text-[13px] text-ink outline-none placeholder:text-muted-foreground" />
              <button type="submit" aria-label="Send" disabled={!input.trim() || busy}
                className="grid h-7 w-7 shrink-0 cursor-pointer place-items-center rounded-lg bg-ink text-background transition disabled:cursor-not-allowed disabled:opacity-25"><Icon name="arrow-up" size={14} strokeWidth={2} /></button>
            </div>
          </form>
        </div>
      )}
      <button type="button" onClick={() => setOpen((o) => !o)} aria-label={open ? "Close assistant" : "Open assistant"}
        className={cn("grid h-12 w-12 cursor-pointer place-items-center rounded-full border bg-background transition",
          open ? "border-ink/30 text-ink shadow-[0_4px_14px_-4px_rgba(0,0,0,0.22)]" : "border-line text-ink-soft shadow-[0_6px_20px_-6px_rgba(0,0,0,0.28)] hover:border-ink/25 hover:text-ink hover:shadow-[0_8px_24px_-6px_rgba(0,0,0,0.32)]")}>
        <Icon name={open ? "x" : "message-circle"} size={20} />
      </button>
    </div>
  );
}

Object.assign(window, { CommandPalette, HelpLauncher });
