// dashboard-crm-browse.js — extracted verbatim from dashboard.js (classic script,
// shared global scope). Loaded by dashboard.html before dashboard.js so its
// top-level declarations are defined when init() runs. Pure move, no logic change.
// ── CRM browse view ───────────────────────────────────────────────

let crmFilter = {
  q: "",
  tag: "",
  sector: "",
  platform: "",
  outreach_status: "",
  is_target: "",
  lead_score_min: null,
  kind: "",
  ai_verdict: "",
  ai_confidence_min: null,
  min_icp_score: null,
  icp_band: "",
  missing: "",
  top: null,
  view: "all",
  page: 1,
  // Task 9a: scope the Contacts tab to one client — the backend joins in
  // that client's per-company ICP score + per-channel outreach status when
  // this is set. Empty string = no client scope (default, unchanged path).
  client_id: "",
};

// Kind taxonomy (v57): orthogonal axes used to segment the Companies tab.
// Maps the API param value → emoji + label for chips and row badges.
const COMPANY_KINDS = [
  { id: "token", column: "is_token_issuer", emoji: "🪙", label: "Token", short: "Token" },
  { id: "service", column: "is_service_provider", emoji: "🛠", label: "Service", short: "Service" },
  { id: "investor", column: "is_investor", emoji: "💰", label: "Investor", short: "Investor" },
];

// Top-N market-cap-rank chips (v59): Companies tab only. Filters down to
// rows whose market_cap_rank is set by the daily CoinGecko ingest.
// Chip selection is mutually exclusive — picking 100 supersedes 500.
const COMPANY_TOP_CHIPS = [
  { id: 100, label: "Top 100" },
  { id: 500, label: "Top 500" },
  { id: 1000, label: "Top 1k" },
  { id: 5000, label: "Top 5k" },
];
let crmTab = "contacts";

const SMART_VIEWS = [
  { id: "all", label: "All", icon: "\ud83d\uddc2", filters: {} },
  {
    id: "follow-up",
    label: "Needs follow-up",
    icon: "\ud83d\udd14",
    filters: { stage: "replied", sort: "last_contacted_at" },
  },
  {
    id: "hot-uncontacted",
    label: "Hot uncontacted",
    icon: "\ud83d\udd25",
    filters: { lead_score_min: 50, outreach_status: "none", sort: "lead_score" },
  },
  {
    id: "targets",
    label: "Targets",
    icon: "\ud83c\udfaf",
    filters: { is_target: "1", sort: "lead_score" },
  },
  {
    id: "warm-stale",
    label: "Going cold",
    icon: "\u2744\ufe0f",
    filters: { stage: "warm", last_contact_days: 7 },
  },
  {
    id: "recently-replied",
    label: "Recently replied",
    icon: "\ud83d\udcac",
    filters: { outreach_status: "replied", sort: "last_contacted_at" },
  },
  {
    id: "high-engagement",
    label: "High engagement",
    icon: "\ud83d\udcc8",
    filters: { lead_score_min: 60, sort: "lead_score" },
  },
  // AI-driven view: only contacts the lead-analyzer scored as "qualified"
  // with high confidence. Empty until the AI analysis button has been run
  // against contacts \u2014 fills in as you analyze.
  {
    id: "hot-ai",
    label: "Hot leads (AI)",
    icon: "\ud83d\udd25",
    filters: { ai_verdict: "qualified", ai_confidence_min: 0.7, sort: "lead_score" },
  },
];

function applySmartView(viewId) {
  const view = SMART_VIEWS.find((v) => v.id === viewId);
  // Reset all filters
  crmFilter.q = "";
  crmFilter.tag = "";
  crmFilter.sector = "";
  crmFilter.platform = "";
  crmFilter.outreach_status = "";
  crmFilter.is_target = "";
  crmFilter.lead_score_min = null;
  crmFilter.kind = "";
  crmFilter.ai_verdict = "";
  crmFilter.ai_confidence_min = null;
  crmFilter.min_icp_score = null;
  crmFilter.icp_band = "";
  crmFilter.missing = "";
  crmFilter.top = null;
  crmFilter.stage = "";
  crmFilter.client_id = "";
  // "priority" sort only means anything paired with a client_id (the server
  // falls back to updated_at without one) — clear it so switching views
  // doesn't leave a stale client-only sort behind.
  if (crmFilter.sort === "priority") crmFilter.sort = "";
  crmFilter.page = 1;
  crmFilter.view = viewId;
  if (view && view.filters) {
    Object.assign(crmFilter, view.filters);
  }
}

function buildActiveFilterPills() {
  const pills = [];
  const addPill = (label, clearFn) => {
    pills.push(
      el("span", { class: "filter-pill" }, [
        document.createTextNode(label),
        el(
          "button",
          {
            class: "filter-pill-x",
            type: "button",
            onclick: (e) => {
              e.stopPropagation();
              clearFn();
              crmFilter.view = "all";
              crmFilter.page = 1;
              loadCrm();
              renderCrm();
            },
          },
          "\u00d7",
        ),
      ]),
    );
  };
  if (crmFilter.stage)
    addPill(`Stage: ${crmFilter.stage === "unknown" ? "lead" : crmFilter.stage}`, () => {
      crmFilter.stage = "";
    });
  if (crmFilter.tag)
    addPill(`Tag: ${crmFilter.tag}`, () => {
      crmFilter.tag = "";
    });
  if (crmFilter.sector)
    addPill(`Sector: ${crmFilter.sector}`, () => {
      crmFilter.sector = "";
    });
  if (crmFilter.platform)
    addPill(`Platform: ${crmFilter.platform}`, () => {
      crmFilter.platform = "";
    });
  if (crmFilter.outreach_status)
    addPill(`Outreach: ${crmFilter.outreach_status}`, () => {
      crmFilter.outreach_status = "";
    });
  if (crmFilter.is_target === "1")
    addPill("Targets only", () => {
      crmFilter.is_target = "";
    });
  if (crmFilter.lead_score_min)
    addPill(`Score \u2265 ${crmFilter.lead_score_min}`, () => {
      crmFilter.lead_score_min = null;
    });
  if (crmFilter.kind) {
    const labels = crmFilter.kind
      .split(",")
      .map((k) => {
        const def = COMPANY_KINDS.find((x) => x.id === k.trim());
        return def ? `${def.emoji} ${def.label}` : k;
      })
      .join(" \u00b7 ");
    addPill(`Kind: ${labels}`, () => {
      crmFilter.kind = "";
    });
  }
  if (crmFilter.ai_verdict) {
    const conf = crmFilter.ai_confidence_min
      ? ` \u2265 ${Math.round(crmFilter.ai_confidence_min * 100)}%`
      : "";
    addPill(`AI: ${crmFilter.ai_verdict}${conf}`, () => {
      crmFilter.ai_verdict = "";
      crmFilter.ai_confidence_min = null;
    });
  }
  if (crmFilter.min_icp_score) {
    addPill(`ICP \u2265 ${Number(crmFilter.min_icp_score).toFixed(1)}`, () => {
      crmFilter.min_icp_score = null;
    });
  }
  if (crmFilter.icp_band) {
    const bandLabel = (ICP_BAND_BADGE[crmFilter.icp_band] || {}).label || crmFilter.icp_band;
    addPill(`Band: ${bandLabel}`, () => {
      crmFilter.icp_band = "";
    });
  }
  if (crmFilter.top) {
    addPill(`\ud83d\udcc8 Top ${crmFilter.top.toLocaleString()}`, () => {
      crmFilter.top = null;
    });
  }
  if (crmFilter.client_id) {
    const client = (_clientsCache || []).find((c) => String(c.id) === String(crmFilter.client_id));
    addPill(`Client: ${client ? client.name : `#${crmFilter.client_id}`}`, () => {
      crmFilter.client_id = "";
      if (crmFilter.sort === "priority") crmFilter.sort = "";
    });
  }
  if (crmFilter.q)
    addPill(`Search: "${crmFilter.q}"`, () => {
      crmFilter.q = "";
    });
  return pills;
}

// ICP filter chip row. Min-score chips are mutually exclusive; band chips
// are mutually exclusive among themselves but combine with min-score.
function buildIcpFilterRow() {
  const setMin = (n) => {
    crmFilter.min_icp_score = crmFilter.min_icp_score === n ? null : n;
    crmFilter.view = "all";
    crmFilter.page = 1;
    loadCrm();
    renderCrm();
  };
  const setBand = (b) => {
    crmFilter.icp_band = crmFilter.icp_band === b ? "" : b;
    crmFilter.view = "all";
    crmFilter.page = 1;
    loadCrm();
    renderCrm();
  };
  const minChip = (n, label) =>
    el(
      "button",
      {
        class: `btn btn-tiny ${crmFilter.min_icp_score === n ? "btn-primary" : "btn-secondary"}`,
        type: "button",
        onclick: () => setMin(n),
      },
      label,
    );
  const bandChip = (b) =>
    el(
      "button",
      {
        class: `btn btn-tiny ${crmFilter.icp_band === b ? "btn-primary" : "btn-secondary"}`,
        type: "button",
        onclick: () => setBand(b),
      },
      ICP_BAND_BADGE[b].label,
    );

  return el("div", { class: "filter-row", style: "gap: 4px; align-items: center;" }, [
    el("span", { class: "muted small", style: "margin-right: 4px;" }, "ICP:"),
    minChip(null, "All"),
    minChip(5, "≥ 5"),
    minChip(7, "≥ 7"),
    minChip(8, "≥ 8"),
    el("span", { style: "width: 8px; display: inline-block;" }),
    bandChip("abm"),
    bandChip("outbound"),
    bandChip("inbound"),
    bandChip("bin"),
  ]);
}

// ── Client filter (Task 9a) ─────────────────────────────────────────────
// Scopes the Contacts tab to one client. The backend (Task 8) only returns
// client_icp_score/client_icp_band + the four per-channel outreach statuses
// when client_id is in the query, so everything below is additive — the
// no-client path never touches the DOM these helpers build.

// Lazily-fetched, fetch-once cache of the owner's clients. Populated by
// loadClientsForPicker(); renderCrm() never awaits it, so a slow
// /api/clients response can't delay the contacts list underneath it.
let _clientsCache = null;
let _clientsCachePromise = null;

function loadClientsForPicker(onLoaded) {
  if (_clientsCache) {
    onLoaded(_clientsCache);
    return;
  }
  if (!_clientsCachePromise) {
    _clientsCachePromise = api("clients-list")
      .then((res) => (Array.isArray(res?.clients) ? res.clients : []))
      .catch(() => []);
  }
  _clientsCachePromise.then((clients) => {
    _clientsCache = clients;
    onLoaded(clients);
  });
}

// Compact <select> for the Contacts filter row. Starts out with just "All
// clients" and swaps in the real option list in place once the lazy fetch
// resolves — the contacts list itself never waits on this.
function buildClientPicker() {
  const select = el(
    "select",
    {
      class: "filter-select",
      title: "Scope to one client's ICP score + outreach status",
      onchange: (e) => {
        crmFilter.client_id = e.target.value;
        // Default to the shared effective-priority sort when a client is
        // picked (the server only honors sort=priority alongside
        // client_id); clearing the picker drops back to updated_at.
        crmFilter.sort = crmFilter.client_id ? "priority" : "";
        crmFilter.view = "all";
        crmFilter.page = 1;
        loadCrm();
        renderCrm();
      },
    },
    [el("option", { value: "" }, "All clients")],
  );
  loadClientsForPicker((clients) => {
    select.innerHTML = "";
    select.appendChild(el("option", { value: "" }, "All clients"));
    for (const c of clients) {
      const o = el("option", { value: String(c.id) }, c.name || `Client #${c.id}`);
      if (String(c.id) === crmFilter.client_id) o.selected = true;
      select.appendChild(o);
    }
  });
  return select;
}

// Launch button for the per-client ICP wizard (Task 9b) — sits next to the
// client picker above. Only rendered once a client is selected: with no
// client_id there is nothing to tailor, and window.openClientIcpWizard
// itself guards against a blank id, so keeping this additive (no client ->
// no button) is simpler than a disabled-state affordance.
function buildIcpWizardLaunchButton() {
  const client = (_clientsCache || []).find((c) => String(c.id) === String(crmFilter.client_id));
  return el(
    "button",
    {
      class: "btn btn-tiny btn-secondary",
      type: "button",
      title: "Tailor this client's ICP: analyze their site, generate filters, score candidates",
      onclick: () => {
        window.openClientIcpWizard(
          crmFilter.client_id,
          client ? client.name : `Client #${crmFilter.client_id}`,
        );
      },
    },
    [icon("target"), " Tailor ICP"],
  );
}

// Post-scoring hook the ICP wizard's "View in CRM" button calls (Task 9b) —
// scopes the Contacts tab to this client's freshly-scored companies. Exposed
// as a window property (like window.openClientIcpWizard) rather than a bare
// top-level function so the wizard's own file — which has no top-level
// `crmFilter`/`crmTab` declarations of its own — can reach it without
// tripping the cross-file no-undef lint rule; `window.<name>(...)` is a
// property access, not a bare identifier, so it needs no globals-list entry.
window.applyClientIcpResult = function applyClientIcpResult(clientId) {
  crmFilter.client_id = String(clientId);
  crmFilter.sort = "priority";
  crmFilter.view = "all";
  crmFilter.page = 1;
  crmTab = "contacts";
};

// Per-channel outreach status pills (contact-row extra, client-scoped only).
// A replied/connected/converted status reads as "warm" — the same
// vocabulary the server already uses for its reply-rate rollups.
const CLIENT_ROW_CHANNELS = [
  { field: "linkedin_status", prefix: "in" },
  { field: "x_status", prefix: "x" },
  { field: "email_status", prefix: "em" },
  { field: "telegram_status", prefix: "tg" },
];
const WARM_CHANNEL_STATUSES = new Set(["replied", "connected", "converted"]);

function renderClientChannelStrip(row) {
  const pills = [];
  for (const { field, prefix } of CLIENT_ROW_CHANNELS) {
    const status = row[field];
    if (status == null) continue;
    const warm = WARM_CHANNEL_STATUSES.has(status);
    pills.push(
      el(
        "span",
        {
          class: `badge ${warm ? "badge-green" : "badge-gray"}`,
          style: "font-size: 9px; padding: 1px 6px;",
        },
        `${prefix}:${status}`,
      ),
    );
  }
  return pills.length > 0
    ? el("div", { class: "row", style: "gap: 3px; flex-wrap: wrap;" }, pills)
    : el("span", { class: "muted small" }, "–");
}

// Client-ICP badge: score (1 decimal) + band, styled via ICP_BAND_BADGE —
// mirrors renderIcpBandBadge's fallback-to-"bin" for an unscored band.
function renderClientIcpBadge(score, band) {
  const info = ICP_BAND_BADGE[band] || ICP_BAND_BADGE.bin;
  return el(
    "span",
    { class: `badge ${info.cls}`, title: "Client ICP score" },
    `${Number(score).toFixed(1)} · ${info.label}`,
  );
}

// Appends one extra <th>/<td> per row onto the already-rendered contacts
// table — client_icp_score/band + the channel-status strip. A DOM
// post-process rather than a change to renderCrmTable() (owned by
// dashboard-bulk-compose.js) so the no-client render path stays byte-for-
// byte unchanged. No-ops before touching the DOM at all unless a client is
// selected.
function injectClientColumns(rows) {
  if (crmTab !== "contacts" || !crmFilter.client_id) return;
  const wrap = $("#crm-list-wrap");
  const table = wrap ? wrap.querySelector("table") : null;
  if (!table) return;
  const headerRow = table.querySelector("thead tr");
  if (headerRow) headerRow.appendChild(el("th", {}, "Client"));
  for (const c of rows) {
    const checkbox = table.querySelector(
      `.row-checkbox[data-row-type="contacts"][data-row-id="${c.id}"]`,
    );
    const tr = checkbox ? checkbox.closest("tr") : null;
    if (!tr) continue;
    const icpBadge =
      c.client_icp_score != null
        ? renderClientIcpBadge(c.client_icp_score, c.client_icp_band)
        : null;
    tr.appendChild(el("td", {}, [icpBadge, renderClientChannelStrip(c)]));
  }
}

// Missing-channel filter chip row (v68). Each chip toggles a channel-key
// in the comma-separated `crmFilter.missing` value. Multiple chips can be
// active simultaneously — the API returns rows missing AT LEAST ONE of
// them (OR semantics, "anything I should re-enrich").
//
// Why two rows of chips: ICP scoring + completeness are orthogonal axes,
// and the operator often wants both ("show ICP ≥ 7 rows that are still
// missing email") to prioritize re-enrichment work.
function buildMissingFilterRow() {
  const current = (crmFilter.missing || "")
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);
  const toggle = (key) => {
    const next = current.includes(key) ? current.filter((x) => x !== key) : [...current, key];
    crmFilter.missing = next.join(",");
    crmFilter.view = "all";
    crmFilter.page = 1;
    loadCrm();
    renderCrm();
  };
  const chip = (key, icon, label) =>
    el(
      "button",
      {
        class: `btn btn-tiny ${current.includes(key) ? "btn-primary" : "btn-secondary"}`,
        type: "button",
        onclick: () => toggle(key),
        title: `Show rows missing ${label}`,
      },
      `${icon} ${label}`,
    );

  return el(
    "div",
    { class: "filter-row", style: "gap: 4px; align-items: center;flex-wrap:wrap;" },
    [
      el("span", { class: "muted small", style: "margin-right: 4px;" }, "Missing:"),
      chip("website", "🌐", "site"),
      chip("twitter", "🐦", "X"),
      chip("linkedin", "💼", "LI"),
      chip("telegram", "💬", "TG"),
      chip("discord", "🎮", "Discord"),
      chip("github", "🐙", "GH"),
      chip("ai", "🤖", "AI"),
      current.length > 0
        ? el(
            "button",
            {
              class: "btn btn-tiny btn-danger",
              type: "button",
              onclick: () => {
                crmFilter.missing = "";
                crmFilter.page = 1;
                loadCrm();
                renderCrm();
              },
            },
            "Clear",
          )
        : null,
      // One-click: select all visible rows AND launch bulk-enrich on them.
      // Saves the operator from clicking the header checkbox + selection-bar
      // button manually after every filter tweak. Only visible when at
      // least one missing-chip is active (otherwise selecting "everything"
      // is unhelpful).
      current.length > 0
        ? el(
            "button",
            {
              class: "btn btn-tiny btn-primary",
              type: "button",
              style: "margin-left:auto;",
              title: "Select every visible row and launch bulk-enrich",
              onclick: () => selectVisibleAndEnrich(),
            },
            "Enrich all visible",
          )
        : null,
    ],
  );
}

// One-click "select every visible row and run enrich" — the natural
// workflow after applying a "missing X" filter. Reads the visible row
// IDs from the table's checkbox column (data attribute), populates the
// selection set, and triggers the existing runBulkEnrichFromSelection.
function selectVisibleAndEnrich() {
  const checkboxes = document.querySelectorAll('.row-checkbox[data-row-type="companies"]');
  const ids = [];
  for (const cb of checkboxes) {
    const id = Number(cb.getAttribute("data-row-id"));
    if (Number.isFinite(id) && id > 0) ids.push(id);
  }
  if (ids.length === 0) {
    toast("No visible rows to select", "info", 3000);
    return;
  }
  // Cap at the bulk-enrich runner's per-job limit. The toast tells the
  // operator if they need to paginate to enrich the rest.
  const MAX_PER_JOB = 50;
  if (ids.length > MAX_PER_JOB) {
    toast(`Selecting first ${MAX_PER_JOB} of ${ids.length} visible rows`, "info", 4000);
    ids.length = MAX_PER_JOB;
  }
  // Set the selection state and re-render the bar so it shows the count
  // before kickoff (the enrich runner reads from dashboardState directly).
  dashboardState.crmSelection.type = "companies";
  dashboardState.crmSelection.ids = new Set(ids);
  renderCrmSelectionBar();
  runBulkEnrichFromSelection();
}

function buildTagAutocomplete() {
  const tags = dashboardState.cache.crmTags || [];
  const wrap = el("div", { class: "tag-autocomplete" });
  const input = el("input", {
    class: "filter-input",
    placeholder: "Filter by tag\u2026",
    value: crmFilter.tag,
    onfocus: () => showTagDropdown(""),
    oninput: (e) => showTagDropdown(e.target.value),
  });
  wrap.appendChild(input);

  let dropdown = null;
  function showTagDropdown(query) {
    if (dropdown) dropdown.remove();
    const q = query.toLowerCase();
    const filtered = tags.filter((t) => !q || t.tag.includes(q)).slice(0, 40);
    if (filtered.length === 0) return;
    dropdown = el(
      "div",
      { class: "tag-dropdown" },
      filtered.map((t) =>
        el(
          "div",
          {
            class: "tag-dropdown-item",
            onmousedown: (e) => {
              e.preventDefault();
              crmFilter.tag = t.tag;
              crmFilter.view = "all";
              crmFilter.page = 1;
              input.value = t.tag;
              if (dropdown) dropdown.remove();
              dropdown = null;
              loadCrm();
              renderCrm();
            },
          },
          [document.createTextNode(t.tag), el("span", { class: "tag-count" }, String(t.count))],
        ),
      ),
    );
    wrap.appendChild(dropdown);
  }

  input.addEventListener("blur", () => {
    setTimeout(() => {
      if (dropdown) {
        dropdown.remove();
        dropdown = null;
      }
    }, 150);
  });
  return wrap;
}

async function renderCrm() {
  // Show skeleton immediately so the page swap doesn't flash blank while we
  // hydrate filter options + the contact/company list. Skipped on warm cache
  // because the render below is essentially instant in that case.
  if (!dashboardState.cache.crmTags) {
    showLoading("Loading CRM…");
  }
  // Load filter options (tags, sectors) once
  if (!dashboardState.cache.crmTags) {
    const [tagsData, sectorsData] = await Promise.all([
      api("crm-contacts-tags").catch(() => ({ tags: [] })),
      api("crm-contacts-sectors").catch(() => ({ sectors: [] })),
    ]);
    dashboardState.cache.crmTags = tagsData?.tags || [];
    dashboardState.cache.crmSectors = sectorsData?.sectors || [];
  }

  const stages = ["unknown", "cold", "contacted", "replied", "warm", "hot", "customer"];
  const stageFilter = crmFilter.stage || "";
  const activePills = buildActiveFilterPills();

  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "CRM"),
        el("div", { class: "page-subtitle" }, "Browse contacts and companies across your pipeline"),
      ]),
      el("div", { class: "row", style: "gap: 8px;" }, [
        // Add Contact / Add Company \u2014 context-aware
        crmTab === "contacts" || crmTab === "companies"
          ? el(
              "button",
              {
                class: "btn btn-primary btn-tiny",
                type: "button",
                title:
                  crmTab === "contacts"
                    ? "Manually add a new contact"
                    : "Manually add a new company",
                onclick: () =>
                  crmTab === "contacts" ? openCrmAddContactDrawer() : openCrmAddCompanyDrawer(),
              },
              crmTab === "contacts" ? "+ Add Contact" : "+ Add Company",
            )
          : null,
        // Bulk AI analysis \u2014 Contacts and Companies tabs. Reads the currently
        // visible page; max 100 per call (server-enforced). Already-analyzed-
        // in-last-24h are no-ops (cached); the AI prompt branch in lead-
        // analyzer.js handles both entity types.
        // NOTE: 88% of companies in this CRM are "thin" (no website/summary
        // data). Run scripts/backfill-company-enrichment.js first if verdicts
        // are coming back as "marginal \u2014 thin profile" too often.
        crmTab === "contacts" || crmTab === "companies"
          ? el(
              "button",
              {
                class: "btn btn-primary btn-tiny",
                type: "button",
                title:
                  crmTab === "contacts"
                    ? "Run AI analysis on every contact on this page (auto-enriches first). Hold Shift to skip enrichment."
                    : "Run AI analysis on every company on this page (auto-enriches first). Hold Shift to skip enrichment.",
                onclick: async (ev) => {
                  const ids =
                    crmTab === "contacts"
                      ? (_lastCrmContactIds || []).slice(0, 100)
                      : (_lastCrmCompanyIds || []).slice(0, 100);
                  if (ids.length === 0) {
                    toast(`No ${crmTab} loaded yet`, "err");
                    return;
                  }
                  // Shift-click skips the enrich phase \u2014 for power users who
                  // already enriched manually and just want a fast re-analyze.
                  const skipEnrich = ev.shiftKey === true;
                  await runBulkLeadAnalysis(ids, {
                    type: crmTab === "contacts" ? "contact" : "company",
                    skipEnrich,
                  });
                },
              },
              `\ud83e\udd16 Analyze with AI`,
            )
          : null,
        el(
          "button",
          {
            class: "btn btn-secondary",
            type: "button",
            onclick: async () => {
              toast("Exporting CSV\u2026", "info");
              const qs = `?${new URLSearchParams({
                ...(crmFilter.q && { q: crmFilter.q }),
                ...(crmFilter.tag && { tag: crmFilter.tag }),
                ...(crmFilter.sector && { sector: crmFilter.sector }),
                ...(crmFilter.stage && { stage: crmFilter.stage }),
                format: "csv",
              }).toString()}`;
              try {
                const { token, apiBase } = await chrome.storage.local.get(["token", "apiBase"]);
                const base = (apiBase || "http://localhost:9091").replace(/\/$/, "");
                const resp = await fetch(`${base}/api/crm/contacts/export${qs}`, {
                  headers: { Authorization: `Bearer ${token}` },
                });
                if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
                const blob = await resp.blob();
                const url = URL.createObjectURL(blob);
                const a = document.createElement("a");
                a.href = url;
                a.download = `nexus-contacts-${new Date().toISOString().slice(0, 10)}.csv`;
                a.click();
                URL.revokeObjectURL(url);
                toast("\u2705 CSV downloaded", "ok");
              } catch (err) {
                toast(`\u274c Export failed: ${err.message}`, "err");
              }
            },
          },
          "\u2b07 Export CSV",
        ),
      ]),
    ]),
    // Smart views (contacts only)
    crmTab === "contacts"
      ? el(
          "div",
          { class: "smart-views" },
          SMART_VIEWS.map((v) =>
            el(
              "button",
              {
                class: `smart-view-chip${crmFilter.view === v.id ? " active" : ""}`,
                type: "button",
                onclick: () => {
                  applySmartView(v.id);
                  renderCrm();
                },
              },
              `${v.icon} ${v.label}`,
            ),
          ),
        )
      : null,
    el("div", { class: "tabs" }, [
      tabBtn("contacts", "Contacts", () => {
        crmTab = "contacts";
        crmFilter.page = 1;
        crmSelClear();
        renderCrm();
      }),
      tabBtn("companies", "Companies", () => {
        crmTab = "companies";
        crmFilter.page = 1;
        crmSelClear();
        renderCrm();
      }),
      tabBtn("pipeline", "Pipeline", () => {
        crmTab = "pipeline";
        crmSelClear();
        renderCrm();
      }),
      tabBtn("inbox", "Inbox", () => {
        crmTab = "inbox";
        crmSelClear();
        renderCrm();
      }),
      tabBtn("activity", "Activity", () => {
        crmTab = "activity";
        crmSelClear();
        renderCrm();
      }),
      tabBtn("sheets", "Sheets", () => {
        crmTab = "sheets";
        crmSelClear();
        renderCrm();
      }),
    ]),
    // Kind filter chips (Companies tab only). Multi-select; OR-combined on
    // the server. Lets the user segment by who-they-talk-to (token issuer
    // vs service vendor vs investor) — the primary outreach axis.
    // Followed by the Top-N market-cap chips (v59) which are mutually
    // exclusive — picking Top 100 supersedes Top 500.
    crmTab === "companies"
      ? el("div", { class: "smart-views" }, [
          ...COMPANY_KINDS.map((k) => {
            const active = (crmFilter.kind || "")
              .split(",")
              .map((s) => s.trim())
              .filter(Boolean)
              .includes(k.id);
            return el(
              "button",
              {
                class: `smart-view-chip${active ? " active" : ""}`,
                type: "button",
                title: active ? `Remove ${k.label} filter` : `Show only ${k.label}s`,
                onclick: () => {
                  const set = new Set(
                    (crmFilter.kind || "")
                      .split(",")
                      .map((s) => s.trim())
                      .filter(Boolean),
                  );
                  if (set.has(k.id)) set.delete(k.id);
                  else set.add(k.id);
                  crmFilter.kind = [...set].join(",");
                  crmFilter.view = "all";
                  crmFilter.page = 1;
                  loadCrm();
                  renderCrm();
                },
              },
              `${k.emoji} ${k.label}`,
            );
          }),
          // Subtle vertical divider between kind chips and top-N chips.
          el("span", {
            style: "width:1px;background:var(--border);margin:0 4px;align-self:stretch;",
          }),
          ...COMPANY_TOP_CHIPS.map((t) => {
            const active = crmFilter.top === t.id;
            return el(
              "button",
              {
                class: `smart-view-chip${active ? " active" : ""}`,
                type: "button",
                title: active ? "Clear top-N filter" : `Show ${t.label} by market cap`,
                onclick: () => {
                  crmFilter.top = active ? null : t.id;
                  crmFilter.view = "all";
                  crmFilter.page = 1;
                  loadCrm();
                  renderCrm();
                },
              },
              `${t.label}`,
            );
          }),
          // Sort chips — companies tab only. Each chip toggles a server-side
          // sort. Mutually exclusive: clicking another sort replaces, clicking
          // the active one clears (back to default updated_at DESC).
          el("span", {
            style: "width:1px;background:var(--border);margin:0 4px;align-self:stretch;",
          }),
          ...[
            {
              id: "contact_count",
              label: "Most contacts",
              emoji: "👥",
              title: "Sort companies by number of CRM contacts (desc)",
            },
            {
              id: "social_score",
              label: "Most socials",
              emoji: "🔗",
              title:
                "Sort companies by count of populated socials (website, twitter, linkedin, telegram, discord, github)",
            },
          ].map((s) => {
            const active = crmFilter.sort === s.id;
            return el(
              "button",
              {
                class: `smart-view-chip${active ? " active" : ""}`,
                type: "button",
                title: active ? "Clear sort" : s.title,
                onclick: () => {
                  crmFilter.sort = active ? null : s.id;
                  crmFilter.view = "all";
                  crmFilter.page = 1;
                  loadCrm();
                  renderCrm();
                },
              },
              `${s.emoji} ${s.label}`,
            );
          }),
        ])
      : null,
    // Filters (contacts & companies only)
    crmTab !== "activity"
      ? el("div", { class: "filter-row" }, [
          el("input", {
            class: "filter-input",
            placeholder: "Search name, company, role, handle\u2026",
            value: crmFilter.q,
            oninput: (e) => {
              crmFilter.q = e.target.value;
              crmFilter.view = "all";
              crmFilter.page = 1;
              debouncedCrmLoad();
            },
          }),
          el(
            "select",
            {
              class: "filter-select",
              onchange: (e) => {
                crmFilter.platform = e.target.value;
                crmFilter.view = "all";
                crmFilter.page = 1;
                loadCrm();
              },
            },
            [
              el("option", { value: "" }, "All platforms"),
              ...["linkedin", "twitter", "telegram", "email"].map((p) => {
                const o = el(
                  "option",
                  { value: p },
                  p === "email"
                    ? "Has email"
                    : p === "twitter"
                      ? "Twitter/X"
                      : p.charAt(0).toUpperCase() + p.slice(1),
                );
                if (p === crmFilter.platform) o.selected = true;
                return o;
              }),
            ],
          ),
          // Outreach status dropdown (contacts only)
          crmTab === "contacts"
            ? el(
                "select",
                {
                  class: "filter-select",
                  onchange: (e) => {
                    crmFilter.outreach_status = e.target.value;
                    crmFilter.view = "all";
                    crmFilter.page = 1;
                    loadCrm();
                  },
                },
                [
                  ...["", "none", "active", "replied", "converted"].map((v) => {
                    const labels = {
                      "": "All outreach",
                      none: "No outreach",
                      active: "Active outreach",
                      replied: "Replied",
                      converted: "Converted",
                    };
                    const o = el("option", { value: v }, labels[v]);
                    if (v === crmFilter.outreach_status) o.selected = true;
                    return o;
                  }),
                ],
              )
            : null,
          // Client picker (contacts only, Task 9a) — scopes the list to one
          // client's per-company ICP score + per-channel outreach status.
          crmTab === "contacts" ? buildClientPicker() : null,
          // ICP wizard launch button (Task 9b) — only once a client is picked.
          crmTab === "contacts" && crmFilter.client_id ? buildIcpWizardLaunchButton() : null,
          // Tag autocomplete (contacts only)
          crmTab === "contacts" && (dashboardState.cache.crmTags || []).length > 0
            ? buildTagAutocomplete()
            : null,
          // Sector filter
          (dashboardState.cache.crmSectors || []).length > 0
            ? el(
                "select",
                {
                  class: "filter-select",
                  onchange: (e) => {
                    crmFilter.sector = e.target.value;
                    crmFilter.view = "all";
                    crmFilter.page = 1;
                    loadCrm();
                  },
                },
                [
                  el("option", { value: "" }, "All sectors"),
                  ...(dashboardState.cache.crmSectors || []).slice(0, 30).map((s) => {
                    const o = el("option", { value: s }, s);
                    if (s === crmFilter.sector) o.selected = true;
                    return o;
                  }),
                ],
              )
            : null,
        ])
      : null,
    // Stage chips + target toggle (contacts only)
    crmTab === "contacts"
      ? el("div", { class: "filter-row", style: "gap: 4px;" }, [
          ...stages.map((s) => {
            const isActive = stageFilter === s;
            return el(
              "button",
              {
                class: `btn btn-tiny ${isActive ? "btn-primary" : "btn-secondary"}`,
                type: "button",
                onclick: () => {
                  crmFilter.stage = isActive ? "" : s;
                  crmFilter.view = "all";
                  crmFilter.page = 1;
                  loadCrm();
                  renderCrm();
                },
              },
              s === "unknown" ? "lead" : s,
            );
          }),
          stageFilter
            ? el(
                "button",
                {
                  class: "btn btn-tiny btn-danger",
                  type: "button",
                  onclick: () => {
                    crmFilter.stage = "";
                    crmFilter.view = "all";
                    crmFilter.page = 1;
                    loadCrm();
                    renderCrm();
                  },
                },
                "Clear",
              )
            : null,
          el("label", { class: "target-toggle", style: "margin-left: 12px;" }, [
            el("input", {
              type: "checkbox",
              checked: crmFilter.is_target === "1",
              onchange: (e) => {
                crmFilter.is_target = e.target.checked ? "1" : "";
                crmFilter.view = "all";
                crmFilter.page = 1;
                loadCrm();
              },
            }),
            document.createTextNode("\ud83c\udfaf Targets only"),
          ]),
        ])
      : null,
    // ICP score chips \u2014 drives `min_icp_score` (and `icp_band`). Both contacts
    // and companies share the same column semantics post-v62; the AI lead-
    // analyzer writes the score, so this row is the natural follow-up to a
    // bulk-analyze run.
    crmTab !== "activity" && crmTab !== "pipeline" && crmTab !== "inbox" && crmTab !== "sheets"
      ? buildIcpFilterRow()
      : null,
    // Missing-channel chips (v68) — companies tab only since those are
    // the channels we enrich (contacts have email/linkedin only, handled
    // by their own enrich path). Lets the operator quickly find rows
    // that still need re-enrichment.
    crmTab === "companies" ? buildMissingFilterRow() : null,
    // Active filter pills
    activePills.length > 0 ? el("div", { class: "filter-pills" }, activePills) : null,
    el("div", { id: "crm-list-wrap" }, [
      el("div", { class: "skeleton skeleton-row" }),
      el("div", { class: "skeleton skeleton-row" }),
      el("div", { class: "skeleton skeleton-row" }),
    ]),
  ]);
  setContentNode(content);
  $$(".tab").forEach((t) => t.classList.toggle("active", t.dataset.tab === crmTab));
  if (crmTab === "activity") {
    await renderActivityCardInCrm();
  } else if (crmTab === "pipeline") {
    await renderPipelineTab();
  } else if (crmTab === "inbox") {
    await renderInboxTab();
  } else if (crmTab === "sheets") {
    await renderCrmSheetsTab();
  } else {
    await loadCrm();
  }
}

// \u2500\u2500\u2500 CRM Sheets sub-tab (Phase 3) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500

async function renderCrmSheetsTab() {
  const wrap = $("#crm-list-wrap");
  if (!wrap) return;
  wrap.innerHTML = "<div class='muted'>Loading Google Sheets status\u2026</div>";

  const data = await api("crm-sheets-status").catch((err) => ({ error: err.message }));
  if (data?.error) {
    wrap.innerHTML = "";
    wrap.appendChild(el("div", { class: "muted" }, `Error: ${data.error}`));
    return;
  }

  const node = el("div", {});

  // Server config status banner
  if (!data.authReady) {
    node.appendChild(
      el("div", { class: "card" }, [
        el("div", { class: "card-title" }, "\u2699 Server config required"),
        el("div", { class: "muted", style: "font-size: 13px; line-height: 1.6;" }, [
          "Google Sheets sync is not enabled on the server. ",
          "Ask the administrator to set ",
          el("code", {}, "GOOGLE_CREDENTIALS_PATH"),
          " in the bot's .env (path to a service-account JSON).",
        ]),
      ]),
    );
    wrap.innerHTML = "";
    wrap.appendChild(node);
    return;
  }

  const hasActive = !!data.activeSheetId;
  const userOwns = data.user?.ownsActiveSheet;

  // Active sheet card
  node.appendChild(
    el("div", { class: "card" }, [
      el(
        "div",
        { class: "card-title" },
        hasActive ? "Active spreadsheet" : "No spreadsheet connected",
      ),
      hasActive
        ? el("div", {}, [
            el("div", { class: "field-row" }, [
              el("span", { class: "field-label" }, "Title"),
              el("span", { class: "field-value" }, data.user?.sheetTitle || "\u2014"),
            ]),
            el("div", { class: "field-row" }, [
              el("span", { class: "field-label" }, "URL"),
              el(
                "a",
                { class: "field-value", href: data.activeSheetUrl, target: "_blank" },
                "Open in Google Sheets \u2197",
              ),
            ]),
            el("div", { class: "field-row" }, [
              el("span", { class: "field-label" }, "Last sync"),
              el(
                "span",
                { class: "field-value mono" },
                data.lastSyncAt ? fmtDate(data.lastSyncAt) : "never",
              ),
            ]),
            !userOwns
              ? el(
                  "div",
                  {
                    class: "muted small",
                    style:
                      "margin-top: 8px; padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-1);",
                  },
                  "\u2139 Note: this spreadsheet was set by another user or env config. Creating/connecting will replace it.",
                )
              : null,
            el("div", { style: "display: flex; gap: 8px; margin-top: 12px;" }, [
              el(
                "button",
                {
                  class: "btn btn-primary btn-tiny",
                  type: "button",
                  onclick: async (ev) => {
                    await withProgress(ev.currentTarget, async () => {
                      const r = await api("crm-sheets-sync");
                      if (r?.error) toast(r.error, "err");
                      else {
                        toast("\u2705 Sync complete", "ok");
                        renderCrmSheetsTab();
                      }
                    });
                  },
                },
                "\u21bb Sync now",
              ),
              el(
                "button",
                {
                  class: "btn btn-secondary btn-tiny",
                  type: "button",
                  onclick: () => chrome.tabs.create({ url: data.activeSheetUrl }),
                },
                "Open spreadsheet \u2197",
              ),
            ]),
          ])
        : el(
            "div",
            { class: "muted", style: "font-size: 13px;" },
            "Create a new sheet (we'll share it with your Google account) or connect an existing one.",
          ),
    ]),
  );

  // Create new sheet
  let createEmail = "";
  let createTitle = "Nexus CRM";
  node.appendChild(
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Create a new spreadsheet"),
      el(
        "div",
        { class: "muted", style: "font-size: 12px; margin-bottom: 12px;" },
        "We'll create a new Google Spreadsheet and share it with you as Editor. The sheet will have 6 tabs (Dashboard / Contacts / Companies / Email Contacts / Outreach Pipeline / Campaign Activity).",
      ),
      el("div", { class: "wizard-label" }, "Your Google account email"),
      el("input", {
        class: "wizard-input",
        type: "email",
        placeholder: "you@gmail.com",
        oninput: (e) => {
          createEmail = e.target.value.trim();
        },
      }),
      el("div", { class: "wizard-label", style: "margin-top: 8px;" }, "Sheet title"),
      el("input", {
        class: "wizard-input",
        placeholder: "Nexus CRM",
        value: createTitle,
        oninput: (e) => {
          createTitle = e.target.value.trim() || "Nexus CRM";
        },
      }),
      el("div", { style: "margin-top: 12px;" }, [
        el(
          "button",
          {
            class: "btn btn-primary",
            type: "button",
            onclick: async (ev) => {
              if (!createEmail || !createEmail.includes("@")) {
                toast("Enter a valid email", "err");
                return;
              }
              await withProgress(ev.currentTarget, async () => {
                const r = await api("crm-sheets-create", {
                  body: { email: createEmail, title: createTitle },
                });
                if (r?.error) toast(r.error, "err");
                else {
                  toast("\u2705 Spreadsheet created \u2014 running first sync\u2026", "ok");
                  await api("crm-sheets-sync").catch(() => {});
                  renderCrmSheetsTab();
                }
              });
            },
          },
          "Create + share with me",
        ),
      ]),
      data.serviceAccountEmail
        ? el(
            "div",
            { class: "muted small", style: "margin-top: 8px;" },
            `Service account: ${data.serviceAccountEmail}`,
          )
        : null,
    ]),
  );

  // Connect existing
  let connectUrl = "";
  node.appendChild(
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Connect existing spreadsheet"),
      el("div", { class: "muted", style: "font-size: 12px; margin-bottom: 12px;" }, [
        "Already have a sheet? Paste the URL below. ",
        data.serviceAccountEmail
          ? el("span", {}, [
              "First share it with ",
              el("code", {}, data.serviceAccountEmail),
              " as Editor.",
            ])
          : "Make sure the bot's service account has Editor access.",
      ]),
      el("div", { class: "wizard-label" }, "Spreadsheet URL or ID"),
      el("input", {
        class: "wizard-input",
        placeholder: "https://docs.google.com/spreadsheets/d/\u2026",
        oninput: (e) => {
          connectUrl = e.target.value.trim();
        },
      }),
      el("div", { style: "margin-top: 12px;" }, [
        el(
          "button",
          {
            class: "btn btn-secondary",
            type: "button",
            onclick: async (ev) => {
              if (!connectUrl) {
                toast("Paste a sheet URL or ID first", "err");
                return;
              }
              await withProgress(ev.currentTarget, async () => {
                const r = await api("crm-sheets-connect", { body: { sheetUrl: connectUrl } });
                if (r?.error) toast(r.error, "err");
                else {
                  toast("\u2705 Connected", "ok");
                  renderCrmSheetsTab();
                }
              });
            },
          },
          "Verify + connect",
        ),
      ]),
    ]),
  );

  wrap.innerHTML = "";
  wrap.appendChild(node);
}

// \u2500\u2500\u2500 Add Contact / Add Company drawers (Phase 3) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500

// Shared labelled input/textarea row for the Add-Contact and Add-Company
// drawers. `state` is mutated by reference on input (trimmed for inputs, raw
// for textareas), so each drawer passes its own state object.
function crmFieldRow(state, label, key, opts = {}) {
  return el("div", { style: "margin-bottom: 10px;" }, [
    el("div", { class: "wizard-label" }, label + (opts.required ? " *" : "")),
    opts.multiline
      ? el("textarea", {
          class: "wizard-textarea",
          placeholder: opts.placeholder || "",
          oninput: (e) => {
            state[key] = e.target.value;
          },
        })
      : el("input", {
          class: "wizard-input",
          type: opts.type || "text",
          placeholder: opts.placeholder || "",
          oninput: (e) => {
            state[key] = e.target.value.trim();
          },
        }),
  ]);
}

function openCrmAddContactDrawer() {
  const state = {
    displayName: "",
    company: "",
    role: "",
    sector: "",
    email: "",
    twitterHandle: "",
    linkedinUrl: "",
    telegramUsername: "",
    tags: "",
    bio: "",
    notes: "",
  };

  const field = (label, key, opts = {}) => crmFieldRow(state, label, key, opts);

  const body = el("div", { class: "drawer-body" }, [
    el(
      "div",
      { class: "muted", style: "font-size: 13px; margin-bottom: 12px;" },
      "Manually add a contact to your CRM. Only Display Name is required \u2014 everything else is optional.",
    ),

    field("Display name", "displayName", { required: true, placeholder: "Jane Doe" }),
    el("div", { class: "row", style: "gap: 8px;" }, [
      el("div", { style: "flex: 1;" }, [field("Role", "role", { placeholder: "Founder" })]),
      el("div", { style: "flex: 1;" }, [field("Company", "company", { placeholder: "AcmeCo" })]),
    ]),
    field("Sector", "sector", { placeholder: "DeFi / Infra / etc." }),

    el("div", { class: "card-title", style: "margin-top: 8px;" }, "Contact info"),
    field("Email", "email", { type: "email", placeholder: "jane@acmeco.io" }),
    field("Twitter / X handle", "twitterHandle", { placeholder: "@janedoe" }),
    field("LinkedIn URL", "linkedinUrl", { placeholder: "https://linkedin.com/in/janedoe" }),
    field("Telegram username", "telegramUsername", { placeholder: "janedoe" }),

    el("div", { class: "card-title", style: "margin-top: 8px;" }, "Extras"),
    field("Tags (comma-separated)", "tags", { placeholder: "warm, partner, defi" }),
    field("Bio", "bio", { multiline: true, placeholder: "Short context on this contact" }),
    field("Initial note", "notes", {
      multiline: true,
      placeholder: "How did you meet? Why are they interesting?",
    }),

    el("div", { style: "display: flex; gap: 8px; margin-top: 16px;" }, [
      el(
        "button",
        {
          class: "btn btn-primary",
          type: "button",
          onclick: async (ev) => {
            if (!state.displayName) {
              toast("Display name is required", "err");
              return;
            }
            await withProgress(ev.currentTarget, async () => {
              const r = await api("crm-contacts-create", { body: state });
              if (r?.error) toast(r.error, "err");
              else {
                toast("\u2705 Contact added", "ok");
                closeDrawer();
                dashboardState.cache.crmTags = null; // bust caches so new tags show
                dashboardState.cache.crmSectors = null;
                renderCrm();
              }
            });
          },
        },
        "Add contact",
      ),
      el("button", { class: "btn btn-secondary", type: "button", onclick: closeDrawer }, "Cancel"),
    ]),
  ]);

  openDrawer("Add contact", body);
}

function openCrmAddCompanyDrawer() {
  const state = {
    name: "",
    website: "",
    sector: "",
    sub_sector: "",
    description: "",
    twitter_handle: "",
    linkedin_url: "",
    telegram_group_url: "",
    token_symbol: "",
    stage: "",
    country: "",
    tags: "",
    is_target: 0,
  };

  const field = (label, key, opts = {}) => crmFieldRow(state, label, key, opts);

  const body = el("div", { class: "drawer-body" }, [
    el(
      "div",
      { class: "muted", style: "font-size: 13px; margin-bottom: 12px;" },
      "Manually add a company to your CRM. Only Name is required.",
    ),

    field("Company name", "name", { required: true, placeholder: "AcmeCo" }),
    field("Website", "website", { placeholder: "https://acmeco.io" }),
    el("div", { class: "row", style: "gap: 8px;" }, [
      el("div", { style: "flex: 1;" }, [field("Sector", "sector", { placeholder: "DeFi" })]),
      el("div", { style: "flex: 1;" }, [
        field("Sub-sector", "sub_sector", { placeholder: "Lending" }),
      ]),
    ]),
    el("div", { class: "row", style: "gap: 8px;" }, [
      el("div", { style: "flex: 1;" }, [
        field("Stage", "stage", { placeholder: "seed / series_a / public" }),
      ]),
      el("div", { style: "flex: 1;" }, [
        field("Country", "country", { placeholder: "United States" }),
      ]),
    ]),
    field("Description", "description", {
      multiline: true,
      placeholder: "What does the company do?",
    }),

    el("div", { class: "card-title", style: "margin-top: 8px;" }, "Socials"),
    field("Twitter handle", "twitter_handle", { placeholder: "@acmeco" }),
    field("LinkedIn URL", "linkedin_url", { placeholder: "https://linkedin.com/company/acmeco" }),
    field("Telegram group URL", "telegram_group_url", { placeholder: "https://t.me/acmeco" }),
    field("Token symbol", "token_symbol", { placeholder: "ACME" }),

    el("div", { class: "card-title", style: "margin-top: 8px;" }, "CRM"),
    field("Tags (comma-separated)", "tags", { placeholder: "outbound, abm, target" }),
    el("div", { class: "toggle-row", style: "padding: 8px 0;" }, [
      el("span", { class: "toggle-label" }, "Mark as target"),
      el("div", {
        class: `toggle ${state.is_target ? "on" : ""}`,
        onclick: (ev) => {
          state.is_target = state.is_target ? 0 : 1;
          ev.currentTarget.classList.toggle("on", !!state.is_target);
        },
      }),
    ]),

    el("div", { style: "display: flex; gap: 8px; margin-top: 16px;" }, [
      el(
        "button",
        {
          class: "btn btn-primary",
          type: "button",
          onclick: async (ev) => {
            if (!state.name) {
              toast("Company name is required", "err");
              return;
            }
            await withProgress(ev.currentTarget, async () => {
              const r = await api("crm-companies-create", { body: state });
              if (r?.error) toast(r.error, "err");
              else {
                toast("\u2705 Company added", "ok");
                closeDrawer();
                dashboardState.cache.crmSectors = null;
                renderCrm();
              }
            });
          },
        },
        "Add company",
      ),
      el("button", { class: "btn btn-secondary", type: "button", onclick: closeDrawer }, "Cancel"),
    ]),
  ]);

  openDrawer("Add company", body);
}

async function renderPipelineTab() {
  const wrap = $("#crm-list-wrap");
  if (!wrap) return;
  wrap.innerHTML = "<div class='muted'>Loading pipeline\u2026</div>";

  try {
    const data = await api("crm-pipeline");
    if (data?.error) {
      wrap.innerHTML = `<div class="muted">${esc(data.error)}</div>`;
      return;
    }

    const stages = data.stages || {};
    const total = data.total || 0;
    const stageOrder = [
      "cold",
      "contacted",
      "replied",
      "engaged",
      "warm",
      "hot",
      "customer",
      "dead",
    ];
    const stageIcons = {
      cold: "\u2744\ufe0f",
      contacted: "\ud83d\udce8",
      replied: "\ud83d\udcac",
      engaged: "\ud83d\udd25",
      warm: "\u2600\ufe0f",
      hot: "\ud83d\udca5",
      customer: "\u2705",
      dead: "\ud83d\udc80",
    };
    const maxCount = Math.max(...Object.values(stages), 1);

    const rows = stageOrder
      .filter((s) => stages[s] > 0)
      .map((s) => {
        const pct = Math.round((stages[s] / maxCount) * 100);
        return el(
          "div",
          {
            class: "pipeline-row",
            style:
              "display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);",
          },
          [
            el("span", { style: "width:24px;text-align:center;" }, stageIcons[s] || ""),
            el("span", { style: "width:90px;font-weight:600;" }, s),
            el(
              "div",
              {
                style:
                  "flex:1;height:20px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden;",
              },
              [
                el("div", {
                  style: `height:100%;width:${pct}%;background:var(--accent);border-radius:4px;transition:width 0.3s;`,
                }),
              ],
            ),
            el(
              "span",
              { style: "width:50px;text-align:right;font-weight:700;" },
              String(stages[s]),
            ),
          ],
        );
      });

    wrap.innerHTML = "";
    wrap.appendChild(
      el("div", { class: "card" }, [
        el("div", { class: "card-title" }, `Contact Pipeline (${total} total)`),
        ...rows,
        rows.length === 0 ? el("div", { class: "muted" }, "No contacts in pipeline yet.") : null,
      ]),
    );
  } catch (err) {
    wrap.innerHTML = `<div class="muted">\u274c ${esc(err.message)}</div>`;
  }
}

async function renderInboxTab() {
  const wrap = $("#crm-list-wrap");
  if (!wrap) return;
  wrap.innerHTML = "<div class='muted'>Loading inbox\u2026</div>";

  try {
    const data = await api("crm-inbox", { queryString: "?limit=30" });
    if (data?.error) {
      wrap.innerHTML = `<div class="muted">${esc(data.error)}</div>`;
      return;
    }

    const { messages = [], stats = {} } = data;
    const platformIcons = {
      telegram: "\ud83d\udcac",
      linkedin: "\ud83d\udcbc",
      email: "\ud83d\udce7",
    };
    const intentColors = {
      deal_interest: "#0d652d",
      partnership: "#1557b0",
      referral: "#7c6bff",
      spam: "#d93025",
    };

    // Stats bar
    const statsParts = [];
    if (stats.telegram?.unhandled) statsParts.push(`\ud83d\udcac TG: ${stats.telegram.unhandled}`);
    if (stats.linkedin?.unhandled) statsParts.push(`\ud83d\udcbc LI: ${stats.linkedin.unhandled}`);
    if (stats.email?.unhandled) statsParts.push(`\ud83d\udce7 EM: ${stats.email.unhandled}`);
    if (stats.newContacts) statsParts.push(`\ud83c\udd95 ${stats.newContacts} new contacts`);
    if (stats.potentialDeals) statsParts.push(`\u26a1 ${stats.potentialDeals} potential deals`);

    const rows = messages.map((m) => {
      const icon = platformIcons[m.platform] || "\ud83d\udcac";
      const intentBadge =
        m.intent_classification && m.intent_classification !== "general"
          ? el(
              "span",
              {
                class: "badge",
                style: `background:${intentColors[m.intent_classification] || "#5f6368"}22;color:${intentColors[m.intent_classification] || "#5f6368"};font-size:10px;`,
              },
              m.intent_classification,
            )
          : null;
      const knownBadge = !m.is_from_known_contact
        ? el("span", { class: "badge badge-new", style: "font-size:10px;" }, "NEW")
        : null;

      return el(
        "div",
        {
          class: `inbox-row${m.is_handled ? " handled" : ""}`,
          style:
            "display:flex;gap:10px;padding:10px 0;border-bottom:1px solid var(--border);align-items:flex-start;",
        },
        [
          el("span", { style: "font-size:18px;flex-shrink:0;" }, icon),
          el("div", { style: "flex:1;min-width:0;" }, [
            el("div", { style: "display:flex;gap:6px;align-items:center;" }, [
              el("span", { class: "bold" }, esc(m.sender_name || "Unknown")),
              m.sender_company
                ? el("span", { class: "muted small" }, `@ ${esc(m.sender_company)}`)
                : null,
              knownBadge,
              intentBadge,
            ]),
            el(
              "div",
              { class: "muted small", style: "margin-top:2px;" },
              esc((m.message_preview || "").slice(0, 80)),
            ),
          ]),
          el("div", { style: "flex-shrink:0;text-align:right;" }, [
            el(
              "div",
              { class: "muted", style: "font-size:10px;" },
              m.received_at?.slice(0, 10) || "",
            ),
            m.contact_display_name
              ? el(
                  "div",
                  { class: "muted", style: "font-size:10px;" },
                  `CRM: ${esc(m.contact_display_name)}`,
                )
              : null,
            !m.is_handled
              ? el(
                  "button",
                  {
                    class: "btn btn-tiny btn-secondary",
                    style: "margin-top:4px;font-size:10px;padding:2px 6px;",
                    onclick: async (e) => {
                      e.target.disabled = true;
                      await api("crm-inbox-handled", { body: { id: m.id } });
                      renderInboxTab();
                    },
                  },
                  "\u2705",
                )
              : null,
          ]),
        ],
      );
    });

    wrap.innerHTML = "";
    wrap.appendChild(
      el("div", { class: "card" }, [
        el(
          "div",
          { class: "card-title" },
          `Unified Inbox (${stats.totalUnhandled || 0} unhandled)`,
        ),
        statsParts.length > 0
          ? el("div", { class: "muted small", style: "margin-bottom:8px;" }, statsParts.join(" | "))
          : null,
        ...rows,
        rows.length === 0
          ? el(
              "div",
              { class: "muted" },
              "No inbox messages yet. Messages from Telegram and LinkedIn will appear here.",
            )
          : null,
      ]),
    );
  } catch (err) {
    wrap.innerHTML = `<div class="muted">\u274c ${esc(err.message)}</div>`;
  }
}

let crmDebounceTimer = null;
function debouncedCrmLoad() {
  clearTimeout(crmDebounceTimer);
  crmDebounceTimer = setTimeout(loadCrm, 400);
}

async function loadCrm() {
  const wrap = $("#crm-list-wrap");
  if (!wrap) return;
  const qs = `?${new URLSearchParams({
    ...(crmFilter.q && { q: crmFilter.q }),
    ...(crmFilter.tag && { tag: crmFilter.tag }),
    ...(crmFilter.sector && { sector: crmFilter.sector }),
    ...(crmFilter.platform && { platform: crmFilter.platform }),
    ...(crmFilter.stage && { stage: crmFilter.stage }),
    ...(crmFilter.outreach_status && { outreach_status: crmFilter.outreach_status }),
    ...(crmFilter.is_target && { is_target: crmFilter.is_target }),
    ...(crmFilter.lead_score_min && { lead_score_min: String(crmFilter.lead_score_min) }),
    ...(crmFilter.last_contact_days && { last_contact_days: String(crmFilter.last_contact_days) }),
    // kind filter only applies to Companies tab; ignored by contacts endpoint
    ...(crmTab === "companies" && crmFilter.kind && { kind: crmFilter.kind }),
    // AI verdict filter only applies to Contacts tab; v58 (Hot leads view).
    ...(crmTab === "contacts" && crmFilter.ai_verdict && { ai_verdict: crmFilter.ai_verdict }),
    ...(crmTab === "contacts" &&
      crmFilter.ai_confidence_min && { ai_confidence_min: String(crmFilter.ai_confidence_min) }),
    // ICP filters (v62) — applied on both Contacts and Companies tabs.
    ...(crmFilter.min_icp_score && { min_icp_score: String(crmFilter.min_icp_score) }),
    ...(crmFilter.icp_band && { icp_band: crmFilter.icp_band }),
    // Missing-channel filter (v68) — companies tab only.
    ...(crmTab === "companies" && crmFilter.missing && { missing: crmFilter.missing }),
    // Top-N market-cap filter (v59), Companies tab only — server defaults sort to rank ASC when set.
    ...(crmTab === "companies" && crmFilter.top && { top: String(crmFilter.top) }),
    // Client scope (Task 9a), Contacts tab only — turns on client_icp_score/
    // band + the four per-channel outreach statuses on each returned row.
    ...(crmTab === "contacts" && crmFilter.client_id && { client_id: crmFilter.client_id }),
    ...(crmFilter.sort && crmFilter.sort !== "updated_at" && { sort: crmFilter.sort }),
    page: crmFilter.page,
    limit: 50,
  }).toString()}`;

  const data = await api(crmTab === "contacts" ? "crm-contacts-list" : "crm-companies-list", {
    queryString: qs,
  });
  if (data?.error) {
    wrap.innerHTML = `<div class="empty-state"><div class="empty-state-title">Error</div><div class="empty-state-hint">${escapeHtml(data.error)}</div></div>`;
    return;
  }
  const rows = data[crmTab] || [];
  if (rows.length === 0) {
    wrap.innerHTML = `<div class="empty-state"><div class="empty-state-icon">🗂</div><div class="empty-state-title">No ${crmTab}</div></div>`;
    return;
  }

  // Cache the visible row ids so the bulk-analyze button can pick them up
  // without re-fetching. Captured for both contacts and companies tabs.
  if (crmTab === "contacts") {
    _lastCrmContactIds = rows.map((r) => r.id).filter((id) => Number.isFinite(id));
    // Also cache the full rows so the bulk-campaign modal can read tag
    // overlap + channel eligibility without an extra round-trip.
    _lastCrmContactRows = rows;
  } else if (crmTab === "companies") {
    _lastCrmCompanyIds = rows.map((r) => r.id).filter((id) => Number.isFinite(id));
  }

  // Refresh the per-tab bookmarked-id Set before rendering so each row's
  // star button shows the right initial state. One bulk fetch beats
  // 50 isBookmarked round-trips.
  await loadBookmarkedIdsForTab(crmTab);

  wrap.innerHTML = "";
  wrap.appendChild(renderCrmTable(rows, crmTab));
  injectClientColumns(rows);
  wrap.appendChild(
    buildPagination(data.page, data.totalPages, data.total, (p) => {
      crmFilter.page = p;
      loadCrm();
    }),
  );
}

// IDs of the contacts/companies currently shown in the CRM list view.
// Captured by loadCrm() each time the relevant tab renders. Used by the
// bulk AI analyze button so we send exactly what the operator can see.
let _lastCrmContactIds = [];
let _lastCrmCompanyIds = [];
let _lastCrmContactRows = []; // rich rows for bulk-campaign modal context

function renderScoreBar(score) {
  const s = score || 0;
  const cls = s >= 76 ? "score-hot" : s >= 51 ? "score-high" : s >= 21 ? "score-mid" : "score-low";
  return el("div", { class: "score-bar-wrap" }, [
    el("div", { class: "score-bar" }, [
      el("div", { class: `score-bar-fill ${cls}`, style: `width: ${Math.min(100, s)}%;` }),
    ]),
    el("span", { class: "score-num" }, String(s)),
  ]);
}

function renderEngagement(c) {
  const items = [
    { label: "LI", count: c.li_engagement_count || 0 },
    { label: "X", count: c.x_engagement_count || 0 },
    { label: "\u2709", count: c.email_engagement_count || 0 },
  ];
  return el(
    "div",
    { class: "engagement-mini" },
    items.map((i) =>
      el("span", { class: `eng-item ${i.count > 0 ? "active" : "zero"}` }, [
        el("span", { class: "eng-label" }, i.label),
        document.createTextNode(String(i.count)),
      ]),
    ),
  );
}

// Render the v57 kind classification as compact badges. NULL = no badge
// (we don't visualize "unknown" vs "checked but negative" \u2014 both render
// as nothing). Multi-positive (e.g. token + investor) shows multiple
// badges side by side.
function renderCompanyKindBadges(c) {
  const wrap = el("div", { class: "row", style: "gap: 3px; flex-wrap: wrap;" });
  let any = false;
  for (const k of COMPANY_KINDS) {
    if (c[k.column] === 1) {
      any = true;
      wrap.appendChild(
        el(
          "span",
          {
            class: "badge badge-purple",
            title: k.label,
            style: "font-size: 9px; padding: 1px 6px;",
          },
          `${k.emoji} ${k.short}`,
        ),
      );
    }
  }
  if (!any) {
    return el("span", { class: "muted small" }, c.kind_classified_at ? "\u2014" : "?");
  }
  return wrap;
}

// Change a contact's stage, then show a 5-second undo toast. Clicking
// Undo inside the window reverts back to the previous stage. Used by the
// inline stage picker and the row dropdown — both can fat-finger easily.
async function changeStageWithUndo(contactId, fromStage, toStage, opts = {}) {
  if (fromStage === toStage) return;
  const r = await api("crm-contact-update", { contactId, body: { stage: toStage } });
  if (!r?.ok) {
    toast(`Error: ${r?.error || "failed"}`, "err");
    return;
  }
  if (opts.onSuccess) opts.onSuccess();
  showUndoToast(`Stage → ${toStage}`, async () => {
    const back = await api("crm-contact-update", {
      contactId,
      body: { stage: fromStage || "unknown" },
    });
    if (back?.ok) {
      toast("Reverted", "ok", 1500);
      if (opts.onSuccess) opts.onSuccess();
    } else {
      toast(`Undo failed: ${back?.error || "unknown"}`, "err");
    }
  });
}

// Toast with an inline Undo button + animated progress bar showing the
// time remaining. Auto-dismisses after `durationMs` (default 5s).
function showUndoToast(text, onUndo, durationMs = 5000) {
  const region = $("#toast-region");
  if (!region) return;
  while (region.children.length >= 3) region.firstChild.remove();
  let dismissed = false;
  const t = el("div", { class: "toast ok with-undo" });
  t.appendChild(el("span", {}, text));
  t.appendChild(
    el(
      "button",
      {
        class: "toast-undo",
        type: "button",
        onclick: async () => {
          if (dismissed) return;
          dismissed = true;
          t.remove();
          try {
            await onUndo();
          } catch (err) {
            toast(`Undo failed: ${err.message}`, "err");
          }
        },
      },
      "Undo",
    ),
  );
  t.appendChild(
    el(
      "button",
      {
        class: "toast-dismiss",
        onclick: () => {
          dismissed = true;
          t.remove();
        },
      },
      "×",
    ),
  );
  const bar = el("div", { class: "toast-progress" });
  bar.style.animationDuration = `${durationMs}ms`;
  t.appendChild(bar);
  region.appendChild(t);
  setTimeout(() => {
    if (!dismissed && t.parentNode) t.remove();
  }, durationMs);
}

function showInlineStageEditor(contactId, currentStage, anchorEl) {
  // Remove any existing picker
  const existing = document.querySelector(".inline-stage-picker");
  if (existing) existing.remove();

  const validStages = ["cold", "contacted", "replied", "warm", "hot", "customer", "dead"];
  // Single dismissal path so the document listener is always detached. The
  // stage-button click stopPropagation()s (so the outside-click handler never
  // fires), which previously orphaned the listener; close() removes both.
  let picker;
  const handler = (e) => {
    if (picker && !picker.contains(e.target)) close();
  };
  const close = () => {
    if (picker) picker.remove();
    document.removeEventListener("click", handler);
  };
  picker = el(
    "div",
    { class: "inline-stage-picker" },
    validStages.map((s) =>
      el(
        "button",
        {
          class: `btn btn-tiny ${currentStage === s ? "btn-primary" : "btn-secondary"}`,
          type: "button",
          onclick: async (e) => {
            e.stopPropagation();
            close();
            await changeStageWithUndo(contactId, currentStage, s, { onSuccess: () => loadCrm() });
          },
        },
        s,
      ),
    ),
  );

  // Position near anchor
  anchorEl.style.position = "relative";
  anchorEl.appendChild(picker);

  setTimeout(() => {
    document.addEventListener("click", handler);
  }, 10);
}
