/**
 * Nexus Dashboard — full-page extension UI.
 *
 * Architecture:
 *   - Single dashboardState object (no framework)
 *   - Hash-based router (#home, #campaigns, #signals, #vc, #crm, #signal-pursue/123, etc.)
 *   - All API calls go through background.js via chrome.runtime.sendMessage
 *   - Each section has a render function that targets #content
 *   - Each wizard has its own sub-state inside dashboardState
 */

// ── State ─────────────────────────────────────────────────────────

const dashboardState = {
  online: false,
  token: null,
  apiBase: null,
  currentRoute: "home",
  currentParams: {},
  // Cached data per section. (API read-caching lives in the TTL map below —
  // apiCached / API_CACHE_TTL / _apiCacheEntries — not here.)
  cache: {
    crmTags: null,
    crmSectors: null,
    accounts: null, // { platform: [...] }, populated lazily (accounts-all)
  },
  // Active wizard state
  signalWizard: null,
  vcWizard: null,
  // Sort state per view
  sort: {
    campaigns: { key: "created_at", dir: "desc" },
    crm: { key: "updated_at", dir: "desc" },
  },
  // CRM bulk selection (v63). One Set per tab, keyed by row id. Reset on
  // tab switch / route change. Drives the floating selection bar.
  crmSelection: {
    type: null, // "companies" | "contacts" | null
    ids: new Set(),
  },
  // Top-bar account context (v3 nav). null = "All accounts" (no filter).
  // When set, page renderers can opt into filtering by this account; for now
  // it's a pure UI affordance signalling which account the user is "in".
  contextAccount: null, // null | { platform, id, label, status }
  // Capabilities that gate nav sections (see V3_SECTIONS `visibleWhen`).
  // Seeded from the last known value in chrome.storage so the sidebar doesn't
  // flicker a room in and out on every load, then refreshed from real data.
  // Defaults TRUE: hiding a room the user actually needs is worse than briefly
  // showing an empty one, and the server scopes the data either way.
  navCaps: { hasClients: true },
};

/** The context passed to a nav section's `visibleWhen` predicate. */
function navCapabilities() {
  return dashboardState.navCaps;
}

/**
 * Update a nav capability from real data and rebuild the sidebar only if it
 * actually changed (a rebuild on every fetch would fight the active-state).
 * Persisted so the next load starts from the truth instead of the default.
 */
function setNavCapability(key, value) {
  const next = !!value;
  if (dashboardState.navCaps[key] === next) return;
  dashboardState.navCaps[key] = next;
  try {
    chrome.storage.local.set({ [`_navCap_${key}`]: next });
  } catch {
    // Storage unavailable (the /web build shims chrome.*) — the in-memory value
    // still applies for this session; only the cross-load seed is lost.
  }
  const sidebar = document.getElementById("sidebar");
  if (sidebar && sidebar.classList.contains("sidebar-v3")) buildSidebar();
}

// ── Sort helper ──────────────────────────────────────────────────

function sortRows(rows, key, dir) {
  if (!key) return rows;
  return [...rows].sort((a, b) => {
    let va = a[key],
      vb = b[key];
    // Null-safe: nulls sort last
    if (va == null && vb == null) return 0;
    if (va == null) return 1;
    if (vb == null) return -1;
    // Numeric
    if (typeof va === "number" && typeof vb === "number") return dir === "asc" ? va - vb : vb - va;
    // Date strings
    if (typeof va === "string" && /^\d{4}-/.test(va)) {
      const da = new Date(va).getTime(),
        db = new Date(vb).getTime();
      if (!isNaN(da) && !isNaN(db)) return dir === "asc" ? da - db : db - da;
    }
    // String
    va = String(va).toLowerCase();
    vb = String(vb).toLowerCase();
    return dir === "asc" ? va.localeCompare(vb) : vb.localeCompare(va);
  });
}

function makeSortableHeader(columns, sortState, onSort) {
  return el("thead", {}, [
    el(
      "tr",
      {},
      columns.map((col) => {
        if (!col.key) return el("th", {}, col.label || "");
        const isSorted = sortState.key === col.key;
        const arrow = isSorted ? (sortState.dir === "asc" ? " \u25b2" : " \u25bc") : "";
        return el(
          "th",
          {
            class: `sortable${isSorted ? " sorted" : ""}`,
            onclick: () => {
              if (sortState.key === col.key) {
                sortState.dir = sortState.dir === "asc" ? "desc" : "asc";
              } else {
                sortState.key = col.key;
                sortState.dir = "desc";
              }
              onSort();
            },
          },
          [col.label || col.key, el("span", { class: "sort-arrow" }, arrow)],
        );
      }),
    ),
  ]);
}

// ── API helpers (via background.js) ───────────────────────────────

function _apiSendOnce(action, payload) {
  return new Promise((resolve, reject) => {
    try {
      chrome.runtime.sendMessage({ action, ...payload }, (response) => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
          return;
        }
        // Global auth-expiry hook: any 401 response from the bot triggers
        // the re-pair overlay once. After dismissal, individual handlers
        // still see the {unauthorized, error} object and can render their
        // own empty-state. Idempotent — overlay only shown once per session.
        if (response && response.unauthorized) {
          showAuthExpiredOverlay(action);
        }
        resolve(response);
      });
    } catch (err) {
      reject(err);
    }
  });
}

async function api(action, payload = {}) {
  try {
    return await _apiSendOnce(action, payload);
  } catch (err) {
    // MV3 cold-start: the first message to a spun-down/booting service worker
    // can drop with "message port closed before a response was received"
    // before its listeners are ready. Retry once after a short beat — this
    // recovers the common dormant-worker case. (A genuinely stale worker —
    // one running OLD code without this action's handler — will fail twice;
    // that surfaces as the same error and the caller's reload hint applies.)
    if (/message port closed/i.test(err.message || "")) {
      await new Promise((r) => setTimeout(r, 350));
      return _apiSendOnce(action, payload);
    }
    throw err;
  }
}

// ── In-memory TTL cache for repeated read-only API calls ───────────────
//
// Why: dashboard renderers tend to refetch the same endpoints (accounts-all,
// crm-stats, campaigns-stats, inbox-replies) on every navigation. With ~200ms
// per round-trip and frequent route switching, this adds up fast. A short TTL
// (15-60s) eliminates redundant fetches without making data feel stale.
//
// How: opt-in via apiCached(action, payload?). Existing `api()` calls are
// unchanged — only hot paths get migrated. Mutating endpoints call
// invalidateApiCache(action) to bust dependent reads (or pass through api()
// directly + manual invalidation for full control).

const API_CACHE_TTL = {
  "accounts-all": 60_000, // accounts rarely change mid-session
  "settings-get": 60_000, // user preferences — cold
  "crm-stats": 30_000, // pipeline counts — moderate volatility
  "campaigns-stats": 20_000, // campaign rollups — slightly faster
  "workflows-list": 30_000, // running-workflow counts in Operations catalog
  "inbox-replies": 15_000, // replies land fast — keep tight
  "crm-activity": 20_000, // activity feed
};

const _apiCacheEntries = new Map();

function _apiCacheKey(action, payload) {
  if (!payload || (typeof payload === "object" && Object.keys(payload).length === 0)) return action;
  // Stable JSON for payload — used to key variants of the same action.
  try {
    return action + ":" + JSON.stringify(payload);
  } catch {
    return action;
  } // payload not stringifiable — fall back to action key (effectively uncached for variants)
}

// Drop-in replacement for api() on read-only endpoints. Returns cached value
// if fresh; otherwise fetches and caches. Errors are NOT cached (they retry).
async function apiCached(action, payload = {}, opts = {}) {
  const ttl = opts.ttl ?? API_CACHE_TTL[action];
  if (!ttl) return api(action, payload); // no TTL configured — bypass cache
  const key = _apiCacheKey(action, payload);
  const cached = _apiCacheEntries.get(key);
  if (cached && Date.now() - cached.ts < ttl) return cached.value;
  const value = await api(action, payload);
  if (value && !value.error) _apiCacheEntries.set(key, { ts: Date.now(), value });
  return value;
}

// Bust cache for one action (any payload variant) or all when called bare.
// Call after mutations that affect cached reads.
function invalidateApiCache(action) {
  if (!action) {
    _apiCacheEntries.clear();
    return;
  }
  for (const k of [..._apiCacheEntries.keys()]) {
    if (k === action || k.startsWith(action + ":")) _apiCacheEntries.delete(k);
  }
}

// Map from mutating action → list of cached reads to bust.
// Auto-applied by apiMutate() below. Add entries as new mutators ship.
const _MUTATION_INVALIDATIONS = {
  "workflow-create": ["campaigns-stats", "workflows-list"],
  "workflow-toggle": ["campaigns-stats", "workflows-list"],
  "workflow-delete": ["campaigns-stats", "workflows-list"],
  "group-campaign-create": ["campaigns-stats"],
  "create-dm-campaign": ["campaigns-stats"],
  "inbox-reply": ["inbox-replies"],
  "crm-contacts-create": ["crm-stats"],
  "crm-companies-create": ["crm-stats"],
  "crm-contacts-update": ["crm-stats"],
  "crm-companies-update": ["crm-stats"],
};

// Convenience: api() + auto-invalidate matching cached reads on success.
// Use for mutating actions where you want the cache to immediately reflect
// the change. For one-off mutations where invalidation doesn't matter, just
// keep using api() directly.
async function apiMutate(action, payload = {}) {
  const result = await api(action, payload);
  if (result && !result.error) {
    const invalidations = _MUTATION_INVALIDATIONS[action];
    if (invalidations) for (const target of invalidations) invalidateApiCache(target);
  }
  return result;
}

// ── Auth-expired overlay ─────────────────────────────────────────────
// Shown once per page load when any /api/* call returns 401. Surfaces
// the /pair flow so users can recover without leaving the dashboard.
let _authOverlayShown = false;
function showAuthExpiredOverlay(triggeringAction) {
  if (_authOverlayShown) return;
  _authOverlayShown = true;

  // Existing overlay backdrop + drawer reused for visual consistency.
  const backdrop = el("div", {
    style:
      "position: fixed; inset: 0; background: rgba(0,0,0,0.65); z-index: 9999; display: flex; align-items: center; justify-content: center;",
  });
  const card = el(
    "div",
    {
      class: "card",
      style: "max-width: 460px; padding: 28px; box-shadow: 0 10px 40px rgba(0,0,0,0.4);",
    },
    [
      el(
        "div",
        { style: "font-size: 40px; text-align: center; margin-bottom: 12px;" },
        icon("lock"),
      ),
      el(
        "div",
        { class: "card-title", style: "font-size: 18px; text-align: center; margin-bottom: 8px;" },
        "Session expired",
      ),
      el(
        "div",
        {
          class: "muted",
          style: "font-size: 13px; line-height: 1.6; text-align: center; margin-bottom: 16px;",
        },
        "Your connection token expired. They last 30 days for security. Open the Telegram bot, run /pair, and paste the new code into the extension popup.",
      ),
      el(
        "div",
        {
          class: "muted small",
          style: "font-family: var(--font-mono); margin-bottom: 16px; text-align: center;",
        },
        `Triggered by: ${triggeringAction}`,
      ),
      el("div", { style: "display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;" }, [
        el(
          "button",
          {
            class: "btn btn-primary",
            type: "button",
            onclick: () => openTgCommand("/pair"),
          },
          "Open /pair in Telegram",
        ),
        el(
          "button",
          {
            class: "btn btn-secondary",
            type: "button",
            onclick: () => chrome.tabs.create({ url: chrome.runtime.getURL("popup.html") }),
          },
          "Open extension popup",
        ),
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            onclick: () => {
              backdrop.remove();
              _authOverlayShown = false;
            },
          },
          "Dismiss",
        ),
      ]),
      el(
        "div",
        { class: "muted small", style: "text-align: center; margin-top: 12px;" },
        "After pairing, refresh this tab with Ctrl+R.",
      ),
    ],
  );
  backdrop.appendChild(card);
  backdrop.addEventListener("click", (e) => {
    if (e.target === backdrop) {
      backdrop.remove();
      _authOverlayShown = false;
    }
  });
  document.body.appendChild(backdrop);
}

// ── DOM helpers ──────────────────────────────────────────────────

const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
const el = (tag, attrs = {}, children = []) => {
  const n = document.createElement(tag);
  for (const [k, v] of Object.entries(attrs || {})) {
    if (k === "class") n.className = v;
    else if (k === "html") n.innerHTML = v;
    else if (k.startsWith("on")) n.addEventListener(k.slice(2).toLowerCase(), v);
    else if (v != null) n.setAttribute(k, v);
  }
  for (const c of Array.isArray(children) ? children : [children]) {
    if (c == null) continue;
    if (typeof c === "string") n.appendChild(document.createTextNode(c));
    else n.appendChild(c);
  }
  return n;
};
const escapeHtml = (s) =>
  String(s ?? "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
const esc = escapeHtml;

// ── Shared UI builders (2026-05-31) ────────────────────────────────
// The dashboard hand-styled every card/modal with copy-pasted inline strings
// (the audit found the same card style 8+ times and 6 separate modal
// implementations). These builders give new code ONE consistent, accessible
// source. They emit token-based styling and bake in keyboard support so we
// stop shipping inaccessible clickable <div>s. Migrate hot spots to them;
// they compose with el() so adoption can be incremental.

// A surface card. When `onClick` is given it becomes a real button to the
// keyboard + screen reader (role + tabindex + Enter/Space), with the hover
// affordance the old inline cards had. `ariaLabel` names it for SRs.
function uiCard({ onClick, ariaLabel, style = "", interactive } = {}, children = []) {
  const clickable = typeof onClick === "function";
  const node = el(
    "div",
    {
      class: "ui-card" + (clickable ? " ui-card-interactive" : ""),
      role: clickable ? "button" : null,
      tabindex: clickable ? "0" : null,
      "aria-label": ariaLabel || null,
      // Surface, border, radius, padding and the hover/focus states all live in
      // .ui-card / .ui-card-interactive in dashboard.css now. `style` is still
      // honoured so callers can override per instance, but the DEFAULTS are no
      // longer written here: an inline style outranks the stylesheet, so the
      // class this element already carried could never style it.
      style: style || null,
    },
    children,
  );
  if (clickable) {
    node.addEventListener("click", onClick);
    node.addEventListener("keydown", (e) => {
      if (e.key === "Enter" || e.key === " ") {
        e.preventDefault();
        onClick(e);
      }
    });
  }
  return node;
}

// A button. Thin wrapper over the existing .btn classes so callers stop
// hand-writing `class:"btn btn-primary"` + inline styles, and so icon-only
// buttons are forced to carry an aria-label.
function uiButton(
  label,
  {
    variant = "secondary",
    onClick,
    title,
    ariaLabel,
    type = "button",
    size,
    style = "",
    disabled,
  } = {},
) {
  const cls = ["btn", `btn-${variant}`];
  if (size === "tiny") cls.push("btn-tiny");
  return el(
    "button",
    {
      type,
      class: cls.join(" "),
      title: title || null,
      "aria-label": ariaLabel || (typeof label === "string" ? null : "button"),
      disabled: disabled ? "" : null,
      style: style || null,
      onclick: onClick || null,
    },
    label,
  );
}

// ── Page-header + empty-state builders (2026-07-21) ────────────────
// ~70 view renderers hand-built the identical three-node page header and the
// icon/title/hint empty-state surface (the audit counted 77 headers and 69
// empty-states). These emit exactly that DOM — same tags, class names and
// nesting — so CSS and behavior are unchanged. `actions`/`action` is appended
// only when provided (el() already drops null children, so passing a falsy
// action yields the same node tree as omitting it). Callers whose header or
// empty-state carries extra wrapper attributes (id, inline style) or a
// non-canonical shape (an <h1>/<p> variant, a back-button before the title)
// keep hand-building; these two cover the canonical shape so the rest can
// migrate incrementally, like uiCard/uiButton above.
function pageHeader(title, subtitle, actions) {
  const children = [
    el("div", {}, [
      el("div", { class: "page-title" }, title),
      el("div", { class: "page-subtitle" }, subtitle),
    ]),
  ];
  if (actions != null) children.push(actions);
  return el("div", { class: "page-header" }, children);
}

// `glyph`, not `icon` — the parameter used to be called `icon`, which now
// shadows the global icon() from dashboard-icons.js inside this body.
// Call sites pass icon("inbox") instead of an emoji.
function emptyState(glyph, title, hint, action) {
  const children = [
    el("div", { class: "empty-state-icon" }, glyph),
    el("div", { class: "empty-state-title" }, title),
    el("div", { class: "empty-state-hint" }, hint),
  ];
  if (action != null) children.push(action);
  return el("div", { class: "empty-state" }, children);
}

// ── Shared modal (replaces the 6 ad-hoc backdrops) ─────────────────
// Provides, once and consistently: backdrop + click-outside, Esc to close,
// focus trap, focus restore to the opener, role="dialog" + aria-modal, and a
// single z-index. `actions` is an array of nodes for the footer (right-aligned).
// Returns { close }. New modals should use this; legacy ones migrate over time.
function openModal({
  title,
  subtitle,
  body,
  actions,
  maxWidth = "600px",
  onClose,
  ariaLabel,
} = {}) {
  const opener = document.activeElement;
  const backdrop = el("div", {
    class: "ui-modal-backdrop",
    style:
      "position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:2000;display:flex;align-items:center;justify-content:center;padding:16px;",
  });
  const modal = el("div", {
    role: "dialog",
    "aria-modal": "true",
    "aria-label": ariaLabel || (typeof title === "string" ? title : "Dialog"),
    style: `background:var(--bg-card,#13131a);border:1px solid var(--border-soft,#2a2a38);border-radius:var(--radius-lg,10px);padding:22px;max-width:${maxWidth};width:100%;max-height:85vh;overflow-y:auto;`,
  });

  let closed = false;
  const close = () => {
    if (closed) return;
    closed = true;
    document.removeEventListener("keydown", onKey, true);
    backdrop.remove();
    if (opener && typeof opener.focus === "function") {
      try {
        opener.focus();
      } catch {}
    }
    if (onClose) {
      try {
        onClose();
      } catch {}
    }
  };

  if (title) {
    modal.appendChild(
      el("div", { style: "display:flex;align-items:flex-start;gap:10px;margin-bottom:6px;" }, [
        el("h2", { style: "margin:0;font-size:18px;color:var(--text-1,#fff);flex:1;" }, title),
        uiButton("×", {
          ariaLabel: "Close dialog",
          onClick: close,
          size: "tiny",
          style: "font-size:18px;line-height:1;padding:0 8px;",
        }),
      ]),
    );
  }
  if (subtitle)
    modal.appendChild(
      el(
        "p",
        { class: "muted", style: "margin:2px 0 16px;font-size:13px;line-height:1.5;" },
        subtitle,
      ),
    );
  if (body) modal.appendChild(body);
  if (actions && actions.length) {
    modal.appendChild(
      el(
        "div",
        {
          style:
            "display:flex;gap:8px;justify-content:flex-end;margin-top:22px;border-top:1px solid var(--border-soft,#2a2a38);padding-top:16px;flex-wrap:wrap;",
        },
        actions,
      ),
    );
  }

  backdrop.appendChild(modal);
  backdrop.addEventListener("click", (e) => {
    if (e.target === backdrop) close();
  });

  // Focus trap: keep Tab within the modal; Esc closes. Capture phase so it
  // wins over view-level key handlers.
  const onKey = (e) => {
    if (e.key === "Escape") {
      e.preventDefault();
      close();
      return;
    }
    if (e.key !== "Tab") return;
    const f = modal.querySelectorAll(
      'a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])',
    );
    if (!f.length) return;
    const first = f[0],
      last = f[f.length - 1];
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  };
  document.addEventListener("keydown", onKey, true);

  document.body.appendChild(backdrop);
  // Move focus into the dialog (first focusable, else the dialog itself).
  const firstFocusable = modal.querySelector(
    'button,a[href],textarea,input,select,[tabindex]:not([tabindex="-1"])',
  );
  if (firstFocusable) {
    try {
      firstFocusable.focus();
    } catch {}
  } else {
    modal.setAttribute("tabindex", "-1");
    try {
      modal.focus();
    } catch {}
  }

  return { close, modal, backdrop };
}

// Show a toast. Optional 4th arg `onClick` makes the entire toast clickable
// (with a hand cursor) \u2014 handy for "\u2713 Started \u2014 click to view" flows where
// we want to navigate the user to the relevant surface.
function toast(text, kind = "ok", duration = 4000, onClick = null) {
  const region = $("#toast-region");
  // Max 3 visible toasts
  while (region.children.length >= 3) region.firstChild.remove();
  // The kind already says whether this succeeded; it used to say it a second
  // time as a "✓ " / "❌ " prefix baked into ~170 call strings. The mark is
  // drawn here once instead, and the strings are just the message.
  const TOAST_ICON = { ok: "check", err: "alert", warn: "alert", info: "info" };
  const span = el("span", {}, String(text).replace(/^\s*[✓✔✅❌⚠️⏳🎉🚀]+\s*/u, ""));
  if (onClick) {
    span.style.cursor = "pointer";
    span.style.textDecoration = "underline";
    span.style.textDecorationStyle = "dotted";
    span.style.textUnderlineOffset = "3px";
  }
  const t = el("div", { class: `toast ${kind}` }, [
    icon(TOAST_ICON[kind] || "info"),
    span,
    el(
      "button",
      {
        class: "toast-dismiss",
        "aria-label": "Dismiss notification",
        onclick: (e) => {
          e.stopPropagation();
          t.remove();
        },
      },
      "\u00d7",
    ),
  ]);
  if (onClick) {
    t.style.cursor = "pointer";
    t.addEventListener("click", () => {
      try {
        onClick();
      } catch {}
      t.remove();
    });
  }
  region.appendChild(t);
  if (duration > 0)
    setTimeout(() => {
      if (t.parentNode) t.remove();
    }, duration);
}

function setStatus(online, text) {
  const pill = $("#status-pill");
  pill.classList.toggle("online", online);
  pill.classList.toggle("offline", !online);
  $("#status-text").textContent = text;
  dashboardState.online = online;
}

function setContent(html) {
  $("#content").innerHTML = html;
}

function setContentNode(node) {
  const c = $("#content");
  c.innerHTML = "";
  c.appendChild(node);
}

function showLoading(msg = "Loading\u2026") {
  setContent(`
    <div class="page-header"><div><div class="skeleton skeleton-text w-40"></div></div></div>
    <div class="skeleton skeleton-stat" style="margin-bottom: 16px;"></div>
    <div class="skeleton skeleton-row"></div>
    <div class="skeleton skeleton-row"></div>
    <div class="skeleton skeleton-row"></div>
    <div style="text-align: center; margin-top: 24px; color: var(--text-4); font-size: 12px;">${escapeHtml(msg)}</div>
  `);
}

// Busy state via a class, not by rewriting textContent. Two reasons: the old
// version spliced an hourglass emoji in (banned), and assigning textContent
// destroys any child nodes the button had \u2014 so a button containing an icon came
// back as bare text. `.is-busy` renders the mono ellipsis and honours
// prefers-reduced-motion. Reaches all 81 call sites without touching one.
async function withProgress(btn, fn) {
  btn.disabled = true;
  btn.classList.add("is-busy");
  btn.setAttribute("aria-busy", "true");
  try {
    return await fn();
  } finally {
    btn.disabled = false;
    btn.classList.remove("is-busy");
    btn.removeAttribute("aria-busy");
  }
}

// Run a mutating api() call with consistent, honest UX:
//   - optional confirm() gate for destructive actions
//   - in-flight spinner on the clicked button (via withProgress)
//   - error toast on thrown/`{error}` responses (never a silent failure)
//   - success toast + after() callback ONLY on real success
// Returns true on success, false otherwise. Pass the click event so the right
// button shows the spinner. Treats "response has no .error" as success, which
// matches the rest of the codebase (e.g. cloneAutomation).
async function mutateAction(
  ev,
  action,
  payload = {},
  { confirm: confirmMsg, success, failPrefix = "Failed", after } = {},
) {
  if (confirmMsg && !window.confirm(confirmMsg)) return false;
  const btn = ev?.currentTarget;
  const run = async () => {
    let r;
    try {
      r = await api(action, payload);
    } catch (e) {
      toast(`${failPrefix}: ${e?.message || "unreachable"}`, "err", 5000);
      return false;
    }
    if (r && r.error) {
      toast(`${failPrefix}: ${r.error}`, "err", 5000);
      return false;
    }
    if (success) toast(success, "ok");
    if (after) {
      try {
        await after(r);
      } catch {}
    }
    return true;
  };
  return btn ? withProgress(btn, run) : run();
}

function showError(message, retryFn) {
  // Detect Chrome MV3 stale-service-worker errors. When background.js was
  // updated (e.g. new DASHBOARD_ROUTES entry) but the extension wasn't
  // reloaded, dispatchDashboardCall throws "The message port closed before
  // a response was received" or "unknown_action". Surface a helpful prompt
  // instead of the cryptic message \u2014 this used to only fire for full-page
  // route errors but now catches per-renderer fetches too.
  const msg = String(message || "");
  const isStaleSW =
    /message port closed/i.test(msg) ||
    /unknown_action/i.test(msg) ||
    /Could not establish connection/i.test(msg) ||
    /Receiving end does not exist/i.test(msg);

  if (isStaleSW) {
    const hint = el("div", { class: "empty-state-hint", style: "line-height:1.6;" });
    hint.innerHTML =
      `The background script was updated but the running copy is older.<br><br>` +
      `<b>Fix in 2 steps:</b><br>` +
      `1. Open <code>chrome://extensions/</code> and click the \u21bb icon on Nexus Connect.<br>` +
      `2. Then refresh this tab with <code>Ctrl+R</code>.`;
    setContentNode(
      el("div", { class: "empty-state" }, [
        el("div", { class: "empty-state-icon" }, "\ud83d\udd04"),
        el("div", { class: "empty-state-title" }, "Extension needs reload"),
        hint,
        el("div", { style: "display:flex;gap:8px;justify-content:center;margin-top:16px;" }, [
          el(
            "button",
            { class: "btn btn-primary", type: "button", onclick: () => location.reload() },
            "\u21bb Refresh this tab",
          ),
          el(
            "button",
            {
              class: "btn btn-secondary",
              type: "button",
              onclick: () => chrome.tabs.create({ url: "chrome://extensions/" }),
            },
            "Open extensions page",
          ),
        ]),
      ]),
    );
    return;
  }

  const wrap = el("div", { class: "empty-state" }, [
    el("div", { class: "empty-state-icon" }, "\u26a0\ufe0f"),
    el("div", { class: "empty-state-title" }, "Something went wrong"),
    el("div", { class: "empty-state-hint" }, message),
    retryFn
      ? el(
          "button",
          {
            class: "btn btn-primary",
            type: "button",
            style: "margin-top: 16px;",
            onclick: retryFn,
          },
          "\u21bb Try again",
        )
      : null,
  ]);
  setContentNode(wrap);
}

function fmtNum(n) {
  if (n == null) return "0";
  if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
  if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
  if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
  return String(n);
}

function fmtDate(iso) {
  if (!iso) return "—";
  try {
    const d = new Date(iso);
    const now = Date.now();
    const diff = now - d.getTime();
    const h = Math.floor(diff / 3600000);
    const days = Math.floor(diff / 86400000);
    if (h < 1) return "just now";
    if (h < 24) return `${h}h ago`;
    if (days < 7) return `${days}d ago`;
    return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
  } catch {
    return iso;
  }
}

function buildPagination(currentPage, totalPages, totalRows, onPageChange) {
  const pages = [];
  // Show at most 7 page buttons: 1 ... 4 5 6 ... 20
  const maxVisible = 7;
  if (totalPages <= maxVisible) {
    for (let i = 1; i <= totalPages; i++) pages.push(i);
  } else {
    pages.push(1);
    let start = Math.max(2, currentPage - 1);
    let end = Math.min(totalPages - 1, currentPage + 1);
    if (currentPage <= 3) {
      start = 2;
      end = 5;
    }
    if (currentPage >= totalPages - 2) {
      start = totalPages - 4;
      end = totalPages - 1;
    }
    if (start > 2) pages.push("...");
    for (let i = start; i <= end; i++) pages.push(i);
    if (end < totalPages - 1) pages.push("...");
    pages.push(totalPages);
  }

  return el("div", { class: "pagination" }, [
    el("div", { class: "muted small" }, `${fmtNum(totalRows)} total`),
    el("div", { class: "pagination-controls" }, [
      el(
        "button",
        {
          class: "pagination-page",
          disabled: currentPage <= 1 ? "disabled" : null,
          onclick: () => onPageChange(currentPage - 1),
        },
        "\u2190",
      ),
      ...pages.map((p) => {
        if (p === "...")
          return el("span", { class: "muted small", style: "padding: 0 4px;" }, "\u2026");
        return el(
          "button",
          {
            class: `pagination-page${p === currentPage ? " active" : ""}`,
            onclick: () => onPageChange(p),
          },
          String(p),
        );
      }),
      el(
        "button",
        {
          class: "pagination-page",
          disabled: currentPage >= totalPages ? "disabled" : null,
          onclick: () => onPageChange(currentPage + 1),
        },
        "\u2192",
      ),
    ]),
  ]);
}

function fmtAmount(amt) {
  if (!amt) return "undisclosed";
  const s = String(amt);
  if (/^\$?[\d.]+[KMB]/.test(s)) return s.startsWith("$") ? s : "$" + s;
  const n = parseFloat(s);
  if (isNaN(n)) return s;
  if (n >= 1e9) return `$${(n / 1e9).toFixed(1)}B`;
  if (n >= 1e6) return `$${(n / 1e6).toFixed(1)}M`;
  if (n >= 1e3) return `$${(n / 1e3).toFixed(0)}K`;
  return `$${n}`;
}

// ── Router ───────────────────────────────────────────────────────

const routes = {
  home: renderHome, // Home = Mission Control top-3 + Quick Actions + Live Map (Phase 1, 2026-04)
  "new-dm-campaign": renderNewDmCampaign,
  "new-email-campaign": renderNewEmailCampaign,
  // Operations Center — unified entry point for all ops (Phase 2 IA redesign)
  operations: renderOperationsCenter,
  "operations/op": renderOperationDetail,
  campaigns: renderCampaigns,
  "campaigns/detail": renderCampaignDetail,
  "workflow/control": renderWorkflowControl,
  // ── Work-section hubs (2026-05-30 from-scratch reorg) ─────────────
  // Phase A: Automations + Action log get full platform-tab hub
  // treatment (most-trafficked surfaces). Workflows + Performance
  // route directly to their existing pages — they get internal-tab
  // hubs in Phase B.
  automations: renderAutomationsHub, // hub: platform tabs (LI/X/Gmail/TG)
  workflows: renderWorkflowsHub, // hub: Library / My workflows / Runs
  "action-log": renderActivityHub, // hub: platform tabs
  "automations-proof": renderAutomationProof, // proof-of-execution board (did each automation do REAL work?)
  performance: renderPerformanceHub, // hub: Funnel / Budgets
  coinmm: renderCoinmm, // read-only coin_mm status card
  agency: renderAgencyOverview, // multi-client cockpit (agency layer)
  // The roster (screener table) is the Clients room's landing page. The legacy
  // renderClients (create / assign accounts / tag work) is still loaded and is
  // reachable at #clients/manage until its remaining functions move onto the
  // command center's Settings tab.
  clients: renderClientsRoster,
  "clients/manage": renderClients,
  "client-report": renderClientReport, // customer-facing "everything we did for you" report
  notifications: renderNotifications, // in-extension notification center (the bell)
  "run-previews": renderRunPreviews, // saved "what Run will do" manifests
  objectives: renderObjectivesHub, // hub: goal-oriented playbooks ("Goals")
  connections: renderConnections, // hub: per-platform connect status + guided add
  "objectives/detail": renderObjectiveDetail, // single objective playbook
  playbooks: renderPlaybooksHub, // hub: mission sections that bundle automations
  "playbooks/detail": renderPlaybookDetail, // single playbook (brief + attachments)
  boost: renderBoostHub, // hub: project-boosting campaigns
  "boost/new": renderBoostBuilder, // create a boost campaign
  "boost/detail": renderBoostDetail, // single boost campaign
  // Legacy "Visual workflow viewer" — preserved for #automations/view/:id deep links.
  "automations/view": renderAutomationView,
  // workflows/edit stays as the legacy wizard, reachable via the "Wizard"
  // button on the canvas top bar for users who prefer step-by-step authoring.
  // (`workflows/new` was a third alias for the canvas with zero callers — removed.)
  "workflows/edit": renderWorkflowComposer,
  "workflows/detail": renderWorkflowDetail,
  "workflows/compare": renderWorkflowCompare,
  "workflows/canvas": renderWorkflowCanvas,
  // Phase ZC — Test-all session results page
  "workflows/test-all-results": renderTestAllResults,
  // Phase ZD — Sales proposals
  proposals: renderProposalsList,
  "proposals/detail": renderProposalDetail,
  // Hiring Outreach — LinkedIn comment review queue (crypto_hiring_outreach)
  "hiring-review": renderHiringReview,
  // Company Console + Conversation Plays + Group-reply review — re-homed onto
  // main's split base in the origin/main merge (renderers appended at file tail).
  "company/detail": renderCompanyConsole,
  // Client Command Center replaces the old per-client campaign list. The legacy
  // renderClientDetail stays loaded but unrouted until its file is retired, so
  // this is revertible by flipping the one line back.
  "clients/detail": renderClientCommandCenter,
  plays: renderConversationPlays,
  "group-replies": renderGroupReplyReview,
  // Route-drift repair: KEPT deliberately. `#new-workflow/<templateKey>` is
  // still emitted with a subpath by live call sites (dashboard-live-ops.js and
  // dashboard-missions-composable-bundles-of-p.js), and parseHash has a matcher
  // for it below. This alias is what stopped those buttons dead-ending after the
  // canvas rename — removing it would re-open the bug it was added to fix.
  // (`new-campaign` / `new-campaign-wizard` had no such callers once the palette
  // was repointed at `campaigns`, so those two are gone.)
  "new-workflow": renderWorkflowCanvas,
  signals: renderSignals,
  "signal-pursue": renderSignalPursue,
  vc: renderVcQueue,
  "vc/detail": renderVcDetail,
  "vc/pursue": renderVcPursue,
  crm: renderCrm,
  x: renderXDashboard,
  "x/account": renderXAccount,
  "telegram/account": renderTgAccount,
  "linkedin/account": renderLiAccount,
  "email/account": renderEmailAccount,
  inbox: renderInbox,
  mission: renderMission,
  earn: renderEarn, // Earn — community quests (the Circuit × Engine seam)
  "email/sequences": renderEmailSequences,
  "email/warmup": renderEmailWarmup,
  import: renderImport,
  scan: renderScan,
  "telegram/autoreply": renderAutoReply,
  "telegram/broadcast": renderBroadcast,
  "telegram/invite": renderMassInvite,
  "telegram/forward": renderForward,
  "telegram/activemember": renderActiveMember,
  "telegram/passive": renderPassiveListener,
  "telegram/stories": renderStories,
  "telegram/scrape": renderSmartScrape,
  "integrations/luma": renderLuma,
  deals: renderDeals,
  accounts: renderAccounts,
  report: renderReport,
  // Per-account performance leaderboard — surfaces sent / replied /
  // reply-rate across all your accounts in the chosen window so you
  // can answer "which of my accounts is producing?" and shut down
  // low-performers. Backed by /api/ops/account-scoreboard.
  scoreboard: renderAccountScoreboard,
  admin: renderAdmin,
  settings: renderSettings,
  icp: renderIcp,

  // Phase 1 routes (Mar 2026 reorg) — all live.
  people: renderPeople,
  strategies: renderStrategies,
  competitors: renderCompetitors,
  "service-match": renderServiceMatch,
  groupfinder: renderGroupFinder,
  tutorial: renderTutorial,
  premium: renderPremium,
  profile: renderProfile,
  "group-ops": renderGroupOps,
  growth: renderGrowth,
  activity: renderActivity,
  bookmarks: renderBookmarks,
  "outreach-queue": renderOutreachQueue,
  health: renderHealth,
  // ── Deprecated → redirect to hub sub-routes ───────────────────────
  // Bookmark/deep-link compatibility. Each old URL navigates to its new
  // home so users on saved links land in the right hub tab. Remove
  // after 1-2 weeks of traffic.
  "linkedin/activity": () => navigate("action-log/linkedin"),
  "linkedin/automations": () => navigate("automations/linkedin"),
  "workflows/performance": () => navigate("performance/funnel"),
  "workflows/budgets": () => navigate("performance/budgets"),

  // Phase 4 — per-platform hub pages (Overview / Operations / Monitor).
  // The bare platform routes (#telegram, #linkedin, #email) and section sub-routes
  // (#telegram/overview, #x/operations) all dispatch to renderPlatformHub.
  // Note: `x` keeps its existing renderXDashboard for backwards compatibility.
  telegram: (params) => renderPlatformHub("telegram", params),
  linkedin: (params) => renderPlatformHub("linkedin", params),
  email: (params) => renderPlatformHub("email", params),
  // The four `<platform>/hub` entries are NOT dead aliases, despite looking like
  // duplicates of the bare routes above and having no literal call site. parseHash
  // SYNTHESIZES these names (see the HUB_SECTIONS block): #telegram/overview,
  // #x/monitor and #x/operations all resolve to "<platform>/hub". Deleting them
  // in the r95 menu pass silently 404'd every platform Overview/Operations button
  // in #growth, the V1/V2 sidebars, and content-twitter.js's "Open Twitter
  // Monitor" — a grep for the string finds nothing, so only executing parseHash
  // reveals the dependency. Do not remove without deleting that block too.
  "x/hub": (params) => renderPlatformHub("x", params),
  "telegram/hub": (params) => renderPlatformHub("telegram", params),
  "linkedin/hub": (params) => renderPlatformHub("linkedin", params),
  "email/hub": (params) => renderPlatformHub("email", params),
  // "live-map" removed — merged into "home"
  // "mission" route also stays available below for any deep links, but no sidebar entry
};

function parseHash() {
  const raw = location.hash.replace(/^#/, "");
  const parts = raw.split("/");
  if (parts[0] === "") parts[0] = "home";

  // Route matching
  if (parts[0] === "campaigns" && parts[1] && parts[2]) {
    return { route: "campaigns/detail", params: { type: parts[1], id: parts[2] } };
  }
  // Visual workflow viewer: #automations/view/:presetId
  if (parts[0] === "automations" && parts[1] === "view" && parts[2]) {
    return { route: "automations/view", params: { id: parts[2] } };
  }
  // ── Hub sub-route matchers (2026-05-30 reorg) ─────────────────────
  // The four Work-section hubs (Automations / Workflows / Action log /
  // Performance) take a sub-segment that drives an internal tab.
  // Pattern: #<hub>/<tab>  →  route="<hub>/<tab>" with params={ tab: <tab> }.
  // Falls through to the generic registered-route fallback at the
  // bottom if no specific match — so #automations/linkedin matches
  // the registered "automations/linkedin" entry directly.
  if (parts[0] === "automations" && parts[1] && parts[1] !== "view") {
    return { route: "automations", params: { platform: parts[1] } };
  }
  if (parts[0] === "action-log" && parts[1]) {
    return { route: "action-log", params: { platform: parts[1] } };
  }
  if (parts[0] === "performance" && parts[1]) {
    return { route: "performance", params: { tab: parts[1] } };
  }
  if (
    parts[0] === "workflows" &&
    parts[1] &&
    ![
      "new",
      "edit",
      "detail",
      "budgets",
      "compare",
      "performance",
      "canvas",
      "test-all-results",
    ].includes(parts[1])
  ) {
    // Tab matcher — but ONLY when parts[1] is not one of the legacy
    // sub-routes already registered (those route to their own handlers,
    // not the workflows hub tab system).
    return { route: "workflows", params: { tab: parts[1] } };
  }
  // Goals: #objectives/:id → single objective playbook (deep-link / reload-safe)
  if (parts[0] === "objectives" && parts[1]) {
    return { route: "objectives/detail", params: { id: parts[1] } };
  }
  // Client report: #client-report/:clientId → report scoped to one customer
  if (parts[0] === "client-report" && parts[1]) {
    return { route: "client-report", params: { client: parts[1] } };
  }
  // Boost: #boost/new → builder; #boost/<type> → builder pre-typed; #boost/<id> → detail
  if (parts[0] === "boost" && parts[1] === "new") {
    return { route: "boost/new", params: { type: parts[2] || null } };
  }
  if (parts[0] === "boost" && parts[1]) {
    if (/^\d+$/.test(parts[1])) return { route: "boost/detail", params: { id: parts[1] } };
    return { route: "boost/new", params: { type: parts[1] } };
  }
  // Phase ZD: Proposal detail #proposals/detail/:id (deep-link / reload-safe)
  if (parts[0] === "proposals" && parts[1] === "detail" && parts[2]) {
    return { route: "proposals/detail", params: { id: parts[2] } };
  }
  if (parts[0] === "signals" && parts[1] === "pursue" && parts[2]) {
    return { route: "signal-pursue", params: { signalId: parts[2] } };
  }
  if (parts[0] === "vc" && parts[1] && parts[2] === "pursue" && parts[3] && parts[4]) {
    return {
      route: "vc/pursue",
      params: { investorId: parts[1], contactId: parts[3], companyId: parts[4] },
    };
  }
  if (parts[0] === "vc" && parts[1]) {
    return { route: "vc/detail", params: { investorId: parts[1] } };
  }
  // Per-client campaign console: #clients/<id> → everything we run for them.
  // `manage` is a reserved SUB-ROUTE, not a client id: without this the generic
  // matcher below swallows it, the roster's "Assign accounts & tag work" button
  // loads the console with clientId="manage", and the user gets
  // "Client not found or not yours" instead of the account-assignment screen.
  if (parts[0] === "clients" && parts[1] === "manage") {
    return { route: "clients/manage", params: {} };
  }
  if (parts[0] === "clients" && parts[1]) {
    return { route: "clients/detail", params: { id: parts[1] } };
  }
  // Company Console: #company/<id> → per-company Campaign Console (re-homed).
  if (parts[0] === "company" && parts[1]) {
    return { route: "company/detail", params: { id: parts[1] } };
  }
  // Workflow creation: #new-workflow/service_outreach
  if (parts[0] === "new-workflow" && parts[1]) {
    return { route: "new-workflow", params: { workflowType: parts[1] } };
  }
  // Operations Center detail: #operations/op/<opId>
  if (parts[0] === "operations" && parts[1] === "op" && parts[2]) {
    return { route: "operations/op", params: { opId: parts[2] } };
  }
  // Workflow Mission Control: #workflow/123
  if (parts[0] === "workflow" && parts[1]) {
    return { route: "workflow/control", params: { workflowId: parts[1] } };
  }
  // Account setup routes: #accounts/twitter, #accounts/linkedin
  if (parts[0] === "accounts" && parts[1]) {
    return { route: "accounts", params: { platform: parts[1] } };
  }
  // Platform hub sections (Phase 4): #telegram/overview, #x/operations, #email/monitor
  // Must run BEFORE the platformAccountRoutes block so e.g. #telegram/overview
  // is treated as a hub-section rather than an account ID.
  const HUB_SECTIONS = new Set(["overview", "operations", "monitor"]);
  if (["x", "telegram", "linkedin", "email"].includes(parts[0]) && HUB_SECTIONS.has(parts[1])) {
    return { route: `${parts[0]}/hub`, params: { section: parts[1] } };
  }

  // Platform feature routes: #telegram/autoreply, #telegram/broadcast, #email/sequences, etc.
  const knownSubRoutes = new Set([
    "autoreply",
    "broadcast",
    "invite",
    "forward",
    "activemember",
    "passive",
    "stories",
    "scrape",
    "sequences",
    "warmup",
    "account",
  ]);
  const platformAccountRoutes = {
    x: "x/account",
    telegram: "telegram/account",
    linkedin: "linkedin/account",
    email: "email/account",
  };
  if (platformAccountRoutes[parts[0]] && parts[1]) {
    // If second part is a known feature, let it match as a route (e.g. telegram/autoreply)
    const featureRoute = `${parts[0]}/${parts[1]}`;
    if (routes[featureRoute]) {
      return { route: featureRoute, params: {} };
    }
    // Otherwise treat as account ID
    return {
      route: platformAccountRoutes[parts[0]],
      params: { accountId: parts.slice(1).join("/") },
    };
  }
  // Generic multi-segment fallback — if the joined path matches a registered
  // route, use it. Catches things like "integrations/luma" without needing
  // a per-prefix special case for every static multi-segment route.
  if (parts.length > 1) {
    const joined = parts.join("/");
    if (routes[joined]) return { route: joined, params: {} };
  }
  return { route: parts[0], params: {} };
}

// ── V3 sidebar config (Vercel/Linear hybrid) ──────────────────────────────
//
// Five primary sections, always visible. Each section has a `children` list
// that renders into the context pane only when that section is active.
// Adding/moving a route into a section requires:
//   1. add the route to the section's children (UI)
//   2. add the route → section mapping in V3_ROUTE_TO_SECTION (active state)
//
// Routes that don't appear in any section's children OR in V3_ROUTE_TO_SECTION
// are "off-sidebar" — only reachable via ⌘K palette / direct hash. That's by
// design for low-traffic surfaces (admin, integrations, wizards, scan, etc.).

// 2026-06 IA rethink: the operator's daily loop — Today → Find → Automate →
// Inbox → Pipeline → Monitor. Six top sections (was 5, with a 13-child "Work"
// blob). The overloaded Work group is split into two clear hubs:
//   Automate — everything that RUNS (Goals/Run now/Scheduled/Campaigns/Boost/Ops)
//   Monitor  — everything that REPORTS (Activity/Performance/Approvals/Coin MM)
// Implementation note: this is a pure SIDEBAR regroup — each child points at an
// EXISTING route/renderer, so no renderers were rewritten and deep links are
// unchanged. Utilities (Accounts/Workspaces/Settings/…) moved to the footer.
const V3_SECTIONS = [
  // Clients — the agency roster and the per-client command center. First,
  // because for an operator running work FOR customers, "which customer" is the
  // question that precedes every other one.
  //
  // GATED: `visibleWhen` hides it for users who have no clients (a solo user
  // running their own growth shouldn't see an empty agency room). The filter is
  // COSMETIC ONLY — server-side owner scoping is the real boundary; a hidden nav
  // item in front of an unscoped endpoint is still a breach.
  {
    key: "clients",
    icon: "users",
    label: "Clients",
    route: "clients",
    gated: true,
    visibleWhen: (ctx) => !!(ctx && ctx.hasClients),
    children: [
      {
        icon: "users",
        label: "All clients",
        route: "clients",
        desc: "Every customer, and what's running for each",
      },
      // "Client report" deliberately lives under Results only — one route, one
      // home. The roster links to each client's report per row.
    ],
  },
  // Today — the launchpad + everything that needs a human (Inbox + Approvals).
  {
    key: "today",
    icon: "home",
    label: "Today",
    route: "mission",
    children: [
      { icon: "inbox", label: "Inbox", route: "inbox" },
      { icon: "check", label: "Approvals", route: "hiring-review" },
      { icon: "message", label: "Group replies", route: "group-replies" },
    ],
  },
  // Contacts — the "who": Pipeline (your contacts) + Discover (who to target).
  {
    key: "contacts",
    icon: "folder",
    label: "Contacts",
    route: "crm",
    children: [
      { group: "Pipeline", icon: "folder", label: "CRM", route: "crm" },
      { group: "Pipeline", icon: "users", label: "Recommended", route: "people" },
      // "OTC Deals" = OTC token deal evaluation, not the sales funnel (that's a
      // CRM stage filter). Proposals are sales collateral tied to contacts/deals.
      { group: "Pipeline", icon: "money", label: "OTC Deals", route: "deals" },
      { group: "Pipeline", icon: "note", label: "Proposals", route: "proposals" },
      { group: "Pipeline", icon: "star", label: "Bookmarks", route: "bookmarks" },
      { group: "Pipeline", icon: "clock", label: "History", route: "activity" },
      { group: "Discover", icon: "flame", label: "Signals", route: "signals" },
      { group: "Discover", icon: "search", label: "Group Finder", route: "groupfinder" },
      { group: "Discover", icon: "target", label: "Competitors", route: "competitors" },
      { group: "Discover", icon: "briefcase", label: "VC / Investors", route: "vc" },
      { group: "Discover", icon: "chart", label: "ICP scoring", route: "icp" },
    ],
  },
  // Campaigns — everything that RUNS. `desc` disambiguates overlapping concepts;
  // each points at its existing renderer (no renderers rewritten in this regroup).
  {
    key: "campaigns",
    icon: "launch",
    label: "Campaigns",
    route: "campaigns",
    children: [
      {
        icon: "megaphone",
        label: "Campaigns",
        route: "campaigns",
        desc: "Everything that runs. Run-now, scheduled and operations are filters on this list, not separate pages",
      },
      {
        icon: "package",
        label: "Library",
        route: "playbooks",
        desc: "Reusable bundles + boost presets to launch from",
      },
    ],
  },
  // Results — everything that REPORTS (approvals moved to Today).
  {
    key: "results",
    icon: "chart",
    label: "Results",
    route: "action-log",
    children: [
      {
        icon: "signal",
        label: "Activity",
        route: "action-log",
        desc: "What fired, to whom, result",
      },
      {
        icon: "check",
        label: "Proof",
        route: "automations-proof",
        desc: "Did each automation do REAL work?",
      },
      { icon: "chart", label: "Performance", route: "performance", desc: "Funnels + budgets" },
      {
        icon: "clipboard",
        label: "Client report",
        route: "client-report",
        desc: "Per-customer 'everything we did' report to share with a client",
      },
      { icon: "money", label: "Coin MM", route: "coinmm", desc: "Poor-MM outreach status" },
    ],
  },
  // Manage — accounts, clients, health, settings (utilities).
  {
    key: "manage",
    icon: "settings",
    label: "Manage",
    route: "accounts",
    children: [
      { group: "Accounts", icon: "link", label: "Accounts", route: "accounts" },
      // Clients moved to its own top-level section; Workspaces (the TENANCY
      // boundary — a different concept from a client label) stays here.
      { group: "Accounts", icon: "building", label: "Workspaces", route: "agency" },
      { group: "System", icon: "lock", label: "Health", route: "health" },
    ],
  },
  // Earn — thin local view of the Circuit seam; deep quests live in the Circuit web app.
  { key: "earn", icon: "◆", label: "Earn", route: "earn", children: [] },
];

// Footer items that collapse behind a single click — the rarely-used utility
// surfaces. `Connected accounts` lives here because the account picker in the
// top bar covers most day-to-day account needs; the full settings page is a
// rare visit.
const V3_FOOTER = [
  // Setup / utilities — out of the daily loop. Accounts / Workspaces / Clients /
  // Health were duplicated here verbatim from the Manage section (and Clients now
  // has its own section); the footer keeps only what has no other home.
  { icon: "bolt", label: "Premium", route: "premium" },
  { icon: "book", label: "Tutorial", route: "tutorial" },
  { icon: "user", label: "Profile", route: "profile" },
  { icon: "settings", label: "Settings", route: "settings" },
];

// Map every known route → its V3 section. Used by the context pane to decide
// which section is "active" when a route doesn't appear in V3_SECTIONS but
// belongs to one (e.g. "campaigns/detail" belongs to Work).
const V3_ROUTE_TO_SECTION = (() => {
  const map = {
    // ── Today — launchpad + human-approval surfaces ───────────────────
    home: "today",
    mission: "today",
    inbox: "today",
    "hiring-review": "today",
    "group-replies": "today",
    earn: "earn",
    mutuals: "earn",
    // ── Contacts — Pipeline + Discover ────────────────────────────────
    crm: "contacts",
    people: "contacts",
    deals: "contacts",
    activity: "contacts",
    bookmarks: "contacts",
    proposals: "contacts",
    "proposals/detail": "contacts",
    "company/detail": "contacts",
    "clients/detail": "clients",
    "clients/manage": "clients",
    signals: "contacts",
    "signal-pursue": "contacts",
    groupfinder: "contacts",
    competitors: "contacts",
    vc: "contacts",
    "vc/detail": "contacts",
    "vc/pursue": "contacts",
    icp: "contacts",
    "service-match": "contacts",
    // ── Campaigns — everything that runs ──────────────────────────────
    automations: "campaigns",
    catalog: "campaigns",
    run: "campaigns",
    "automations/linkedin": "campaigns",
    "automations/x": "campaigns",
    "automations/gmail": "campaigns",
    "automations/telegram": "campaigns",
    "automations/multi": "campaigns",
    "automations/system": "campaigns",
    "automations/view": "campaigns",
    objectives: "campaigns",
    "objectives/detail": "campaigns",
    playbooks: "campaigns",
    "playbooks/detail": "campaigns",
    workflows: "campaigns",
    "workflows/library": "campaigns",
    "workflows/mine": "campaigns",
    "workflows/runs": "campaigns",
    "workflows/canvas": "campaigns",
    "workflows/edit": "campaigns",
    "workflows/detail": "campaigns",
    "workflows/budgets": "campaigns",
    "workflows/performance": "campaigns",
    "workflows/compare": "campaigns",
    "workflows/test-all-results": "campaigns",
    "workflow/control": "campaigns",
    campaigns: "campaigns",
    "campaigns/detail": "campaigns",
    "campaigns-legacy": "campaigns",
    plays: "campaigns",
    "outreach-queue": "campaigns",
    operations: "campaigns",
    "operations/op": "campaigns",
    boost: "campaigns",
    "boost/new": "campaigns",
    "boost/detail": "campaigns",
    "email/sequences": "campaigns",
    "email/warmup": "campaigns",
    "integrations/luma": "campaigns",
    "telegram/broadcast": "campaigns",
    "telegram/invite": "campaigns",
    "telegram/forward": "campaigns",
    "telegram/activemember": "campaigns",
    "telegram/autoreply": "campaigns",
    "telegram/passive": "campaigns",
    "telegram/scrape": "campaigns",
    "telegram/stories": "campaigns",
    "group-ops": "campaigns",
    "new-dm-campaign": "campaigns",
    "new-email-campaign": "campaigns",
    "new-workflow": "campaigns",
    "linkedin/automations": "campaigns",
    // ── Results — everything that reports ─────────────────────────────
    "action-log": "results",
    "automations-proof": "results",
    "action-log/all": "results",
    "action-log/linkedin": "results",
    "action-log/x": "results",
    "action-log/gmail": "results",
    "action-log/telegram": "results",
    performance: "results",
    "performance/overview": "results",
    "performance/funnel": "results",
    "performance/budgets": "results",
    coinmm: "results",
    "run-previews": "results",
    scoreboard: "results",
    "client-report": "results",
    report: "results",
    "linkedin/activity": "results",
    // ── Manage ────────────────────────────────────────────────────────
    accounts: "manage",
    agency: "manage",
    clients: "clients",
    health: "manage",
  };
  return map;
})();

function v3SectionForRoute(route) {
  const r = String(route || "");
  if (V3_ROUTE_TO_SECTION[r]) return V3_ROUTE_TO_SECTION[r];
  // Fallback: try the topmost segment
  return V3_ROUTE_TO_SECTION[r.split("/")[0]] || null;
}

// ── Run-hub pill nav ──────────────────────────────────────────────────────
// The three "things firing right now" surfaces — Active workflows, Running
// campaigns, Outreach Queue — share one sidebar entry ("Run"). This pill row
// renders at the top of each of those pages so you can switch between them
// without the sidebar carrying three near-identical items.
const _RUN_HUB_TABS = [
  { route: "automations", icon: "▶️", label: "Active" },
  { route: "campaigns", icon: "megaphone", label: "Campaigns" },
  { route: "outreach-queue", icon: "send", label: "Queue" },
];
function _runHubNav(activeRoute) {
  return el(
    "div",
    {
      class: "run-hub-nav",
      style: "display:flex; gap:6px; margin-bottom:14px;",
    },
    _RUN_HUB_TABS.map((t) =>
      el(
        "button",
        {
          type: "button",
          class: `btn btn-tiny ${t.route === activeRoute ? "btn-primary" : "btn-secondary"}`,
          onclick: () => {
            if (t.route !== activeRoute) navigate(t.route);
          },
        },
        [ICON_PATHS[t.icon] ? icon(t.icon) : null, " " + t.label],
      ),
    ),
  );
}

// ── Discover-hub pill nav ─────────────────────────────────────────────────
// The four "find entities to reach out to" surfaces — Signals, Competitors,
// VC / Investors, Service Match — share one sidebar entry ("Discover"). Same
// pattern as _runHubNav.
const _DISCOVER_HUB_TABS = [
  { route: "signals", icon: "flame", label: "Signals" },
  { route: "competitors", icon: "eye", label: "Competitors" },
  { route: "vc", icon: "building", label: "VC / Investors" },
  { route: "service-match", icon: "search", label: "Service Match" },
  { route: "groupfinder", icon: "search", label: "Group Finder" },
];
function _discoverHubNav(activeRoute) {
  return el(
    "div",
    {
      class: "discover-hub-nav",
      style: "display:flex; gap:6px; margin-bottom:14px; flex-wrap:wrap;",
    },
    _DISCOVER_HUB_TABS.map((t) =>
      el(
        "button",
        {
          type: "button",
          class: `btn btn-tiny ${t.route === activeRoute ? "btn-primary" : "btn-secondary"}`,
          onclick: () => {
            if (t.route !== activeRoute) navigate(t.route);
          },
        },
        [ICON_PATHS[t.icon] ? icon(t.icon) : null, " " + t.label],
      ),
    ),
  );
}

// (Removed 2026-05-30: r51 _PLATFORM_TABS / _platformTabsNav /
//  _showPlatformComingSoon scaffold. Superseded by the from-scratch
//  hub renderers (renderAutomationsHub, renderActivityHub) which own
//  their own tab strip via _renderHubTabStrip.)

// Build the V3 sidebar into a fresh #sidebar. Idempotent — destroys whatever
// is in the sidebar first. Called both on initial load and on route change
// (so the context pane swaps to the new active section).
function buildV3Sidebar(sidebar) {
  const { route } = parseHash();
  const activeSection = v3SectionForRoute(route);

  sidebar.innerHTML = "";
  sidebar.classList.add("sidebar-v3");

  // Beta indicator
  sidebar.appendChild(
    el(
      "div",
      {
        class: "sidebar-beta-pill",
        title: "Compact navigation (v3) — toggle in Settings",
      },
      "COMPACT NAV · v3",
    ),
  );

  // Primary nav. Sections may declare `visibleWhen(ctx)`; a section WITHOUT one
  // always renders, so annotating a single section can't affect the others.
  // Cosmetic only — the server's owner scoping is the actual access boundary.
  const isMac = /Mac/i.test(navigator.platform || "");
  const modKey = isMac ? "⌘" : "Ctrl";
  const primary = el("nav", { class: "sidebar-nav sidebar-nav-primary" });
  const visibleSections = V3_SECTIONS.filter((sec) => {
    if (typeof sec.visibleWhen !== "function") return true;
    try {
      return sec.visibleWhen(navCapabilities());
    } catch {
      // A predicate that throws must not blank the nav — fail open, since the
      // server rejects anything the user isn't entitled to anyway.
      return true;
    }
  });
  visibleSections.forEach((sec, idx) => {
    primary.appendChild(
      el(
        "a",
        {
          class: `nav-item nav-item-primary${activeSection === sec.key ? " active" : ""}`,
          "data-route": sec.route,
          "data-section": sec.key,
          href: `#${sec.route}`,
          title: `${sec.label} — ${modKey}+${idx + 1}`,
          onclick: (e) => {
            e.preventDefault();
            navigate(sec.route);
          },
        },
        [
          el("span", { class: "nav-icon" }, ICON_PATHS[sec.icon] ? icon(sec.icon) : sec.icon),
          el("span", { class: "nav-label" }, sec.label),
          sec.key === "inbox" ? el("span", { class: "nav-badge", id: "nav-v3-inbox-badge" }) : null,
          // Shortcut hint — small kbd-style chip aligned right
          el("span", { class: "nav-kbd" }, String(idx + 1)),
        ],
      ),
    );
  });
  sidebar.appendChild(el("div", { class: "sidebar-section sidebar-section-primary" }, [primary]));

  // Context pane: only the active section's children
  if (activeSection) {
    const sec = V3_SECTIONS.find((s) => s.key === activeSection);
    if (sec && sec.children.length > 0) {
      const ctx = el("nav", { class: "sidebar-nav sidebar-nav-context" });
      // Render children grouped: emit a small sub-header whenever the
      // child's `group` changes. Children without a `group` render flat
      // (back-compat for sections that don't define groups).
      let lastGroup = null;
      for (const child of sec.children) {
        if (child.group && child.group !== lastGroup) {
          ctx.appendChild(el("div", { class: "sidebar-context-subheader" }, child.group));
          lastGroup = child.group;
        }
        ctx.appendChild(
          el(
            "a",
            {
              class: `nav-item nav-item-child${route === child.route ? " active" : ""}`,
              "data-route": child.route,
              href: `#${child.route}`,
              // `desc` (added in Tranche B) disambiguates the overlapping
              // automation labels — surface it as the hover tooltip so the words
              // stop reading as synonyms even in the compact sidebar.
              title: child.desc ? `${child.label} — ${child.desc}` : child.label,
              onclick: (e) => {
                e.preventDefault();
                navigate(child.route);
              },
            },
            [
              el(
                "span",
                { class: "nav-icon" },
                ICON_PATHS[child.icon] ? icon(child.icon) : child.icon,
              ),
              el("span", { class: "nav-label" }, child.label),
            ],
          ),
        );
      }
      sidebar.appendChild(
        el("div", { class: "sidebar-section sidebar-section-context" }, [
          el("div", { class: "sidebar-context-header" }, sec.label),
          ctx,
        ]),
      );
    }
  }

  // Spacer to push footer down
  sidebar.appendChild(el("div", { class: "sidebar-spacer", style: "flex:1;" }));

  // Footer: collapsed by default. Click "More" to toggle.
  const footerOpen = collapseStateFor("v3_footer", false);
  const footer = el("div", { class: "sidebar-section sidebar-section-footer" });
  const footerToggle = el(
    "a",
    {
      class: "nav-item nav-item-footer-toggle",
      href: "#",
      onclick: (e) => {
        e.preventDefault();
        toggleCollapseState("v3_footer");
        buildV3Sidebar(sidebar);
      },
    },
    [
      el("span", { class: "nav-icon" }, footerOpen ? "▾" : "▸"),
      el("span", { class: "nav-label" }, footerOpen ? "Less" : "More"),
    ],
  );
  footer.appendChild(footerToggle);
  if (footerOpen) {
    const nav = el("nav", { class: "sidebar-nav" });
    for (const item of V3_FOOTER) {
      nav.appendChild(
        el(
          "a",
          {
            class: `nav-item${route === item.route ? " active" : ""}`,
            "data-route": item.route,
            href: `#${item.route}`,
            onclick: (e) => {
              e.preventDefault();
              navigate(item.route);
            },
          },
          [
            el("span", { class: "nav-icon" }, ICON_PATHS[item.icon] ? icon(item.icon) : item.icon),
            el("span", { class: "nav-label" }, item.label),
          ],
        ),
      );
    }
    footer.appendChild(nav);
  }
  sidebar.appendChild(footer);
}

// Lightweight collapse-state store backed by chrome.storage.local. Synchronous
// reads via an in-memory cache populated lazily on first use; writes are
// fire-and-forget. Keeps the V3 sidebar from re-fetching state on every render.
const _collapseStateCache = {};
function collapseStateFor(key, defaultVal) {
  if (key in _collapseStateCache) return _collapseStateCache[key];
  // Hydrate from chrome.storage on first access. Until it lands, return default.
  chrome.storage.local
    .get([`_collapse_${key}`])
    .then((s) => {
      if (typeof s[`_collapse_${key}`] === "boolean")
        _collapseStateCache[key] = s[`_collapse_${key}`];
    })
    .catch(() => {});
  _collapseStateCache[key] = defaultVal;
  return defaultVal;
}
function toggleCollapseState(key) {
  _collapseStateCache[key] = !_collapseStateCache[key];
  chrome.storage.local.set({ [`_collapse_${key}`]: _collapseStateCache[key] }).catch(() => {});
  return _collapseStateCache[key];
}

// ── Drawer ────────────────────────────────────────────────────────

function openDrawer(title, htmlOrNode) {
  $("#drawer-title").textContent = title;
  const body = $("#drawer-body");
  body.innerHTML = "";
  if (typeof htmlOrNode === "string") body.innerHTML = htmlOrNode;
  else body.appendChild(htmlOrNode);
  $("#drawer").classList.add("show");
  $("#drawer-backdrop").classList.add("show");
}
function closeDrawer() {
  $("#drawer").classList.remove("show");
  $("#drawer-backdrop").classList.remove("show");
}

// renderHome merged into renderLiveMap (route `home` → Live Map). statCard kept below.
function statCard(label, value, sub, variant = null) {
  const card = el("div", { class: "stat" });
  if (variant === "warning") card.style.borderColor = "rgba(251, 191, 36, 0.3)";
  card.appendChild(el("div", { class: "stat-label" }, label));
  card.appendChild(el("div", { class: "stat-value" }, String(value)));
  if (sub) card.appendChild(el("div", { class: "stat-sub" }, sub));
  return card;
}

// ── Run → preview → confirm ("show me exactly what it will do") ───────
// Every Run funnels through runWithPreview(kind,id,params): we fetch a manifest
// of exactly what the run WOULD do (resolved targets, accounts, drafted text)
// without sending, show it, and only resolve {confirmed:true} when the operator
// approves — so the existing run call site stays unchanged behind the gate.
const _RUN_CHANNEL_ICON = { x: "𝕏", linkedin: "in", telegram: "TG", email: "✉", null: "·" };

function _manifestModal(manifest, onConfirm, onCancel) {
  const overlay = el("div", {
    style:
      "position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;padding:20px;",
  });
  const close = (cb) => {
    try {
      overlay.remove();
    } catch {}
    if (cb) cb();
  };
  overlay.addEventListener("click", (e) => {
    if (e.target === overlay) close(onCancel);
  });

  const actions = manifest?.actions || [];
  const warnings = manifest?.warnings || [];

  // Group actions by surface (coin / target).
  const groups = {};
  for (const a of actions) {
    (groups[a.surface || "actions"] = groups[a.surface || "actions"] || []).push(a);
  }

  const groupNodes = Object.entries(groups).map(([surface, list]) =>
    el("div", { style: "margin:10px 0;" }, [
      el("div", { style: "font-weight:600;margin-bottom:4px;" }, surface),
      ...list.map((a) =>
        el(
          "div",
          { style: "border-left:2px solid #3a3a4e;padding:4px 0 4px 10px;margin:6px 0;" },
          [
            el(
              "div",
              { class: "muted", style: "font-size:12px;" },
              `${_RUN_CHANNEL_ICON[a.channel] || a.channel || "·"} · ${a.action || "?"}${a.account ? ` · as ${a.account}` : ""}${a.when && a.when !== "now" ? ` · ${a.when}` : ""}`,
            ),
            a.target
              ? el(
                  "div",
                  { style: "font-size:12px;" },
                  a.target_url
                    ? el(
                        "a",
                        { href: a.target_url, target: "_blank", style: "color:#7aa2f7;" },
                        a.target,
                      )
                    : a.target,
                )
              : null,
            a.message
              ? el(
                  "div",
                  {
                    style:
                      "font-size:12px;white-space:pre-wrap;background:#0e0e14;border-radius:6px;padding:6px 8px;margin-top:4px;",
                  },
                  a.message,
                )
              : null,
          ].filter(Boolean),
        ),
      ),
    ]),
  );

  const card = el(
    "div",
    {
      style:
        "background:var(--bg-card,#13131a);border:1px solid var(--border-soft);border-radius:12px;max-width:680px;width:100%;max-height:84vh;overflow:auto;padding:18px;",
    },
    [
      el(
        "div",
        { style: "font-size:16px;font-weight:700;margin-bottom:2px;" },
        "Confirm run — exactly what will happen",
      ),
      el(
        "div",
        { class: "muted", style: "font-size:13px;margin-bottom:10px;" },
        manifest?.summary || `${actions.length} action(s)`,
      ),
      ...(warnings.length
        ? [
            el(
              "div",
              {
                style:
                  "background:rgba(251,191,36,0.12);border:1px solid rgba(251,191,36,0.3);border-radius:8px;padding:8px 10px;margin-bottom:10px;font-size:12px;",
              },
              warnings.map((w) => el("div", {}, `${w}`)),
            ),
          ]
        : []),
      ...(actions.length
        ? groupNodes
        : [
            el(
              "div",
              { class: "muted" },
              "No per-action detail for this run — Confirm will run it as configured.",
            ),
          ]),
      el(
        "div",
        {
          style:
            "display:flex;gap:8px;justify-content:flex-end;margin-top:14px;position:sticky;bottom:0;background:var(--bg-card,#13131a);padding-top:10px;",
        },
        [
          el(
            "button",
            { class: "btn btn-secondary", type: "button", onclick: () => close(onCancel) },
            "Cancel",
          ),
          onConfirm
            ? el(
                "button",
                { class: "btn btn-primary", type: "button", onclick: () => close(onConfirm) },
                "Confirm & run",
              )
            : null,
        ].filter(Boolean),
      ),
    ],
  );
  overlay.appendChild(card);
  document.body.appendChild(overlay);
}

// Preview-only (no Confirm/run) — for surfaces driven by the scheduler/CLI.
async function previewOnly(kind, id, params = {}) {
  let pv;
  try {
    pv = await api("run-preview", { body: { kind, id, params } });
  } catch (err) {
    pv = { error: err?.message || "preview failed" };
  }
  if (pv?.error) {
    _manifestModal(
      { summary: "Preview unavailable", warnings: [pv.error], actions: [] },
      null,
      null,
    );
    return;
  }
  _manifestModal(pv.manifest, null, null);
}

function runWithPreview(kind, id, params = {}) {
  return new Promise((resolve) => {
    (async () => {
      let pv;
      try {
        pv = await api("run-preview", { body: { kind, id, params } });
      } catch (err) {
        pv = { error: err?.message || "preview failed" };
      }
      if (pv?.error) {
        _manifestModal(
          { summary: "Preview unavailable", warnings: [pv.error], actions: [] },
          () => resolve({ confirmed: true }),
          () => resolve({ cancelled: true }),
        );
        return;
      }
      _manifestModal(
        pv.manifest,
        () => {
          if (pv.previewId) api("run-preview-confirm", { previewId: pv.previewId }).catch(() => {});
          resolve({ confirmed: true, previewId: pv.previewId });
        },
        () => resolve({ cancelled: true }),
      );
    })();
  });
}

// ── Coin MM status (read-only) ────────────────────────────────────────
// Mirrors `node scripts/coinmm-status.js` in the dashboard. Fetches the shared
// snapshot from GET /api/coinmm/status (background route "coinmm-status").
const _COINMM_STAGE = {
  0: "enrolling",
  1: "complaint",
  2: "react",
  3: "pile-on",
  4: "pitch",
  5: "done",
};

async function renderCoinmm() {
  showLoading("Loading coin MM status…");

  const header = el("div", { class: "page-header" }, [
    el("div", {}, [
      el("div", { class: "page-title" }, "Coin MM"),
      el(
        "div",
        { class: "page-subtitle" },
        "Poor-market-making outreach — ingest, sends, group-activation status",
      ),
    ]),
    el("div", { class: "row", style: "gap:6px;" }, [
      el(
        "button",
        {
          class: "btn btn-secondary",
          type: "button",
          title: "Show exactly what the outreach run would do (no send)",
          onclick: () => previewOnly("coin_mm_outreach", null, {}),
        },
        "Preview outreach",
      ),
      el(
        "button",
        {
          class: "btn btn-secondary",
          type: "button",
          title: "Show the multi-day group-activation plan (no send)",
          onclick: () => previewOnly("coin_mm_group_activation", null, {}),
        },
        "Preview activation",
      ),
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: () => renderCoinmm() },
        "↻ Refresh",
      ),
    ]),
  ]);

  let data;
  try {
    data = await api("coinmm-status");
  } catch (err) {
    data = { error: err.message };
  }

  if (!data || data.error) {
    setContentNode(
      el("div", {}, [
        header,
        el("div", { class: "card" }, [
          el(
            "div",
            { class: "muted" },
            `Couldn't load coin MM status: ${data?.error || "unknown error"}. The bot may need a restart to expose this endpoint.`,
          ),
          el("div", { style: "margin-top:8px;" }, [
            el(
              "button",
              { class: "btn btn-secondary", type: "button", onclick: () => renderCoinmm() },
              "Retry",
            ),
          ]),
        ]),
      ]),
    );
    return;
  }

  const sw = data.switches || {},
    ing = data.ingest || {},
    camp = data.campaigns || {},
    acts = data.activations || {};
  const prog = camp.progressed || {};
  const grid = (cards) => el("div", { style: "display:flex;flex-wrap:wrap;gap:10px;" }, cards);

  const switchesCard = el("div", { class: "card" }, [
    el("div", { class: "page-title", style: "font-size:15px;" }, "Status"),
    el(
      "div",
      { class: "muted" },
      `Kill switch: ${sw.killSwitch ? "ON — everything paused" : "off"}  ·  ` +
        `Group activation: ${sw.activationEnabled ? "enabled" : "off (default)"}  ·  ` +
        `venues: ${(sw.venues || []).join(", ") || "none"}`,
    ),
    el(
      "div",
      { class: "muted", style: "margin-top:4px;" },
      `Complainer pool — TG ${sw.complainer?.telegram ?? 0}, X ${sw.complainer?.x ?? 0}  ·  ` +
        `Promo — TG ${sw.promo?.telegram ?? 0}, X ${sw.promo?.x ?? 0}, LinkedIn ${sw.promo?.linkedin_account_id ?? "–"}, Email ${sw.promo?.email_account_id ?? "–"}`,
    ),
  ]);

  const statsCard = el("div", { class: "card" }, [
    grid([
      statCard("Coins loaded", ing.loaded ?? 0, `${ing.remaining ?? 0} remaining`),
      statCard(
        "Campaigns active",
        camp.active ?? 0,
        `${camp.paused ?? 0} paused · ${camp.completed ?? 0} done`,
      ),
      statCard(
        "Actions today",
        camp.todayTotal ?? 0,
        (camp.today || []).map((r) => `${r.platform} ${r.c}`).join(" · ") || "none",
      ),
      statCard(
        "Activations",
        acts.total ?? 0,
        `${(acts.aborted || []).length} banned/aborted`,
        (acts.aborted || []).length ? "warning" : null,
      ),
    ]),
  ]);

  const progCard = el("div", { class: "card" }, [
    el("div", { class: "page-title", style: "font-size:15px;" }, "Channel progress"),
    grid([
      statCard("Telegram", prog.tg ?? 0, ""),
      statCard("X", prog.x ?? 0, ""),
      statCard("LinkedIn", prog.li ?? 0, ""),
      statCard("Email", prog.em ?? 0, ""),
    ]),
  ]);

  const children = [header, switchesCard, statsCard, progCard];

  if ((acts.stages || []).length) {
    children.push(
      el("div", { class: "card" }, [
        el("div", { class: "page-title", style: "font-size:15px;" }, "Active activation stages"),
        el(
          "div",
          { class: "muted" },
          acts.stages.map((r) => `${_COINMM_STAGE[r.stage] || r.stage}: ${r.c}`).join("  ·  "),
        ),
      ]),
    );
  }

  if ((acts.aborted || []).length) {
    children.push(
      el("div", { class: "card" }, [
        el("div", { class: "page-title", style: "font-size:15px;" }, "Banned / aborted"),
        ...acts.aborted.map((a) =>
          el(
            "div",
            { class: "muted", style: "font-size:12px;" },
            `${a.status} · company ${a.company_id} / ${a.venue}: ${String(a.abort_reason || "").slice(0, 90)}`,
          ),
        ),
      ]),
    );
  }

  children.push(
    el("div", { class: "card" }, [
      el("div", { class: "page-title", style: "font-size:15px;" }, "Recent events"),
      ...((data.recent || []).length
        ? data.recent.map((r) =>
            el(
              "div",
              { class: "muted", style: "font-size:12px;" },
              `${String(r.created_at).slice(5, 16)} · ${r.platform} · ${r.event_type} · ${r.name || ""}`,
            ),
          )
        : [el("div", { class: "muted" }, "none yet")]),
    ]),
  );

  setContentNode(el("div", {}, children));
}

// ── Saved run-preview manifests ───────────────────────────────────────
// ── Notification center (the bell) ────────────────────────────────────
// Lists owner-facing alerts recorded in app_notifications. Works with no
// Telegram at all — the bell is the source of truth; DMs are an add-on.
const _NOTIF_LEVEL = {
  info: { icon: "ℹ️", color: "#6ea8fe" },
  success: { icon: "check", color: "#3fb950" },
  warn: { icon: "alert", color: "#d29922" },
  error: { icon: "stop", color: "#f85149" },
};

async function renderNotifications() {
  showLoading("Loading notifications…");
  const header = el("div", { class: "page-header" }, [
    el("div", {}, [
      el("div", { class: "page-title" }, "Notifications"),
      el(
        "div",
        { class: "page-subtitle" },
        "Alerts from your automations. Telegram DMs are sent too when the add-on is connected.",
      ),
    ]),
    el("div", { style: "display:flex;gap:8px;" }, [
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: () => renderNotifications() },
        "↻ Refresh",
      ),
      el(
        "button",
        {
          class: "btn btn-secondary",
          type: "button",
          onclick: async () => {
            await api("notifications-read-all", { queryString: "?scope=all" }).catch(() => {});
            refreshNotifBadge();
            renderNotifications();
          },
        },
        "Mark all read",
      ),
    ]),
  ]);

  let data;
  try {
    data = await api("notifications-list", { queryString: "?limit=100&scope=all" });
  } catch (err) {
    data = { error: err.message };
  }

  if (!data || data.error) {
    setContentNode(
      el("div", {}, [
        header,
        el("div", { class: "card" }, [
          el(
            "div",
            { class: "muted" },
            `Couldn't load notifications: ${data?.error || "unknown error"}.`,
          ),
          el(
            "button",
            { class: "btn btn-secondary", type: "button", onclick: () => renderNotifications() },
            "Retry",
          ),
        ]),
      ]),
    );
    return;
  }

  const rows = data.notifications || [];
  const list = el(
    "div",
    { class: "card" },
    rows.length
      ? rows.map((n) => {
          const lvl = _NOTIF_LEVEL[n.level] || _NOTIF_LEVEL.info;
          const item = el(
            "div",
            {
              style: `display:flex;gap:10px;align-items:flex-start;border-bottom:1px solid #20202c;padding:10px 0;cursor:pointer;${n.is_read ? "opacity:0.55;" : ""}`,
              onclick: async () => {
                if (!n.is_read) {
                  await api("notification-read", { id: n.id }).catch(() => {});
                  refreshNotifBadge();
                }
                if (n.link && String(n.link).startsWith("#")) {
                  location.hash = n.link;
                } else {
                  renderNotifications();
                }
              },
            },
            [
              el("div", { style: `font-size:16px;line-height:1.2;` }, lvl.icon),
              el(
                "div",
                { style: "flex:1;min-width:0;" },
                [
                  el(
                    "div",
                    { style: `font-size:13px;font-weight:600;color:${lvl.color};` },
                    n.title || "(untitled)",
                  ),
                  n.body
                    ? el(
                        "div",
                        {
                          class: "muted",
                          style: "font-size:12px;white-space:pre-wrap;margin-top:2px;",
                        },
                        String(n.body).slice(0, 600),
                      )
                    : null,
                  el(
                    "div",
                    { class: "muted", style: "font-size:11px;margin-top:3px;" },
                    `${String(n.created_at).slice(5, 16)}${n.kind ? ` · ${n.kind}` : ""}${n.link ? " · open →" : ""}`,
                  ),
                ].filter(Boolean),
              ),
              n.is_read
                ? null
                : el("div", {
                    style: `width:8px;height:8px;border-radius:50%;background:${lvl.color};margin-top:5px;flex:0 0 auto;`,
                  }),
            ].filter(Boolean),
          );
          return item;
        })
      : [
          el(
            "div",
            { class: "muted" },
            "No notifications yet. Alerts from your automations will appear here.",
          ),
        ],
  );

  setContentNode(el("div", {}, [header, list]));
}

async function renderRunPreviews() {
  showLoading("Loading saved run previews…");
  const header = el("div", { class: "page-header" }, [
    el("div", {}, [
      el("div", { class: "page-title" }, "Run previews"),
      el(
        "div",
        { class: "page-subtitle" },
        "Saved manifests of exactly what each Run would do (before you confirmed)",
      ),
    ]),
    el(
      "button",
      { class: "btn btn-secondary", type: "button", onclick: () => renderRunPreviews() },
      "↻ Refresh",
    ),
  ]);

  let data;
  try {
    data = await api("run-previews");
  } catch (err) {
    data = { error: err.message };
  }

  if (!data || data.error) {
    setContentNode(
      el("div", {}, [
        header,
        el("div", { class: "card" }, [
          el(
            "div",
            { class: "muted" },
            `Couldn't load previews: ${data?.error || "unknown error"}.`,
          ),
          el(
            "button",
            { class: "btn btn-secondary", type: "button", onclick: () => renderRunPreviews() },
            "Retry",
          ),
        ]),
      ]),
    );
    return;
  }

  const rows = data.previews || [];
  const list = el(
    "div",
    { class: "card" },
    rows.length
      ? rows.map((p) =>
          el(
            "div",
            {
              style:
                "display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #20202c;padding:8px 0;cursor:pointer;",
              onclick: async () => {
                const full = await api("run-preview-get", { id: p.id }).catch(() => null);
                if (full?.manifest) _manifestModal(full.manifest, null, null);
              },
            },
            [
              el("div", {}, [
                el(
                  "div",
                  { style: "font-size:13px;" },
                  `${p.kind}${p.run_key ? ` · ${p.run_key}` : ""}`,
                ),
                el(
                  "div",
                  { class: "muted", style: "font-size:11px;" },
                  `${String(p.created_at).slice(5, 16)} · ${p.action_count} action(s) · ${p.status}`,
                ),
              ]),
              el("div", { class: "muted", style: "font-size:11px;" }, "view →"),
            ],
          ),
        )
      : [
          el(
            "div",
            { class: "muted" },
            "No previews yet — click Run on any automation to generate one.",
          ),
        ],
  );

  setContentNode(el("div", {}, [header, list]));
}

// ── Automations view (visual workflow blueprints) ─────────────────
//
// Shows the system's preset workflow DAGs. Each preset is loaded from
// nexus-bot/src/workflow/presets/*.json and rendered read-only via
// window.WorkflowViewer + ELK auto-layout. v1 is viewer-only; v2 will add
// clone-and-edit on top of the same data model.

// Purpose groups for the Automations grid. Mapping is presentation-only —
// preset JSONs don't carry this field. Group order here drives section order
// in the UI. Workflows not listed fall into "Other".
const AUTOMATION_PURPOSE_GROUPS = [
  {
    key: "discover",
    label: "Discover",
    icon: "search",
    blurb: "Find new signals, opportunities, and tokens",
    workflows: [
      "signal_twitter",
      "signal_linkedin",
      "signal_reddit",
      "signal_hn",
      "opportunity_defillama",
      "opportunity_rootdata",
      "opportunity_jobs",
      "top_tokens_ingest",
      "feed_monitor",
    ],
  },
  {
    key: "outreach",
    label: "Outreach",
    icon: "mail",
    blurb: "Reach out across Telegram, Twitter, LinkedIn, and email",
    workflows: [
      "outreach_orchestrator",
      "tg_campaign",
      "email_campaign",
      "email_sequence",
      "comment_campaign",
      "group_amplification",
      "new_token_outreach",
      "coin_mm_outreach",
      "coin_mm_group_activation",
      "event_promotion",
      "active_member_listener",
    ],
  },
  {
    key: "enrichment",
    label: "Enrichment",
    icon: "bot",
    blurb: "Resolve missing data and grow the contact graph",
    workflows: ["enrichment_pipeline", "group_join", "cross_account_react"],
  },
  {
    key: "reports",
    label: "Reports & maintenance",
    icon: "chart",
    blurb: "Briefings, account warming, and other housekeeping",
    workflows: ["daily_briefing", "account_warmer"],
  },
];

function purposeOfWorkflow(workflowId) {
  for (const g of AUTOMATION_PURPOSE_GROUPS) {
    if (g.workflows.includes(workflowId)) return g;
  }
  return { key: "other", label: "Other", icon: "·", blurb: "", workflows: [] };
}

async function renderAutomations() {
  // 2026-05-30 reorg shim — the #automations route now resolves to
  // renderAutomationsHub (the platform-tabs hub). This function name is
  // still called by composition-card / drawer event handlers that want
  // to refresh the active surface after pause/cancel/run-now. Forward
  // them to the hub so they don't blow it away with the now-orphaned
  // "Active workflows" page. The legacy body below this return is dead
  // code, preserved only because its own internal closures still call
  // renderAutomations() (which now resolves to this shim → hub).
  return renderAutomationsHub({ platform: parseHash().params?.platform });

  // ↓↓↓ Legacy body — unreachable after the early return above. ↓↓↓
  // eslint-disable-next-line no-unreachable

  // Read last-visit timestamp BEFORE updating it, so we can pass it as the
  // `since=` cutoff for the "since you were here" delta. After we read it,
  // we immediately stamp the new visit so that the next render is relative
  // to right now. Skipped on first-ever visit (delta would be all-time).
  const STATUS_STRIP_LAST_VISIT_KEY = "automations:lastVisitAt";
  const lastVisitAt = localStorage.getItem(STATUS_STRIP_LAST_VISIT_KEY) || null;
  try {
    localStorage.setItem(STATUS_STRIP_LAST_VISIT_KEY, new Date().toISOString());
  } catch {}

  // Bump the page-visit counter (Phase D progressive disclosure). Used
  // by getLibraryExpanded() to auto-collapse the library after the user
  // has been here a few times — by then they've explored and don't need
  // the wall of cards on every load.
  try {
    const VISITS_KEY = "automations:visits";
    const cur = Number(localStorage.getItem(VISITS_KEY) || 0);
    localStorage.setItem(VISITS_KEY, String(cur + 1));
  } catch {}

  // Fetch list + run-summary + ops-live + coverage + outcomes + inbox in
  // parallel — one less round-trip than serial. ops-live powers the
  // "Live now" panel; coverage powers the testing-progress stat strip;
  // outcomes-today drives the headline numbers; outcomes-since (only
  // fetched if lastVisitAt is set) drives the "since you were here"
  // delta line; inbox-replies powers the "Replies waiting" lane in
  // Needs-attention (the variable-reward dopamine hit — what makes
  // the user come back).
  const [
    data,
    summaryResp,
    opsLive,
    coverage,
    outcomesToday,
    outcomesSince,
    inboxResp,
    systemJobsResp,
    togglesResp,
    compositionsResp,
  ] = await Promise.all([
    api("automations-list").catch(() => ({})),
    api("automations-runs-summary", { days: 1 }).catch(() => ({})),
    api("ops-live").catch(() => ({})),
    api("ops-coverage").catch(() => ({})),
    api("ops-outcomes", { queryString: "?days=1" }).catch(() => ({})),
    lastVisitAt
      ? api("ops-outcomes", { queryString: `?since=${encodeURIComponent(lastVisitAt)}` }).catch(
          () => ({}),
        )
      : Promise.resolve(null),
    api("inbox-replies").catch(() => ({})),
    api("ops-system-jobs").catch(() => ({})),
    api("services-toggles").catch(() => ({})),
    // Phase C: surface composer workflows on the Active cockpit alongside legacy customs.
    api("compositions-list").catch(() => ({})),
  ]);
  if (data?.error) {
    setContentNode(emptyState("⚠️", "Could not load automations", escapeHtml(data.error)));
    return;
  }
  const workflows = data?.workflows || [];
  const presetCount = data?.preset_count ?? workflows.filter((w) => w.kind !== "custom").length;
  const customCount = data?.custom_count ?? workflows.filter((w) => w.kind === "custom").length;
  const runSummary = (summaryResp && summaryResp.summary) || null;
  // Refresh wired-runners cache from the response so subsequent isWiredRunner()
  // calls (in card actions, viewer header, banner) reflect the server's truth.
  if (Array.isArray(data?.wired_runners)) {
    _wiredRunnersCache = new Set(data.wired_runners);
  }

  // Split presets vs customs. Customs render in their own "My workflows"
  // section at the top so the user sees their work first.
  const presets = workflows.filter((w) => w.kind !== "custom");
  const customs = workflows.filter((w) => w.kind === "custom");
  const presetById = {};
  for (const w of presets) presetById[w.id] = w;

  // Phase C: composer workflows (workflow_compositions table) surface here
  // alongside the legacy customs so the user has ONE place to see "what's
  // running on my account". Clicking opens the n8n canvas, not the legacy
  // viewer (the click destination depends on wf.kind).
  const compositions = (compositionsResp?.compositions || []).map((c) => ({
    id: c.id,
    kind: "composition",
    name: c.name,
    description: c.description || "",
    status: c.status,
    plays: c.plays,
    channels: c.channels,
    audience: c.audience,
    trigger: c.trigger,
    last_run_at: c.last_run_at,
    total_runs: c.total_runs || 0,
    total_replies: c.total_replies || 0,
    total_audience_reached: c.total_audience_reached || 0,
    funnel: c.funnel,
    _raw: c, // full body for the canvas to load
  }));

  // Build the resolution context for pinned shortcuts. The cockpit's
  // shortcuts row reads window.__automationsRenderCtx so it can resolve
  // pin ids ("preset:foo", "custom:42", "platform:bar", "operation:baz")
  // back into the live data needed to render + run them. Refreshed each
  // render so pinned items reflect current state (status, last_run, etc.).
  const customById = {};
  for (const w of customs) customById[String(w.id)] = w;
  const platformById = {};
  if (Array.isArray(window.PLATFORM_ACTIONS)) {
    for (const a of window.PLATFORM_ACTIONS) platformById[a.id] = a;
  }
  const operationById = {};
  if (typeof OPERATIONS !== "undefined" && Array.isArray(OPERATIONS)) {
    for (const op of OPERATIONS) operationById[op.id] = op;
  }
  // Phase H — pinnable composer compositions
  const compositionById = {};
  for (const c of compositions) compositionById[String(c.id)] = c;
  window.__automationsRenderCtx = {
    presetById,
    customById,
    platformById,
    operationById,
    compositionById,
  };

  // `buildLibrary` renders three fixed zones — Per-account actions,
  // Campaigns & workflows, Background services. The topbar account
  // picker (dashboardState.contextAccount) no longer switches layouts;
  // it just expands the matching account's sub-group in zone 1.
  const sections = buildLibrary(dashboardState.contextAccount, {
    workflows,
    presets,
    customs,
    compositions,
    presetById,
  });

  setContentNode(
    el("div", {}, [
      el("div", { class: "page-header" }, [
        el("div", {}, [
          el("div", { class: "page-title" }, "Active workflows"),
          el(
            "div",
            { class: "page-subtitle" },
            (() => {
              // Phase B+C: subtitle reflects both composer compositions and legacy customs.
              const compCount = compositions.length;
              const bits = [];
              if (compCount > 0) bits.push(`${compCount} composer`);
              if (customCount > 0) bits.push(`${customCount} legacy`);
              if (bits.length === 0) bits.push(`${presetCount} preset workflows`);
              return `${bits.join(" · ")}. Click any card to open it. New work happens on Build a workflow →`;
            })(),
          ),
        ]),
        // Phase ZA + ZB — Diagnostic buttons. Right side of the page header.
        el("div", { class: "row", style: "gap:8px;" }, [
          el(
            "button",
            {
              class: "btn btn-primary",
              type: "button",
              title:
                "Run every one of YOUR workflows in dry-run mode, one at a time, and watch each play execute live",
              onclick: () => _openTestAllModal(),
            },
            "▶ Test all my workflows",
          ),
          el(
            "button",
            {
              class: "btn btn-secondary",
              type: "button",
              title: "Run composer-internal quality gates (schema / lint / smoke / e2e)",
              onclick: () => _openHealthCheckModal(),
            },
            "Health check",
          ),
        ]),
      ]),
      // Run-hub pill nav — switch between Active / Campaigns / Queue
      _runHubNav("automations"),
      // ── Day-1 outcome picker (Phase D progressive disclosure) ────────
      // Returns null for non-newcomers, so it transparently disappears
      // for everyone except first-visit users with no prior interaction.
      buildDayOnePicker(workflows),
      // ── Cockpit (Phase A of the catalog → operating-surface reframe) ─
      // Three rows that answer "is the system alive, what needs me, what
      // do I run most?" — before the user has to scan any catalog cards.
      // Status strip — Row 1: "is the system alive and producing value?"
      buildAutomationStatusStrip(opsLive, outcomesToday, outcomesSince, lastVisitAt),
      // Needs attention — Row 2: replies-waiting / errors / stale / broken.
      // Hidden when 0. Replies-waiting is intentionally first because it
      // surfaces the emotional payload (someone responded → act).
      buildCockpitNeedsAttention(opsLive, runSummary, coverage, workflows, inboxResp, lastVisitAt),
      // Your shortcuts — Row 3: user-pinned cards with one-click run.
      // Always renders (even empty) so it can be replaced in-place when
      // user pins their first card.
      buildCockpitShortcuts(),
      // Always-on services panel — Row 4: passive cron services that run
      // perpetually (Luma, signal scrapers, enrichment, smart alerts...).
      // Default-collapsed; expanding shows every always-on engine grouped
      // by Discovery / Enrichment / Engagement / Intel with state pills
      // AND real ON/OFF toggles (Phase 2 — backed by user_service_toggles).
      buildAlwaysOnServicesPanel(systemJobsResp, togglesResp),
      // Coverage strip — testing progress across preset types. Renders only
      // if the user has at least one cloned custom (otherwise it's all 0).
      buildCoverageStrip(coverage),
      // Live-now panel — shows what's currently executing right now across
      // user's customs, long-running campaign/listener loops, and recent
      // system cron fires. Hidden when nothing is active to avoid clutter.
      buildLiveOpsPanel(opsLive),
      // Run health strip — only render if the user has at least one custom run.
      // Otherwise the strip would be a 0/0/0 visual with no signal.
      runSummary && runSummary.total_runs > 0 ? buildAutomationRunHealth(runSummary) : null,
      workflows.length === 0
        ? el("div", { class: "empty-state" }, [
            el("div", { class: "empty-state-icon" }, icon("package")),
            el("div", { class: "empty-state-title" }, "No automations registered"),
            el(
              "div",
              { class: "empty-state-hint" },
              "Restart the bot and look for [WORKFLOW] Loaded N preset workflow(s) in the logs.",
            ),
          ])
        : sections,
      // System automations — collapsible list of every named cron job in the
      // backend. Read-only; surfaces "what's running in the background" for
      // visibility (e.g. li-keep-alive, smart-alerts, archival cycles).
      buildSystemAutomationsSection(),
    ]),
  );

  // Auto-refresh the Live-now panel every 15s while the user is on this page.
  // Cleared on navigation by the standard render-cycle reset (setContentNode
  // replaces the panel container, the interval below detaches itself when
  // the panel element is no longer in the DOM).
  scheduleLiveOpsRefresh();
}

// ── Status strip ─────────────────────────────────────────────────────
// "Is the system alive and producing value?" answered in one horizontal
// line. Built per product-psychology recs: anchoring (first numbers seen
// set the tone), status visibility (Nielsen #1), variable reward (the
// "since you were here" delta below), endowed progress (counts already
// accumulated > options unused), peak-end rule (reframes the page open
// from "wall of cards" to "system producing").
//
// Layout (left → right):
//   [● Live   N running]   [↑ X sent today · ↩ Y replied · ✓ Z%]
// Below, when there's a lastVisitAt:
//   Since you were here: A new replies · B new events
//
// Inputs are best-effort: if any are missing/empty, we degrade
// gracefully (hide stats with 0 data; hide the delta line if no prior
// visit). Returns null only on first-ever visit with NO live activity
// AND no today-numbers — at that point we have nothing to say.

function buildAutomationStatusStrip(opsLive, outcomesToday, outcomesSince, lastVisitAt) {
  const live = opsLive || {};
  const today = outcomesToday && outcomesToday.funnel ? outcomesToday : null;
  const since = outcomesSince && outcomesSince.funnel ? outcomesSince : null;

  // Phase C2: composer composition runs (running + recently finished) count
  // toward the live indicator. We count only currently-running ones for the
  // pulsing dot — finished-but-recent surfaces in the drawer feed.
  const runningCompositions = (live.compositionRuns || []).filter((r) => r.status === "running");
  const liveCount =
    (live.customWorkflows?.length || 0) +
    (live.longRunningLoops?.length || 0) +
    runningCompositions.length;
  const sentToday = today?.funnel?.sent || 0;
  const repliedToday = today?.funnel?.replied || 0;
  const replyRateToday = today?.rates?.replyRate || 0;

  // First-ever visit with nothing to say — skip rendering entirely.
  if (liveCount === 0 && sentToday === 0 && repliedToday === 0 && !lastVisitAt) {
    return null;
  }

  const wrap = el("div", {
    class: "automation-status-strip",
    style: [
      "display:flex",
      "align-items:center",
      "gap:18px",
      "padding:10px 16px",
      "margin:8px 0 10px",
      "border:1px solid rgba(99,102,241,0.18)",
      "border-radius:10px",
      "background:linear-gradient(135deg,rgba(99,102,241,0.06),rgba(34,197,94,0.04))",
      "flex-wrap:wrap",
    ].join(";"),
  });

  // Live indicator (left). Pulsing green dot when something's running;
  // static grey when idle. CLICKABLE when liveCount > 0 — opens the
  // Live-now drawer for per-item progress + drill-down.
  // Affordance: cursor:pointer + hover ring + "▸" arrow signal that
  // this is interactive only when there's something to show.
  const liveClickable = liveCount > 0;
  const liveBlock = el(
    liveClickable ? "button" : "div",
    {
      type: liveClickable ? "button" : undefined,
      style: [
        "display:flex",
        "align-items:center",
        "gap:8px",
        "min-width:120px;",
        liveClickable ? "cursor:pointer" : "",
        liveClickable ? "background:transparent" : "",
        liveClickable ? "border:1px solid transparent" : "border:none",
        liveClickable ? "padding:4px 10px" : "padding:4px 0",
        liveClickable ? "border-radius:8px" : "",
        "transition:border-color 120ms,background 120ms",
        "color:inherit",
        "font:inherit",
      ].join(";"),
      title: liveClickable
        ? `Click to see live progress for ${liveCount} running ${liveCount === 1 ? "item" : "items"}`
        : "Nothing is running right now",
      onmouseover: liveClickable
        ? (e) => {
            e.currentTarget.style.borderColor = "rgba(34,197,94,0.4)";
            e.currentTarget.style.background = "rgba(34,197,94,0.06)";
          }
        : null,
      onmouseout: liveClickable
        ? (e) => {
            e.currentTarget.style.borderColor = "transparent";
            e.currentTarget.style.background = "transparent";
          }
        : null,
      onclick: liveClickable
        ? (e) => {
            e.stopPropagation();
            openLiveNowDrawer(opsLive);
          }
        : null,
    },
    [
      el("span", {
        class:
          liveCount > 0
            ? "status-strip-dot status-strip-dot-live"
            : "status-strip-dot status-strip-dot-idle",
        style: [
          "display:inline-block",
          "width:8px",
          "height:8px",
          "border-radius:50%",
          liveCount > 0 ? "background:var(--green)" : "background:var(--text-4)",
          liveCount > 0 ? "box-shadow:0 0 0 0 rgba(34,197,94,0.7)" : "",
          liveCount > 0 ? "animation:status-pulse 2s ease-out infinite" : "",
        ].join(";"),
      }),
      el(
        "div",
        { style: "display:flex;align-items:baseline;gap:6px;" },
        [
          el(
            "span",
            { style: "font-size:13px;font-weight:600;color:var(--text-1);" },
            liveCount > 0 ? `${liveCount} running` : "Idle",
          ),
          liveCount > 0
            ? el(
                "span",
                {
                  style:
                    "font-size:10px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.04em;",
                },
                "now",
              )
            : null,
          liveClickable
            ? el(
                "span",
                { style: "font-size:11px;color:var(--green);margin-left:4px;font-weight:500;" },
                "▸",
              )
            : null,
        ].filter(Boolean),
      ),
    ],
  );
  wrap.appendChild(liveBlock);

  // Vertical divider before the today-stats group
  wrap.appendChild(
    el("div", {
      style: "width:1px;height:22px;background:rgba(255,255,255,0.08);",
    }),
  );

  // Today's outcomes (center). Shown even when 0 — a "0 sent today" gives
  // honest status, not a hidden gap. Reply-rate hidden when sent=0 (no
  // denominator).
  // Phase J: tooltip surfaces composer breakdown when ops-outcomes returned
  // a `composer` breakout. Lets the user see "X sent: Y composer + Z legacy".
  const composerToday = today?.composer || null;
  const composerSent = composerToday?.sent || 0;
  const composerReplied = composerToday?.replied || 0;
  const legacySent = Math.max(0, sentToday - composerSent);
  const legacyReplied = Math.max(0, repliedToday - composerReplied);
  const sentTooltip =
    composerSent > 0 || legacySent > 0
      ? `Sent today: ${legacySent} legacy + ${composerSent} composer = ${sentToday}`
      : `Sent today across all workflows`;
  const repliedTooltip =
    composerReplied > 0 || legacyReplied > 0
      ? `Replied today: ${legacyReplied} legacy + ${composerReplied} composer = ${repliedToday}`
      : `Replies today across all workflows`;

  const todayBlock = el("div", {
    style: "display:flex;align-items:baseline;gap:14px;flex:1;flex-wrap:wrap;",
  });
  const stat = (icon, num, label, color, tooltip) =>
    el(
      "div",
      {
        style: "display:flex;align-items:baseline;gap:5px;",
        title: tooltip || "",
      },
      [
        el("span", { style: `font-size:13px;color:${color || "#9ca3af"};` }, icon),
        el(
          "span",
          {
            style:
              "font-size:14px;font-weight:600;color:var(--text-1);font-variant-numeric:tabular-nums;",
          },
          String(num),
        ),
        el("span", { style: "font-size:11px;color:var(--text-2);" }, label),
        // Phase J: composer-share badge — small dot when composer contributed
        // to this stat. Shows ratio on hover via the parent tooltip.
        icon === "↑" && composerSent > 0
          ? el(
              "span",
              {
                style:
                  "font-size:9px;color:#5adada;background:rgba(90,218,218,0.12);border:1px solid rgba(90,218,218,0.3);padding:1px 5px;border-radius:8px;margin-left:2px;font-family:ui-monospace,monospace;",
                title: `${composerSent} of ${sentToday} from composer workflows`,
              },
              `${composerSent}`,
            )
          : null,
        icon === "↩" && composerReplied > 0
          ? el(
              "span",
              {
                style:
                  "font-size:9px;color:#5adada;background:rgba(90,218,218,0.12);border:1px solid rgba(90,218,218,0.3);padding:1px 5px;border-radius:8px;margin-left:2px;font-family:ui-monospace,monospace;",
                title: `${composerReplied} of ${repliedToday} from composer workflows`,
              },
              `${composerReplied}`,
            )
          : null,
      ].filter(Boolean),
    );
  todayBlock.appendChild(stat("↑", sentToday, "sent today", "#60a5fa", sentTooltip));
  todayBlock.appendChild(stat("↩", repliedToday, "replied", "#a78bfa", repliedTooltip));
  if (sentToday > 0) {
    const pct = Math.round(replyRateToday * 100);
    todayBlock.appendChild(
      stat("✓", `${pct}%`, "reply rate", "#34d399", `Across ${sentToday} sent today`),
    );
  }
  wrap.appendChild(todayBlock);

  // "Since you were here" delta (right or new line on narrow screens).
  // Only renders when we have a prior visit AND there's something
  // meaningful to report (>0 in any bucket). Variable-reward hook —
  // surfaces what changed without manufacturing suspense.
  if (since && lastVisitAt) {
    const f = since.funnel || {};
    const newReplies = f.replied || 0;
    const newSent = f.sent || 0;
    const newAccepted = f.accepted || 0;
    if (newReplies > 0 || newSent > 0 || newAccepted > 0) {
      const parts = [];
      if (newReplies > 0) parts.push(`${newReplies} new ${newReplies === 1 ? "reply" : "replies"}`);
      if (newAccepted > 0)
        parts.push(`${newAccepted} new ${newAccepted === 1 ? "accept" : "accepts"}`);
      if (newSent > 0 && newReplies === 0 && newAccepted === 0) parts.push(`${newSent} sent`);
      const deltaText = parts.join(" · ");

      // Render as a full-width second row so it doesn't fight the stats
      // for horizontal space at narrower widths.
      const sincePill = el(
        "div",
        {
          style: [
            "flex-basis:100%",
            "display:flex",
            "align-items:center",
            "gap:8px",
            "padding-top:8px",
            "margin-top:2px",
            "border-top:1px dashed rgba(255,255,255,0.06)",
            "font-size:12px",
            "color:var(--text-1)",
          ].join(";"),
        },
        [
          el(
            "span",
            {
              style:
                "font-size:11px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.05em;",
            },
            "Since you were here",
          ),
          el("span", { style: "font-size:13px;font-weight:500;color:#fde68a;" }, deltaText),
        ],
      );
      wrap.appendChild(sincePill);
    }
  }

  // Pulse animation injected once per page (CSS keyframes can't be inlined
  // on the element). Idempotent — only inserts if missing.
  if (!document.getElementById("status-strip-pulse-css")) {
    const style = document.createElement("style");
    style.id = "status-strip-pulse-css";
    style.textContent = `
      @keyframes status-pulse {
        0%   { box-shadow: 0 0 0 0   rgba(34,197,94,0.55); }
        70%  { box-shadow: 0 0 0 8px rgba(34,197,94,0);   }
        100% { box-shadow: 0 0 0 0   rgba(34,197,94,0);   }
      }
    `;
    document.head.appendChild(style);
  }

  return wrap;
}

// ── Day-1 outcome picker (Phase D progressive disclosure) ──────────
//
// True newcomers (no pins, no missions, no customs, ≤2 visits, no
// goal selected yet) see a goal-picker hero INSTEAD of an empty
// cockpit. Picking a goal batch-pins 3 starter automations matched
// to that outcome — collapsing 60 cards into "here are your 3
// starters" on day one.
//
// All client-side: localStorage["automations:goal"] stores either
// the chosen goal id OR the literal string "skipped". Either value
// hides the picker forever. We don't touch the backend onboarding_progress
// table here — that lives at a higher abstraction (whole-product
// onboarding, not Automations-page onboarding) and would couple
// this surface to a server round-trip we don't need.

const GOAL_KEY = "automations:goal";
const GOAL_AT_KEY = "automations:goalAt";

// Each goal maps to 3 starter pins. Pin ids must match the format
// used elsewhere (preset:<id>, platform:<id>, operation:<id>).
// If a referenced id doesn't exist in the user's data, the pin
// will land but resolveTarget will return missing → user can ✕ it.
const ONBOARDING_GOALS = [
  {
    id: "leads",
    icon: "target",
    label: "Get more leads",
    blurb: "Find B2B prospects across X + LinkedIn and route them into outreach.",
    starters: [
      { pinId: "operation:service_outreach", label: "Service Outreach" },
      { pinId: "preset:signal_linkedin", label: "LinkedIn signals" },
      { pinId: "preset:signal_twitter", label: "Twitter signals" },
    ],
  },
  {
    id: "meetings",
    icon: "message",
    label: "Land sales meetings",
    blurb: "Multi-step outreach with email sequences and follow-ups.",
    starters: [
      { pinId: "preset:outreach_orchestrator", label: "Outreach orchestrator" },
      { pinId: "preset:email_sequence", label: "Email sequence" },
      { pinId: "operation:hiring_outreach", label: "Hiring outreach" },
    ],
  },
  {
    id: "audience",
    icon: "trend",
    label: "Grow my audience",
    blurb: "Engagement on X and LinkedIn — likes, replies, follower growth.",
    starters: [
      { pinId: "platform:x_engage_feed_ai", label: "X auto-engage" },
      { pinId: "platform:li_engage_feed", label: "LinkedIn engage feed" },
      { pinId: "platform:x_follower_sniper", label: "X follower sniper" },
    ],
  },
  {
    id: "opportunities",
    icon: "bolt",
    label: "Track opportunities",
    blurb: "Funding rounds, hiring waves, new tokens, events worth being early on.",
    starters: [
      { pinId: "preset:opportunity_defillama", label: "DeFi opportunities" },
      { pinId: "preset:opportunity_jobs", label: "Hiring opportunities" },
      { pinId: "operation:event_promotion", label: "Event promotion" },
    ],
  },
];

// Decide whether to show the picker. Conservative — many AND'd
// conditions so we never surprise existing users.
function shouldShowDayOnePicker({ workflows }) {
  try {
    const goalSet = localStorage.getItem(GOAL_KEY);
    if (goalSet) return false; // already chose or skipped
    const visits = Number(localStorage.getItem(VISITS_KEY) || 0);
    if (visits > 2) return false; // not a newcomer
    if (getPinnedAutomations().length > 0) return false;
    if (getMissions().length > 0) return false;
    const customs = (workflows || []).filter((w) => w.kind === "custom");
    if (customs.length > 0) return false; // they've cloned things — not a newcomer
    return true;
  } catch {
    return false;
  }
}

// Apply a picked goal: batch-pin its starter automations, stamp the
// goal as chosen, dismiss the picker.
function applyOnboardingGoal(goal) {
  const list = getPinnedAutomations();
  for (const s of goal.starters) {
    if (!list.some((p) => p.id === s.pinId)) {
      list.push({ id: s.pinId, label: s.label, pinnedAt: new Date().toISOString() });
    }
  }
  setPinnedAutomations(list);
  try {
    localStorage.setItem(GOAL_KEY, goal.id);
    localStorage.setItem(GOAL_AT_KEY, new Date().toISOString());
  } catch {}
  toast(`Pinned ${goal.starters.length} starters for "${goal.label}"`, "ok", 3000);
  logTelemetry("automations", "goal_set", { goal: goal.id });
}

function dismissDayOnePicker() {
  try {
    localStorage.setItem(GOAL_KEY, "skipped");
    localStorage.setItem(GOAL_AT_KEY, new Date().toISOString());
  } catch {}
  logTelemetry("automations", "goal_skipped");
}

// Build the picker hero. Returns null if conditions don't match (so
// the caller can splice it in conditionally without an if/else).
function buildDayOnePicker(workflows) {
  if (!shouldShowDayOnePicker({ workflows })) return null;

  const grid = el("div", {
    style: [
      "display:grid",
      "grid-template-columns:repeat(auto-fit,minmax(280px,1fr))",
      "gap:12px",
      "margin-top:14px",
    ].join(";"),
  });

  for (const goal of ONBOARDING_GOALS) {
    const card = el(
      "button",
      {
        type: "button",
        style: [
          "background:var(--bg-2)",
          "border:1px solid var(--border)",
          "border-radius:10px",
          "padding:16px 18px",
          "text-align:left",
          "cursor:pointer",
          "display:flex",
          "flex-direction:column",
          "gap:10px",
          "transition:border-color 150ms,transform 150ms,box-shadow 150ms",
        ].join(";"),
        onmouseover: (e) => {
          e.currentTarget.style.borderColor = "#fbbf24";
          e.currentTarget.style.transform = "translateY(-2px)";
          e.currentTarget.style.boxShadow = "0 8px 24px -8px rgba(251,191,36,0.25)";
        },
        onmouseout: (e) => {
          e.currentTarget.style.borderColor = "var(--border)";
          e.currentTarget.style.transform = "translateY(0)";
          e.currentTarget.style.boxShadow = "none";
        },
        onclick: () => {
          applyOnboardingGoal(goal);
          // Re-render so the picker disappears and the user sees their
          // new pins land in the shortcuts row.
          try {
            renderAutomations();
          } catch {}
        },
      },
      [
        el("div", { style: "display:flex;align-items:center;gap:10px;" }, [
          el("div", { style: "font-size:24px;line-height:1;" }, goal.icon),
          el("div", { style: "font-size:15px;font-weight:600;color:var(--text-1);" }, goal.label),
        ]),
        el("div", { style: "font-size:12px;color:var(--text-2);line-height:1.5;" }, goal.blurb),
        el(
          "div",
          {
            style: "display:flex;flex-wrap:wrap;gap:5px;margin-top:6px;",
          },
          goal.starters.map((s) =>
            el(
              "span",
              {
                style: [
                  "font-size:10px",
                  "background:rgba(251,191,36,0.08)",
                  "color:#fde68a",
                  "border:1px solid rgba(251,191,36,0.25)",
                  "padding:2px 8px",
                  "border-radius:10px",
                  "font-family:ui-monospace,monospace",
                ].join(";"),
              },
              s.label,
            ),
          ),
        ),
        el(
          "div",
          {
            style: "font-size:11px;color:#fbbf24;font-weight:600;margin-top:auto;padding-top:6px;",
          },
          "Pin these 3 starters →",
        ),
      ],
    );
    grid.appendChild(card);
  }

  return el(
    "div",
    {
      id: "day-one-picker",
      style: [
        "padding:18px 20px",
        "margin:8px 0 16px",
        "border:1px solid rgba(251,191,36,0.18)",
        "border-radius:12px",
        "background:linear-gradient(135deg,rgba(251,191,36,0.04),rgba(99,102,241,0.03))",
      ].join(";"),
    },
    [
      el("div", { style: "display:flex;align-items:center;gap:10px;" }, [
        el("div", { style: "font-size:18px;" }, icon("user")),
        el("div", { style: "flex:1;" }, [
          el(
            "div",
            { style: "font-size:16px;font-weight:600;color:var(--text-1);" },
            "What do you want to do first?",
          ),
          el(
            "div",
            { style: "font-size:12px;color:var(--text-2);margin-top:3px;" },
            "Pick a goal — we'll pin 3 starter automations to your shortcuts. You can change them anytime.",
          ),
        ]),
        el(
          "button",
          {
            type: "button",
            style: [
              "background:transparent",
              "border:none",
              "color:var(--text-2)",
              "font-size:12px",
              "cursor:pointer",
              "padding:6px 10px",
              "text-decoration:underline",
            ].join(";"),
            title: "Skip the picker and browse the catalog yourself",
            onclick: () => {
              dismissDayOnePicker();
              try {
                renderAutomations();
              } catch {}
            },
          },
          "Skip — show me everything",
        ),
      ]),
      grid,
    ],
  );
}

// ── Cockpit: pinned shortcuts & needs-attention (Phase A of reframe) ─
//
// The Automations page's new front door. Three rows total:
//   Row 1: Status strip (above) — what the system produced
//   Row 2: Needs attention   — errors / stale / broken / ready-for-action
//   Row 3: Your shortcuts    — user-pinned cards with one-click run
//
// Pin storage: localStorage["automations:pinned"] = JSON array of
//   { id: "preset:<id>" | "custom:<id>" | "platform:<id>" | "operation:<id>",
//     label: "Service Outreach",
//     pinnedAt: ISO timestamp }
//
// Pin id prefix tells the runner what to do when the user clicks "Run":
//   preset:    presets are runnable only via clone — "Configure" instead
//   custom:    calls runAutomationNow() — works only if status=active+wired
//   platform:  chrome.tabs.create(launchUrl) — opens the platform tab
//   operation: navigate(`new-workflow/${templateKey}`) — wizard

const PINNED_KEY = "automations:pinned";

function getPinnedAutomations() {
  try {
    const raw = localStorage.getItem(PINNED_KEY);
    if (!raw) return [];
    const arr = JSON.parse(raw);
    return Array.isArray(arr) ? arr : [];
  } catch {
    return [];
  }
}

function setPinnedAutomations(list) {
  try {
    localStorage.setItem(PINNED_KEY, JSON.stringify(list));
  } catch (err) {
    console.warn("[automations] pin write failed:", err);
  }
}

function isPinnedAutomation(pinId) {
  return getPinnedAutomations().some((p) => p.id === pinId);
}

function togglePinnedAutomation(pinId, label) {
  const list = getPinnedAutomations();
  const idx = list.findIndex((p) => p.id === pinId);
  if (idx >= 0) {
    list.splice(idx, 1);
    setPinnedAutomations(list);
    return false;
  }
  list.push({ id: pinId, label: String(label || pinId), pinnedAt: new Date().toISOString() });
  setPinnedAutomations(list);
  return true;
}

// ── Library: account-picker driven catalog (Phase B of reframe) ─────
//
// The Automations page's second layer. Sits below the cockpit and is
// driven by `dashboardState.contextAccount` (the topbar picker — same
// pattern Campaigns and Signals already use).
//
// Three rendering modes:
//   ctx === null            → All-accounts view: 3 buckets
//                             (Cross-account campaigns / Per-account
//                              prompt / Background services collapsed)
//   ctx.platform === "..."  → Per-account view: only that platform's
//                             actions + "Participates in" callout
//
// Why three buckets in All-accounts: the page contains three primitives
// that don't share a mental model — per-account actions (28), cross-
// account campaigns (~9), and background services (~12-14). Forcing
// them into one grid is the original sin we're undoing.

// Preset ids that run as PASSIVE BACKGROUND SERVICES — they don't have
// a target list to run "against" (no contacts to outreach to, etc.).
// User mental model: "the system runs these on its own." Anything not
// in this set lands in cross-account campaigns.
const BACKGROUND_PRESET_IDS = new Set([
  "signal_twitter",
  "signal_linkedin",
  "signal_reddit",
  "signal_hn",
  "opportunity_defillama",
  "opportunity_rootdata",
  "opportunity_jobs",
  "top_tokens_ingest",
  "feed_monitor",
  "enrichment_pipeline",
  "group_join",
  "cross_account_react",
  "daily_briefing",
  "account_warmer",
]);

function classifyPreset(presetId) {
  return BACKGROUND_PRESET_IDS.has(presetId) ? "background" : "cross_account";
}

const LIBRARY_EXPANDED_KEY = "automations:libraryExpanded";
const LIBRARY_AUTOCOLLAPSED_KEY = "automations:libraryAutoCollapsedAt";
const VISITS_KEY = "automations:visits";

// Library expanded resolution (Phase D progressive disclosure, revised):
//
//   1. If user has explicit preference saved (LIBRARY_EXPANDED_KEY) →
//      respect it absolutely. Once they manually toggle, we never override.
//   2. Else, if the user has no workflows at all (no compositions, no
//      customs) → expand by default. The library IS the only useful
//      surface for a fresh user; collapsing it leaves them staring at
//      empty cockpit panels.
//   3. Else → collapsed by default. Once the user has any workflow, the
//      live cockpit is the dominant signal; the legacy preset library is
//      a build-time discovery surface that belongs on the Build page.
//
// Phase D rationale: the old visits-based auto-collapse heuristic took
// 3 visits + a pin to fire. For the IA to feel right, we need the
// library collapsed from the first visit for any returning user.
function getLibraryExpanded({ hasAnyWorkflows = true } = {}) {
  try {
    const explicit = localStorage.getItem(LIBRARY_EXPANDED_KEY);
    if (explicit === "0") return false;
    if (explicit === "1") return true;
    // No explicit preference: expand only if the user has nothing.
    if (!hasAnyWorkflows) return true;
    return false;
  } catch {
    return true;
  }
}
function setLibraryExpanded(expanded) {
  // User toggle: persist as explicit preference. Once set, the heuristic
  // is skipped forever — the user's choice wins.
  try {
    localStorage.setItem(LIBRARY_EXPANDED_KEY, expanded ? "1" : "0");
  } catch {}
}

// Top-level library entry — replaces the old `sections` block in
// renderAutomations. Reads the topbar picker from dashboardState and
// branches into all-accounts vs. per-account rendering.
function buildLibrary(ctx, { workflows, presets, customs, compositions = [], presetById }) {
  // Phase D: collapse by default unless the user has zero workflows.
  // Compositions count too — a user with composer-built workflows but no
  // legacy customs should still get a collapsed legacy library.
  const hasAnyWorkflows = customs.length + compositions.length > 0;
  const expanded = getLibraryExpanded({ hasAnyWorkflows });
  const totalCount =
    presets.length +
    customs.length +
    compositions.length +
    (Array.isArray(window.PLATFORM_ACTIONS) ? window.PLATFORM_ACTIONS.length : 0) +
    (typeof OPERATIONS !== "undefined" && Array.isArray(OPERATIONS) ? OPERATIONS.length : 0);

  // Header — collapse toggle + account-context label.
  const ctxLabel = ctx ? ctx.label : "All accounts";
  const header = el(
    "div",
    {
      style: [
        "display:flex",
        "align-items:center",
        "gap:10px",
        "padding:10px 0 8px",
        "margin-top:14px",
        "border-bottom:1px solid var(--border)",
      ].join(";"),
    },
    [
      el(
        "button",
        {
          type: "button",
          style: [
            "background:transparent",
            "border:none",
            "cursor:pointer",
            "color:var(--text-1)",
            "font-size:14px",
            "font-weight:600",
            "display:flex",
            "align-items:center",
            "gap:8px",
            "padding:0",
          ].join(";"),
          onclick: (e) => {
            e.stopPropagation();
            // Toggle: invert current state and persist as explicit preference.
            const nowExpanded = !expanded;
            setLibraryExpanded(nowExpanded);
            // Re-render the library section in place so toggling feels instant.
            const mount = document.getElementById("automations-library-mount");
            if (mount) {
              const fresh = buildLibrary(dashboardState.contextAccount, {
                workflows,
                presets,
                customs,
                compositions,
                presetById,
              });
              mount.replaceWith(fresh);
            }
            logTelemetry("automations", nowExpanded ? "library_expand" : "library_collapse");
          },
          title: expanded
            ? "Click to collapse the automation library"
            : "Click to expand the automation library",
        },
        [
          el("span", { style: "font-size:12px;color:var(--text-2);" }, expanded ? "▾" : "▸"),
          el("span", {}, "Automation library"),
          el(
            "span",
            {
              style:
                "font-size:11px;color:var(--text-4);font-family:ui-monospace,monospace;font-weight:400;",
            },
            `${totalCount} items`,
          ),
          // When collapsed, a soft "Browse →" hint signals it's clickable.
          !expanded
            ? el(
                "span",
                {
                  style: "font-size:11px;color:#fbbf24;font-weight:500;margin-left:6px;",
                },
                "Browse →",
              )
            : null,
        ],
      ),
      el("span", { style: "flex:1;" }),
      // Context indicator: shows what filter the picker is applying.
      el(
        "div",
        {
          style: [
            "display:inline-flex",
            "align-items:center",
            "gap:6px",
            "padding:4px 10px",
            "background:" + (ctx ? "rgba(99,102,241,0.12)" : "rgba(255,255,255,0.03)"),
            "border:1px solid " + (ctx ? "rgba(99,102,241,0.35)" : "#26262a"),
            "border-radius:14px",
            "font-size:11px",
            "color:" + (ctx ? "#a5b4fc" : "#9ca3af"),
          ].join(";"),
          title: ctx
            ? "Topbar picker is set — this account's per-account actions are expanded below."
            : "All accounts. Pick one in the topbar to jump to its per-account actions.",
        },
        [el("span", { style: "font-size:10px;" }, ctx ? "👤" : "🌐"), el("span", {}, ctxLabel)],
      ),
    ],
  );

  const mount = el("div", { id: "automations-library-mount" }, [header]);
  if (!expanded) return mount;

  const body = el("div", {
    style: "display:flex;flex-direction:column;gap:24px;padding:16px 0 64px;",
  });

  // Three fixed zones, always rendered in this order:
  //   1) Per-account actions  2) Campaigns & workflows  3) Background services
  // The topbar account picker (ctx) no longer switches layouts — it just
  // expands the matching per-account sub-group in zone 1 and collapses the rest.
  const connectedAccounts = getConnectedAccountsForLibrary();
  const crossAccountPresets = presets.filter((p) => classifyPreset(p.id) === "cross_account");
  const backgroundPresets = presets.filter((p) => classifyPreset(p.id) === "background");

  body.appendChild(buildPerAccountActionsZone(connectedAccounts, ctx));
  body.appendChild(buildCampaignsZone({ compositions, customs, crossAccountPresets, presets }));
  if (backgroundPresets.length > 0) {
    body.appendChild(buildBackgroundServicesSection(backgroundPresets));
  }

  mount.appendChild(body);
  return mount;
}

// ── Coverage strip ───────────────────────────────────────────────────
// Compact bar showing testing progress: how many preset workflow types
// have at least one verified-working run vs. how many are still untested.
// Hidden when the user has 0 clones (nothing to verify yet).

function buildCoverageStrip(coverage) {
  if (!coverage || !coverage.totalPresets) return null;
  const total = coverage.totalPresets || 0;
  const cloned = coverage.clonedPresets || 0;
  const verified = coverage.verifiedWorking || 0;
  const broken = coverage.verifiedBroken || 0;
  const recent = coverage.recentAnalyses || {};
  const recent7d = (recent.contacts7d || 0) + (recent.companies7d || 0);
  const recent24h = (recent.contacts24h || 0) + (recent.companies24h || 0);
  // Render the strip if EITHER the user has cloned customs OR there's
  // recent enrich-then-analyze activity. A new operator might never clone
  // a workflow but still run lots of CRM analyses — they should see a
  // "you've analyzed 47 entities this week" signal.
  if (cloned === 0 && recent7d === 0) return null;
  const untested = cloned - verified - broken;
  const pct = total > 0 ? Math.round((verified / total) * 100) : 0;

  const stat = (label, value, color) =>
    el(
      "div",
      {
        style: "display:flex;flex-direction:column;gap:2px;min-width:80px;",
      },
      [
        el(
          "div",
          {
            style: `font-size:18px;font-weight:600;font-family:ui-monospace,monospace;color:${color};`,
          },
          String(value),
        ),
        el(
          "div",
          {
            style:
              "font-size:10px;color:var(--text-4);text-transform:uppercase;letter-spacing:0.05em;",
          },
          label,
        ),
      ],
    );

  // Sub-line varies with state. If the user has clones, lead with workflow
  // coverage; the recent-analyses count tucks in as a secondary stat.
  const headlineLine =
    cloned > 0
      ? `Testing coverage: ${verified} of ${total} preset types verified`
      : `Recent AI analyses: ${recent7d} entities (last 7 days)`;
  const subLine =
    cloned > 0
      ? `${cloned} cloned · ${untested} untested · ${broken} flagged broken · ${pct}% complete`
      : "Click any preset to clone and start verifying — coverage tracks here.";

  const stats = [
    cloned > 0 ? stat("Working", verified, "#a7f3d0") : null,
    cloned > 0 ? stat("Broken", broken, "#fca5a5") : null,
    cloned > 0 ? stat("Untested", untested, "#cbd5e1") : null,
    // Always show the analyses-7d stat — it's the orchestrator activity
    // signal the user requested. Color goes blue (informational, not a
    // pass/fail metric).
    recent7d > 0
      ? stat(`Analyses 7d${recent24h > 0 ? ` (${recent24h} today)` : ""}`, recent7d, "#93c5fd")
      : null,
  ].filter(Boolean);

  return el(
    "div",
    {
      class: "card",
      style:
        "padding:12px 16px;margin-bottom:12px;border-left:3px solid var(--green);display:flex;align-items:center;gap:24px;flex-wrap:wrap;",
      title:
        "Coverage = how many preset workflow types you've verified end-to-end. Click any run row to mark it ✓ working / ✗ broken. Analyses 7d counts contacts + companies AI-analyzed via the enrich-then-analyze flow.",
    },
    [
      el("div", { style: "display:flex;flex-direction:column;gap:2px;flex:1;min-width:160px;" }, [
        el("div", { style: "font-size:13px;font-weight:600;color:var(--text-1);" }, headlineLine),
        el("div", { style: "font-size:11px;color:var(--text-2);" }, subLine),
      ]),
      ...stats,
    ],
  );
}

// ── Signal pursue wizard ──────────────────────────────────────────

async function renderSignalPursue({ signalId }) {
  showLoading("Fetching post context + drafting comment…");
  const data = await api("signal-preview", { signalId });
  if (data?.error && !data?.signal) {
    // Total failure — no signal data at all
    showError(data.error, () => renderSignalPursue({ signalId }));
    return;
  }
  if (data?.alreadyEngaged) {
    const log = data.engagementLog;
    const body = el("div", { class: "alert-card-body" });
    body.innerHTML =
      `Engaged at: ${escapeHtml(log.timestamp || "unknown")}<br>` +
      (log.liked ? "Liked<br>" : "") +
      (log.commented ? "Commented<br>" : "") +
      (log.connectRequested ? "Connection request sent<br>" : "") +
      (log.messaged ? "DM sent<br>" : "") +
      (log.commentText
        ? `<br><strong>Comment:</strong><br><em>${escapeHtml(log.commentText)}</em>`
        : "");
    setContentNode(
      el("div", { class: "wizard" }, [
        el("div", { class: "alert-card" }, [
          el("div", { class: "alert-card-title" }, "Already engaged"),
          body,
        ]),
        el(
          "button",
          {
            class: "btn btn-secondary",
            type: "button",
            onclick: () => {
              location.hash = "#signals";
            },
          },
          "« Back",
        ),
      ]),
    );
    return;
  }

  // postContext may be null if fetching the post failed — still let user proceed with outreach
  const hasPost = data.postContext && data.postContext.postText;
  dashboardState.signalWizard = {
    signalId,
    signal: data.signal,
    postContext: data.postContext || {
      platform: data.signal.platform || "linkedin",
      authorName: data.signal.source_handle,
      postText: null,
    },
    connectionState: data.connectionState || "unknown",
    comment: data.comment || "",
    toggles: { doLike: !!hasPost, doComment: !!hasPost && !!data.comment, doFollowUp: true },
    noPostContext: !hasPost,
    errorReason: data.errorReason || data.error || null,
  };
  renderSignalPursueView();
}

function renderSignalPursueView() {
  const s = dashboardState.signalWizard;
  if (!s) return navigate("signals");

  // Reddit / HackerNews — read-only platforms. We branch into a manual-assist
  // view that drops the like/comment/follow toggles and replaces Publish with
  // Copy + Open buttons. The bot doesn't have OAuth on these platforms so we
  // ship the user a draft + the post URL and they paste/post themselves.
  const sigPlatform = String(s.postContext.platform || s.signal?.platform || "").toLowerCase();
  if (sigPlatform === "reddit" || sigPlatform === "hackernews") {
    return renderSignalPursueManualAssist(s, sigPlatform);
  }

  const platform = s.postContext.platform || "linkedin";
  const connectLine =
    platform === "linkedin"
      ? s.connectionState === "connected"
        ? "\u2705 Already connected"
        : s.connectionState === "not_connected"
          ? "\u274c Not connected"
          : "\u2753 Status unknown"
      : null;

  const followupLabel =
    platform === "linkedin"
      ? s.connectionState === "connected"
        ? "Send DM"
        : "Connection request"
      : "Follow";

  const content = el("div", { class: "wizard" }, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el(
          "button",
          { class: "btn btn-secondary btn-tiny", onclick: () => navigate("signals") },
          "\u00ab Back",
        ),
        el(
          "div",
          { class: "page-title", style: "margin-top: 10px;" },
          `\ud83c\udfaf Pursue Signal #${s.signalId}`,
        ),
        el(
          "div",
          { class: "page-subtitle" },
          `${s.postContext.authorName || s.signal.source_handle} \u00b7 matched "${s.signal.matched_keyword}"`,
        ),
      ]),
    ]),

    // Warning if no post context
    s.noPostContext
      ? el("div", { class: "alert-card warning", style: "margin-bottom: 16px;" }, [
          el("div", { class: "alert-card-title" }, "\u26a0\ufe0f No recent post found"),
          el("div", { class: "alert-card-body" }, [
            s.errorReason ||
              "Couldn't fetch their latest post. You can still send a connection request or DM directly.",
          ]),
        ])
      : null,

    // Post section (only if we have post content)
    s.postContext.postText
      ? el("div", { class: "wizard-section" }, [
          el("div", { class: "wizard-section-title" }, "\ud83d\udcdd Their latest post"),
          el("div", { class: "wizard-post" }, s.postContext.postText),
        ])
      : null,

    // Comment section (only when we have a post to engage with)
    !s.noPostContext
      ? el("div", { class: "wizard-section" }, [
          el(
            "div",
            { class: "wizard-section-title" },
            `\ud83d\udcac Draft ${platform === "twitter" ? "reply" : "comment"}`,
          ),
          el(
            "textarea",
            {
              class: "wizard-textarea",
              id: "signal-comment-input",
              oninput: (e) => {
                dashboardState.signalWizard.comment = e.target.value;
                const counter = $("#signal-char-counter");
                if (counter) {
                  const max = platform === "twitter" ? 240 : 260;
                  const len = e.target.value.length;
                  counter.textContent = `${len}/${max}`;
                  counter.classList.toggle("over", len > max);
                }
              },
            },
            s.comment || "",
          ),
          (() => {
            const max = platform === "twitter" ? 240 : 260;
            const len = (s.comment || "").length;
            return el(
              "div",
              { class: `char-counter${len > max ? " over" : ""}`, id: "signal-char-counter" },
              `${len}/${max}`,
            );
          })(),
          el("div", { class: "wizard-actions" }, [
            el(
              "button",
              {
                class: "btn btn-secondary",
                onclick: async () => {
                  toast("Regenerating\u2026", "info");
                  const r = await api("signal-regen-comment", { signalId: s.signalId });
                  if (r?.ok) {
                    dashboardState.signalWizard.comment = r.comment;
                    const inp = $("#signal-comment-input");
                    if (inp) inp.value = r.comment;
                    toast("Regenerated", "ok");
                  } else toast(`Error: ${r?.error || "failed"}`, "err");
                },
              },
              "\ud83d\udd04 Regenerate",
            ),
          ]),
        ])
      : null,

    el("div", { class: "wizard-section" }, [
      el("div", { class: "wizard-section-title" }, "\ud83c\udfaf Actions"),
      connectLine ? el("div", { class: "wizard-label" }, connectLine) : null,
      // Only show like/comment toggles if we have a post
      !s.noPostContext
        ? toggleRow("Like the post", s.toggles.doLike, (v) => {
            s.toggles.doLike = v;
            renderSignalPursueView();
          })
        : null,
      !s.noPostContext
        ? toggleRow(
            `Post ${platform === "twitter" ? "reply" : "comment"}`,
            s.toggles.doComment,
            (v) => {
              s.toggles.doComment = v;
              renderSignalPursueView();
            },
          )
        : null,
      // Always show follow-up (connect/DM/follow) — this works without a post
      toggleRow(followupLabel, s.toggles.doFollowUp, (v) => {
        s.toggles.doFollowUp = v;
        renderSignalPursueView();
      }),
    ]),

    el("div", { class: "wizard-actions" }, [
      el("button", { class: "btn btn-primary", onclick: publishSignal }, "Publish"),
      el("button", { class: "btn btn-secondary", onclick: () => navigate("signals") }, "Cancel"),
    ]),
  ]);
  setContentNode(content);
}

function toggleRow(label, on, onchange) {
  const row = el("div", { class: "toggle-row" }, [
    el("div", { class: "toggle-label" }, label),
    el("div", {
      class: "toggle" + (on ? " on" : ""),
      onclick: () => onchange(!on),
    }),
  ]);
  return row;
}

async function publishSignal() {
  const s = dashboardState.signalWizard;
  if (!s) return;
  toast("Publishing…", "info");
  const result = await api("signal-publish", {
    signalId: s.signalId,
    body: {
      comment: s.comment,
      toggles: s.toggles,
      postContext: s.postContext,
      connectionState: s.connectionState,
    },
  });
  if (result?.ok || result?.actions) {
    const okCount = (result.actions || []).filter((a) => a.status === "ok").length;
    const errCount = (result.actions || []).filter((a) => a.status === "error").length;
    toast(`${okCount} succeeded, ${errCount} failed`, errCount > 0 ? "info" : "ok", 6000);
    dashboardState.signalWizard = null;
    setTimeout(() => navigate("signals"), 1500);
  } else {
    toast(`Error: ${result?.error || "publish failed"}`, "err");
  }
}

// ── Reddit / HackerNews manual-assist wizard ─────────────────────
// These platforms have no OAuth integration — we ship the user a draft and
// the post URL; they paste/post manually. The wizard mirrors the auto-publish
// view's layout (post preview + comment textarea + actions) but swaps the
// publish button for Copy / Open buttons.
function renderSignalPursueManualAssist(s, platform) {
  const platformLabel = platform === "reddit" ? "Reddit" : "HackerNews";
  const maxChars = platform === "reddit" ? 500 : 400;

  const content = el("div", { class: "wizard" }, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el(
          "button",
          { class: "btn btn-secondary btn-tiny", onclick: () => navigate("signals") },
          "« Back",
        ),
        el(
          "div",
          { class: "page-title", style: "margin-top: 10px;" },
          `${platform === "reddit" ? "🟠" : "🟧"} Pursue Signal #${s.signalId}`,
        ),
        el(
          "div",
          { class: "page-subtitle" },
          `${s.postContext.authorName || s.signal.source_handle} on ${platformLabel} · matched "${s.signal.matched_keyword}"`,
        ),
      ]),
    ]),

    el(
      "div",
      {
        class: "alert-card",
        style: "margin-bottom: 16px;background: #1f2c3a;border-color: #355f8a;color: #93c5fd;",
      },
      [
        el("div", { class: "alert-card-title" }, `Manual-assist mode`),
        el(
          "div",
          { class: "alert-card-body" },
          `${platformLabel} doesn't expose a posting API we have OAuth for. The bot drafts a comment and gives you the post URL — you paste it manually. Your action is logged so the signal moves to "pursuing" and won't resurface.`,
        ),
      ],
    ),

    el("div", { class: "wizard-section" }, [
      el("div", { class: "wizard-section-title" }, `The post`),
      el("div", { class: "wizard-post" }, s.postContext.postText || "(no text cached)"),
    ]),

    el("div", { class: "wizard-section" }, [
      el("div", { class: "wizard-section-title" }, `Draft comment`),
      el(
        "textarea",
        {
          class: "wizard-textarea",
          id: "signal-comment-input",
          oninput: (e) => {
            dashboardState.signalWizard.comment = e.target.value;
            const counter = $("#signal-char-counter");
            if (counter) {
              const len = e.target.value.length;
              counter.textContent = `${len}/${maxChars}`;
              counter.classList.toggle("over", len > maxChars);
            }
          },
        },
        s.comment || "",
      ),
      (() => {
        const len = (s.comment || "").length;
        return el(
          "div",
          { class: `char-counter${len > maxChars ? " over" : ""}`, id: "signal-char-counter" },
          `${len}/${maxChars}`,
        );
      })(),
      el("div", { class: "wizard-actions" }, [
        el(
          "button",
          {
            class: "btn btn-secondary",
            onclick: async () => {
              toast("Regenerating…", "info");
              const r = await api("signal-regen-comment", { signalId: s.signalId });
              if (r?.ok) {
                dashboardState.signalWizard.comment = r.comment;
                const inp = $("#signal-comment-input");
                if (inp) inp.value = r.comment;
                toast("Regenerated", "ok");
              } else toast(`Error: ${r?.error || "failed"}`, "err");
            },
          },
          "Regenerate",
        ),
      ]),
    ]),

    el("div", { class: "wizard-section" }, [
      el("div", { class: "wizard-section-title" }, "Manual-assist actions"),
      el("div", { class: "wizard-actions", style: "gap: 8px;flex-wrap:wrap;" }, [
        el(
          "button",
          { class: "btn btn-primary", onclick: () => publishSignalManualAssist({ copy: true }) },
          "Copy comment",
        ),
        el(
          "button",
          { class: "btn btn-secondary", onclick: () => publishSignalManualAssist({ open: true }) },
          "↗ Open post",
        ),
        el(
          "button",
          {
            class: "btn btn-primary",
            onclick: () => publishSignalManualAssist({ copy: true, open: true }),
          },
          "Copy + Open",
        ),
        el("button", { class: "btn btn-secondary", onclick: () => navigate("signals") }, "Done"),
      ]),
    ]),
  ]);
  setContentNode(content);
}

async function publishSignalManualAssist({ copy, open }) {
  const s = dashboardState.signalWizard;
  if (!s) return;
  const comment = String(s.comment || "");
  const url = s.postContext?.postUrl;

  if (copy && comment) {
    try {
      await navigator.clipboard.writeText(comment);
      toast("Comment copied", "ok");
    } catch {
      toast("Clipboard unavailable — select the text manually", "err");
    }
  }
  if (open && url) {
    window.open(url, "_blank", "noopener,noreferrer");
  }

  // Best-effort: tell backend so signal status updates new → pursuing and the
  // engagement_log records the user's draft. Manual-assist mode skips
  // platform calls and just records intent.
  try {
    await api("signal-publish", {
      signalId: s.signalId,
      body: {
        comment,
        toggles: { doLike: false, doComment: false, doFollowUp: false },
        postContext: s.postContext,
        mode: "manual_assist",
      },
    });
  } catch {
    /* non-fatal */
  }
}

// ── CRM row status indicators (v64) ─────────────────────────────
// At-a-glance signals so the operator can scan the list and immediately
// see which rows have been enriched, which are AI-analyzed, and which
// are still empty. Shown as a single compact column of colored dots:
//
//   📧 = email     (filled = green, empty = ghost-gray)
//   💼 = linkedin  (filled = green, empty = ghost-gray)
//   🐦 = twitter   (filled = green, empty = ghost-gray)
//   🤖 = AI scored (filled = blue, empty = ghost-gray)
//
// For companies the email dot is replaced with 🌐 website + 💬 telegram.

function _statusDot(icon, isFilled, tooltip) {
  return el(
    "span",
    {
      title: tooltip,
      style: `display:inline-block;margin-right:3px;font-size:13px;line-height:1;
            opacity:${isFilled ? "1" : "0.22"};
            filter:${isFilled ? "none" : "grayscale(1)"};
            transition:opacity 200ms ease;`,
    },
    icon,
  );
}

function renderContactStatusCell(c) {
  const hasEmail = !!(c.email && String(c.email).trim());
  const hasLi = !!(c.linkedin_url && String(c.linkedin_url).trim());
  const hasTwitter = !!(c.twitter_handle && String(c.twitter_handle).trim());
  const aiAt = c.ai_lead_analysis_at;
  const aiAnalyzed = !!aiAt;
  // Tooltip shows the date when something is set, "missing" when not.
  const tip = (label, val) => (val ? `${label}: ${val}` : `${label}: missing`);
  return el("td", { style: "white-space:nowrap;width:100px;" }, [
    _statusDot("📧", hasEmail, tip("email", c.email)),
    _statusDot("💼", hasLi, tip("linkedin", c.linkedin_url)),
    _statusDot("🐦", hasTwitter, tip("twitter", c.twitter_handle ? `@${c.twitter_handle}` : null)),
    _statusDot(
      "🤖",
      aiAnalyzed,
      aiAnalyzed ? `AI analyzed ${fmtDate(aiAt)}` : "Not yet AI-analyzed",
    ),
  ]);
}

/**
 * Big confidence badge for the company drawer. Shows a colored bar +
 * percentage + a per-signal breakdown when the operator hovers. The
 * value here mirrors the small ✓/?/⚠ chevron in the table — same data,
 * just the loud version so the operator sees it before clicking any
 * outreach button.
 */
function renderDataConfidenceBadge(company) {
  const conf = company?.match_confidence;
  if (conf == null) {
    return el(
      "div",
      {
        style:
          "padding:8px 12px;background:var(--bg-2);border-radius:var(--radius-sm);margin-bottom:14px;font-size:11px;color:var(--text-3);",
        title: "No confidence score yet — re-run enrichment to compute one.",
      },
      [el("span", {}, "Data confidence: "), el("span", { class: "mono" }, "—")],
    );
  }

  const pct = Math.round(conf * 100);
  let color, label, tip;
  if (conf >= 0.65) {
    color = "var(--green)";
    label = "Verified";
    tip =
      "Multiple signals confirm this company's data: channels filled, crypto-relevance evidence in description, and (if AI-analyzed) verdict was qualified or marginal.";
  } else if (conf >= 0.3) {
    color = "var(--yellow)";
    label = "Partial";
    tip =
      "Some channels are filled but missing strong corroborating signals. Verify before outreach.";
  } else {
    color = "var(--red)";
    label = "Low";
    tip =
      "Few channels filled OR negative signals (e.g., description has no crypto keywords — likely a name-collision wrong match). Re-enrich or verify manually.";
  }

  // Locally compute the same signals the server uses, for the tooltip
  // breakdown. This avoids a round-trip; the few-bytes shipped with
  // each row are enough.
  const breakdownLines = [];
  if (company.website) breakdownLines.push("Website");
  if (company.twitter_handle) breakdownLines.push("Twitter");
  if (company.linkedin_url) breakdownLines.push("LinkedIn");
  if (company.telegram_group_url) breakdownLines.push("Telegram");
  if (company.discord_url) breakdownLines.push("Discord");
  if (company.github_url) breakdownLines.push("GitHub");
  if (company.ai_lead_analysis_at) breakdownLines.push("AI analyzed");
  const breakdown = breakdownLines.length > 0 ? breakdownLines.join("\n") : "(no channels filled)";
  const fullTip = `${tip}\n\nSignals contributing:\n${breakdown}`;

  return el(
    "div",
    {
      title: fullTip,
      style: `padding:10px 12px;background:var(--bg-2);border-radius:var(--radius-sm);margin-bottom:14px;
            border-left:3px solid ${color};display:flex;align-items:center;gap:10px;`,
    },
    [
      el("div", { style: "flex:1;" }, [
        el(
          "div",
          {
            style:
              "font-size:10px;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-3);",
          },
          "Data confidence",
        ),
        el(
          "div",
          { style: `font-size:14px;font-weight:700;color:${color};margin-top:2px;` },
          `${pct}% · ${label}`,
        ),
      ]),
      // Mini bar
      el(
        "div",
        {
          style: "width:80px;height:6px;background:var(--bg-3);border-radius:3px;overflow:hidden;",
        },
        [
          el("div", {
            style: `width:${pct}%;height:100%;background:${color};transition:width 200ms ease;`,
          }),
        ],
      ),
    ],
  );
}

function renderCompanyStatusCell(c) {
  const hasWebsite = !!(c.website && String(c.website).trim());
  const hasLi = !!(c.linkedin_url && String(c.linkedin_url).trim());
  const hasTwitter = !!(c.twitter_handle && String(c.twitter_handle).trim());
  const hasTelegram = !!(c.telegram_group_url && String(c.telegram_group_url).trim());
  const hasDiscord = !!(c.discord_url && String(c.discord_url).trim());
  const hasGithub = !!(c.github_url && String(c.github_url).trim());
  const aiAt = c.ai_lead_analysis_at;
  const aiAnalyzed = !!aiAt;
  const tip = (label, val) => (val ? `${label}: ${val}` : `${label}: missing`);

  // Data confidence (v67/v68) — holistic 0-1 score aggregating channel
  // completeness, AI lead-analysis verdict, and crypto-relevance evidence.
  // Computed by recomputeMatchConfidence at the end of every bulk enrich.
  // Bands match the drawer's prominent badge: ≥0.65 verified (green),
  // ≥0.30 partial (yellow), <0.30 low (red). NULL = no score yet,
  // typically legacy rows that haven't been re-enriched since v68.
  const conf = c.match_confidence;
  let confDot = null;
  if (conf != null) {
    if (conf >= 0.65) {
      confDot = el(
        "span",
        {
          title: `Verified (${(conf * 100).toFixed(0)}%) — multiple signals corroborate this company's data.`,
          style:
            "display:inline-block;margin-right:3px;font-size:11px;line-height:1;color:var(--green);font-weight:700;",
        },
        "✓",
      );
    } else if (conf >= 0.3) {
      confDot = el(
        "span",
        {
          title: `Partial (${(conf * 100).toFixed(0)}%) — some channels filled, weaker corroboration. Verify before outreach.`,
          style:
            "display:inline-block;margin-right:3px;font-size:13px;line-height:1;color:var(--yellow);font-weight:700;",
        },
        "?",
      );
    } else {
      confDot = el(
        "span",
        {
          title: `Low (${(conf * 100).toFixed(0)}%) — likely a wrong match (e.g., name collision with a non-crypto company). Re-enrich or clear and retry.`,
          style:
            "display:inline-block;margin-right:3px;font-size:13px;line-height:1;color:var(--red);font-weight:700;",
        },
        "⚠",
      );
    }
  }

  return el("td", { style: "white-space:nowrap;width:175px;" }, [
    confDot,
    _statusDot("🌐", hasWebsite, tip("website", c.website)),
    _statusDot("💼", hasLi, tip("linkedin", c.linkedin_url)),
    _statusDot("🐦", hasTwitter, tip("twitter", c.twitter_handle ? `@${c.twitter_handle}` : null)),
    _statusDot("💬", hasTelegram, tip("telegram", c.telegram_group_url)),
    _statusDot("🎮", hasDiscord, tip("discord", c.discord_url)),
    _statusDot("🐙", hasGithub, tip("github", c.github_url)),
    _statusDot(
      "🤖",
      aiAnalyzed,
      aiAnalyzed ? `AI analyzed ${fmtDate(aiAt)}` : "Not yet AI-analyzed",
    ),
  ]);
}

// ── CRM bulk selection (v63) ─────────────────────────────────────
// Set-based per-tab selection, with helpers + a floating selection bar.
// State lives on dashboardState.crmSelection so it survives loadCrm()
// re-renders within a tab. crmSelClear() is called on tab switch.

function crmSelToggle(type, id) {
  const sel = dashboardState.crmSelection;
  // First click on a different tab type wipes the prior selection — we
  // never mix companies + contacts in one bulk action.
  if (sel.type !== type) {
    sel.type = type;
    sel.ids = new Set();
  }
  if (sel.ids.has(id)) sel.ids.delete(id);
  else sel.ids.add(id);
  renderCrmSelectionBar();
  // Re-paint the row's own checkbox without a full re-render.
  const cb = document.querySelector(`.row-checkbox[data-row-type="${type}"][data-row-id="${id}"]`);
  if (cb) cb.checked = sel.ids.has(id);
}

function crmSelClear() {
  dashboardState.crmSelection.type = null;
  dashboardState.crmSelection.ids = new Set();
  renderCrmSelectionBar();
}

function crmSelHas(type, id) {
  const sel = dashboardState.crmSelection;
  return sel.type === type && sel.ids.has(id);
}

function crmSelToggleAll(type, idsOnPage, headerCb) {
  const sel = dashboardState.crmSelection;
  if (sel.type !== type) {
    sel.type = type;
    sel.ids = new Set();
  }
  // If every visible id is already selected, clicking again deselects them.
  // Otherwise add them all.
  const allSelected = idsOnPage.every((id) => sel.ids.has(id));
  if (allSelected) {
    for (const id of idsOnPage) sel.ids.delete(id);
  } else {
    for (const id of idsOnPage) sel.ids.add(id);
  }
  // Sync header checkbox visual + per-row checkboxes
  if (headerCb) headerCb.checked = !allSelected;
  for (const id of idsOnPage) {
    const cb = document.querySelector(
      `.row-checkbox[data-row-type="${type}"][data-row-id="${id}"]`,
    );
    if (cb) cb.checked = sel.ids.has(id);
  }
  renderCrmSelectionBar();
}

/**
 * Render (or remove) the floating selection bar at the bottom of the page.
 * Idempotent — replaces any existing bar.
 */
function renderCrmSelectionBar() {
  const existing = document.getElementById("crm-selection-bar");
  if (existing) existing.remove();

  const sel = dashboardState.crmSelection;
  if (!sel.type || sel.ids.size === 0) return;

  const ids = [...sel.ids];
  const isCompanies = sel.type === "companies";
  const noun = isCompanies ? "companies" : "contacts";

  const bar = el("div", {
    id: "crm-selection-bar",
    style: `
      position: fixed;
      left: 50%;
      bottom: 18px;
      transform: translateX(-50%);
      z-index: 240;
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 10px 16px;
      background: var(--bg-1);
      border: 1px solid var(--border);
      border-left: 3px solid var(--accent);
      border-radius: var(--radius-md);
      box-shadow: 0 12px 32px rgba(0,0,0,0.45);
      font-size: 13px;
      animation: slideIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
    `,
  });

  bar.appendChild(
    el(
      "span",
      { style: "font-weight:600;color:var(--text-1);" },
      `${ids.length} ${ids.length === 1 ? noun.slice(0, -1) : noun} selected`,
    ),
  );

  const btn = (icon, label, color, onClick) =>
    el(
      "button",
      {
        class: "btn btn-tiny btn-secondary",
        type: "button",
        style: `padding:6px 10px; font-size:12px; ${color ? `border-color:${color};color:${color};` : ""}`,
        onclick: onClick,
      },
      `${icon} ${label}`,
    );

  bar.appendChild(btn("✨", "Enrich", "", () => runBulkEnrichFromSelection()));
  bar.appendChild(btn("💌", "Run campaign", "", () => runBulkCampaignFromSelection()));
  if (isCompanies) {
    // Grow is company-only — it acts on company.twitter_handle / linkedin_url.
    bar.appendChild(btn("📈", "Grow", "", () => runBulkGrowFromSelection()));
  }
  // Analyze button: Shift+click skips the enrich phase (for users who already
  // enriched manually). Tooltip explains this on the button.
  const analyzeBtn = btn("🧠", "Analyze", "", (ev) =>
    runBulkAnalyzeFromSelection({ skipEnrich: ev.shiftKey === true }),
  );
  analyzeBtn.title = "Run AI analysis (auto-enriches first). Hold Shift to skip enrichment.";
  bar.appendChild(analyzeBtn);

  // Close button
  bar.appendChild(
    el(
      "button",
      {
        class: "icon-btn",
        style: "margin-left:8px; opacity:0.7;",
        title: "Clear selection",
        onclick: () => crmSelClear(),
      },
      "×",
    ),
  );

  document.body.appendChild(bar);
}

/** Render a checkbox cell for a row. */
function selectionCheckbox(type, id) {
  return el(
    "td",
    {
      style: "width: 32px; padding-right: 0;",
      onclick: (e) => e.stopPropagation(),
    },
    [
      el("input", {
        type: "checkbox",
        class: "row-checkbox",
        "data-row-type": type,
        "data-row-id": String(id),
        checked: crmSelHas(type, id) ? "checked" : null,
        onclick: (e) => {
          e.stopPropagation();
          crmSelToggle(type, id);
        },
      }),
    ],
  );
}

/** Header "select all visible" checkbox. */
function selectionHeaderCheckbox(type, idsOnPage) {
  const sel = dashboardState.crmSelection;
  const allSelected =
    sel.type === type && idsOnPage.length > 0 && idsOnPage.every((id) => sel.ids.has(id));
  return el("th", { style: "width: 32px; padding-right: 0;" }, [
    el("input", {
      type: "checkbox",
      class: "row-checkbox-header",
      checked: allSelected ? "checked" : null,
      onclick: (e) => {
        e.stopPropagation();
        crmSelToggleAll(type, idsOnPage, e.currentTarget);
      },
    }),
  ]);
}

// ── Bulk analyze (selection-bar wrapper around existing runner) ──
async function runBulkAnalyzeFromSelection(opts = {}) {
  const sel = dashboardState.crmSelection;
  if (!sel.type || sel.ids.size === 0) return;
  const ids = [...sel.ids];
  const type = sel.type === "companies" ? "company" : "contact";
  await runBulkLeadAnalysis(ids, { type, skipEnrich: !!opts.skipEnrich });
}

// ── Bulk campaign (Step 14) and Bulk grow (Step 18) — stubs. The wiring
// for these comes in Batch 2; for now they show a clear "in progress" toast
// so the buttons in the selection bar aren't dead-clicks.

async function runBulkCampaignFromSelection() {
  const sel = dashboardState.crmSelection;
  if (!sel.type || sel.ids.size === 0) {
    toast("Select contacts or companies first", "err");
    return;
  }
  if (sel.type === "companies") {
    // Bulk-campaign on companies doesn't make sense — they're not message
    // recipients. Hint the user to select contacts instead.
    toast("Switch to the Contacts tab and select contacts to run a campaign", "err", 4000);
    return;
  }
  openBulkCampaignModal([...sel.ids]);
}

// ── Hiring Outreach — LinkedIn comment review queue (crypto_hiring_outreach) ──
//
// AI-drafted LinkedIn comments (company page + director's latest post) wait HERE
// for the operator's approval before they post publicly. Mirrors the event-
// campaign approval drawer: sub-tab by status, editable comment per draft, with
// the job + target-post context shown so the operator sees WHY each was picked.
// The like on the director's post already fired in the engine (it's low-risk),
// so this queue is comments only.
async function showHiringOutreachDrawer() {
  let activeStatus = "drafted";

  async function refresh() {
    const data = await api("lic-list", {
      queryString: `?status=${encodeURIComponent(activeStatus)}&limit=100`,
    });
    if (data?.error) {
      toast(`Error: ${data.error}`, "err");
      return;
    }
    renderBody(data);
  }

  function renderBody(data) {
    const body = $("#drawer-body");
    if (!body) return;
    body.innerHTML = "";
    const counts = data.counts || {};
    const drafts = data.drafts || [];

    body.appendChild(
      el(
        "div",
        { class: "muted small", style: "line-height:1.6;margin-bottom:12px;" },
        "AI-drafted LinkedIn comments for companies hiring in crypto. Nothing posts until you approve it. (The like on each director's post already fired.)",
      ),
    );

    // Status pills
    const countsRow = el("div", {
      class: "row",
      style: "gap:6px;flex-wrap:wrap;margin-bottom:12px;font-size:11px;",
    });
    const PILL = (label, n, color) =>
      el(
        "span",
        {
          style: `padding:3px 8px;background:var(--bg-2);border:1px solid ${color}33;color:${color};border-radius:var(--radius-full);font-weight:600;`,
        },
        `${label} ${n || 0}`,
      );
    countsRow.appendChild(PILL("drafted", counts.drafted, "var(--accent)"));
    countsRow.appendChild(PILL("approved", counts.approved, "var(--blue)"));
    countsRow.appendChild(PILL("sent", counts.sent, "var(--green)"));
    if (counts.cooled) countsRow.appendChild(PILL("cooled", counts.cooled, "var(--yellow)"));
    if (counts.skipped) countsRow.appendChild(PILL("skipped", counts.skipped, "var(--text-4)"));
    if (counts.failed) countsRow.appendChild(PILL("failed", counts.failed, "var(--red)"));
    body.appendChild(countsRow);

    // Status sub-tabs
    const subtabs = el("div", { class: "tabs", style: "margin-bottom:8px;font-size:11px;" });
    for (const s of ["drafted", "approved", "sent", "cooled", "skipped", "failed"]) {
      subtabs.appendChild(
        el(
          "button",
          {
            class: `tab${activeStatus === s ? " active" : ""}`,
            type: "button",
            onclick: () => {
              activeStatus = s;
              refresh();
            },
          },
          `${s} ${counts[s] || 0}`,
        ),
      );
    }
    body.appendChild(subtabs);

    if (drafts.length === 0) {
      body.appendChild(
        el(
          "div",
          { class: "muted small", style: "padding:12px;text-align:center;" },
          activeStatus === "drafted"
            ? "No drafts yet. Once the Hiring Outreach automation runs, AI-drafted comments appear here for review."
            : `No ${activeStatus} drafts.`,
        ),
      );
      return;
    }

    if (activeStatus === "drafted") {
      body.appendChild(
        el(
          "button",
          {
            class: "btn btn-tiny btn-primary",
            type: "button",
            style: "margin-bottom:10px;",
            onclick: async () => {
              const ids = drafts.map((d) => d.id);
              const r = await api("lic-approve", { body: { ids } });
              if (r?.ok) {
                toast(`Approved ${r.updated} comments`, "ok");
                refresh();
              }
            },
          },
          `Approve all ${drafts.length} visible`,
        ),
      );
    }

    for (const row of drafts) {
      const kindIcon = row.kind === "company_page" ? "🏢" : "👤";
      const kindLabel =
        row.kind === "company_page" ? "Company page" : row.person_name || "Director";
      const card = el("div", {
        style:
          "border:1px solid var(--border);background:var(--bg-2);border-radius:var(--radius-md);padding:10px 12px;margin-bottom:8px;",
      });
      // Header: company + kind
      card.appendChild(
        el(
          "div",
          { class: "row", style: "gap:6px;align-items:center;margin-bottom:4px;font-size:12px;" },
          [
            el("span", { style: "font-size:14px;" }, kindIcon),
            el("span", { style: "font-weight:700;" }, row.company_name || "(company)"),
            el("span", { class: "muted small" }, `· ${kindLabel}`),
            row.company_linkedin_url || row.person_linkedin_url
              ? el(
                  "a",
                  {
                    href:
                      row.kind === "company_page"
                        ? row.company_linkedin_url
                        : row.person_linkedin_url,
                    target: "_blank",
                    style: "margin-left:auto;color:var(--accent);font-size:11px;",
                  },
                  "open ↗",
                )
              : null,
          ],
        ),
      );
      // Why: the hiring role + relevance reasoning
      if (row.job_title || row.relevance_reasoning) {
        card.appendChild(
          el(
            "div",
            { class: "muted small", style: "margin-bottom:6px;line-height:1.5;" },
            [
              row.job_title
                ? el("div", {}, [
                    "Hiring: ",
                    row.job_url
                      ? el(
                          "a",
                          { href: row.job_url, target: "_blank", style: "color:var(--accent);" },
                          row.job_title,
                        )
                      : el("span", {}, row.job_title),
                    row.role_service
                      ? el(
                          "span",
                          { class: "mono", style: "margin-left:6px;color:var(--text-3);" },
                          `→ ${row.role_service}`,
                        )
                      : null,
                  ])
                : null,
              row.relevance_reasoning
                ? el("div", { style: "font-style:italic;" }, `"${row.relevance_reasoning}"`)
                : null,
            ].filter(Boolean),
          ),
        );
      }
      // The post we're commenting on (context)
      if (row.target_post_text) {
        card.appendChild(
          el(
            "div",
            {
              style:
                "background:var(--bg-1);border-left:2px solid var(--border);padding:6px 8px;margin-bottom:6px;font-size:11px;color:var(--text-2);line-height:1.5;max-height:64px;overflow:hidden;",
            },
            `"${String(row.target_post_text).slice(0, 220)}${row.target_post_text.length > 220 ? "…" : ""}"`,
          ),
        );
      }
      // Editable AI comment
      const ta = el(
        "textarea",
        {
          class: "wizard-textarea",
          rows: "2",
          style: "width:100%;font-size:12px;line-height:1.5;",
          onblur: async (e) => {
            const t = e.target.value.trim();
            if (t === row.ai_drafted_comment) return;
            const r = await api("lic-edit", { body: { id: row.id, comment: t } });
            if (r?.ok) {
              toast("Draft saved", "ok", 1200);
              row.ai_drafted_comment = t;
            } else toast(r?.error || "save failed", "err");
          },
        },
        row.ai_drafted_comment || "",
      );
      card.appendChild(ta);

      if (row.last_error) {
        card.appendChild(
          el(
            "div",
            {
              class: "muted small",
              style: "color:var(--red);font-family:var(--font-mono);font-size:10px;margin-top:4px;",
            },
            row.last_error,
          ),
        );
      }

      if (row.status === "drafted") {
        card.appendChild(
          el("div", { class: "row", style: "gap:6px;margin-top:6px;" }, [
            el(
              "button",
              {
                class: "btn btn-tiny btn-primary",
                type: "button",
                onclick: async () => {
                  const r = await api("lic-approve", { body: { ids: [row.id] } });
                  if (r?.ok) {
                    toast("Approved", "ok", 1500);
                    refresh();
                  }
                },
              },
              "Approve",
            ),
            el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                onclick: async () => {
                  const r = await api("lic-skip", { body: { ids: [row.id] } });
                  if (r?.ok) {
                    toast("Skipped", "ok", 1500);
                    refresh();
                  }
                },
              },
              "Skip",
            ),
          ]),
        );
      }

      const meta = [];
      if (row.like_status) meta.push(`like: ${row.like_status}`);
      if (row.approved_at) meta.push(`approved ${fmtDate(row.approved_at)}`);
      if (row.sent_at) meta.push(`sent ${fmtDate(row.sent_at)}`);
      if (meta.length)
        card.appendChild(
          el(
            "div",
            { class: "muted small mono", style: "font-size:10px;margin-top:4px;" },
            meta.join(" · "),
          ),
        );

      body.appendChild(card);
    }
  }

  openDrawer("Hiring Outreach — comment review", el("div", { class: "muted small" }, "Loading…"));
  refresh();
}

// ── Activity feed (CRM tab) ──────────────────────────────────────
async function renderActivityCardInCrm() {
  const wrap = $("#crm-list-wrap");
  if (!wrap) return;
  wrap.innerHTML =
    '<div class="skeleton skeleton-row"></div><div class="skeleton skeleton-row"></div>';

  const data = await apiCached("crm-activity").catch(() => ({}));
  const events = data?.recent || [];

  wrap.innerHTML = "";
  if (events.length === 0) {
    wrap.innerHTML =
      '<div class="empty-state"><div class="empty-state-icon">\ud83d\udcac</div><div class="empty-state-title">No recent activity</div><div class="empty-state-hint">Outreach events will appear here as they happen</div></div>';
    return;
  }

  const platformIcons = {
    x: "\ud835\udd4f",
    linkedin: "\ud83d\udd17",
    telegram: "\u2708\ufe0f",
    email: "\u2709\ufe0f",
  };
  const list = el("div", { style: "padding: 4px 0;" });
  for (const ev of events) {
    const icon = platformIcons[ev.platform] || "\u2022";
    const contactLink = ev.contactName
      ? el(
          "span",
          {
            class: "activity-contact",
            onclick: (e) => {
              e.stopPropagation();
              if (ev.contactId) showContactDrawer(ev.contactId);
            },
          },
          ev.contactName,
        )
      : null;

    list.appendChild(
      el("div", { class: "activity-item" }, [
        el("div", { class: "activity-icon" }, icon),
        el("div", { class: "activity-body" }, [
          el("div", { class: "activity-text" }, [
            document.createTextNode(ev.text || ev.platform || ""),
            contactLink ? document.createTextNode(" \u2014 ") : null,
            contactLink,
          ]),
          el("div", { class: "activity-time" }, ev.time || ""),
        ]),
        ev.success === false
          ? el("span", { class: "badge badge-red", style: "align-self: center;" }, "failed")
          : null,
      ]),
    );
  }
  wrap.appendChild(list);
}

// ── DM Campaign wizard ──────────────────────────────────────────

// ── Email Campaign wizard ───────────────────────────────────────

let emailWizard = {
  name: "",
  subject: "",
  body: "",
  targetSource: "all_emails",
  manualRecipients: "",
  tag: "",
  dailyLimit: 50,
  accountId: "",
};

async function renderNewEmailCampaign() {
  const tagsData = dashboardState.cache.crmTags || [];

  const content = el("div", { class: "wizard" }, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el(
          "button",
          { class: "btn btn-secondary btn-tiny", onclick: () => navigate("operations") },
          "\u00ab Back",
        ),
        el("div", { class: "page-title", style: "margin-top: 10px;" }, "New Email Campaign"),
      ]),
    ]),
    el("div", { class: "wizard-section" }, [
      el("div", { class: "wizard-section-title" }, "1. Email content"),
      el("div", { class: "wizard-label" }, "Campaign name"),
      el("input", {
        class: "wizard-input",
        placeholder: "e.g. April newsletter",
        value: emailWizard.name,
        oninput: (e) => {
          emailWizard.name = e.target.value;
        },
      }),
      el("div", { class: "wizard-label", style: "margin-top: 12px;" }, "Subject line"),
      el("input", {
        class: "wizard-input",
        placeholder: "Your subject line here",
        value: emailWizard.subject,
        oninput: (e) => {
          emailWizard.subject = e.target.value;
        },
      }),
      el("div", { class: "wizard-label", style: "margin-top: 12px;" }, "Email body"),
      el(
        "div",
        { class: "muted small", style: "margin-bottom: 4px;" },
        "Variables: {name}, {first_name}, {company}, {role}, {email}",
      ),
      el(
        "textarea",
        {
          class: "wizard-textarea",
          style: "min-height: 200px;",
          placeholder: "Hi {first_name},\n\nI wanted to reach out about...",
          oninput: (e) => {
            emailWizard.body = e.target.value;
          },
        },
        emailWizard.body,
      ),
      el("div", { class: "wizard-label", style: "margin-top: 12px;" }, "Daily limit"),
      el("input", {
        class: "wizard-input",
        type: "number",
        value: String(emailWizard.dailyLimit),
        style: "width: 100px;",
        oninput: (e) => {
          emailWizard.dailyLimit = parseInt(e.target.value) || 50;
        },
      }),
    ]),
    el("div", { class: "wizard-section" }, [
      el("div", { class: "wizard-section-title" }, "2. Recipients"),
      el("div", { class: "row", style: "gap: 8px; margin-bottom: 12px; flex-wrap: wrap;" }, [
        radioBtn(
          "all_emails",
          "All contacts with email",
          emailWizard.targetSource === "all_emails",
          () => {
            emailWizard.targetSource = "all_emails";
            renderNewEmailCampaign();
          },
        ),
        radioBtn("tag", "By tag", emailWizard.targetSource.startsWith("tag"), () => {
          emailWizard.targetSource = "tag:" + (emailWizard.tag || "");
          renderNewEmailCampaign();
        }),
        radioBtn("manual", "Manual emails", emailWizard.targetSource === "manual", () => {
          emailWizard.targetSource = "manual";
          renderNewEmailCampaign();
        }),
      ]),
      emailWizard.targetSource === "manual"
        ? el(
            "textarea",
            {
              class: "wizard-textarea",
              placeholder: "email1@example.com, email2@example.com",
              oninput: (e) => {
                emailWizard.manualRecipients = e.target.value;
              },
            },
            emailWizard.manualRecipients,
          )
        : emailWizard.targetSource.startsWith("tag")
          ? el("input", {
              class: "wizard-input",
              placeholder: "Enter tag name",
              value: emailWizard.tag,
              oninput: (e) => {
                emailWizard.tag = e.target.value;
                emailWizard.targetSource = "tag:" + e.target.value;
              },
            })
          : el(
              "div",
              { class: "muted small" },
              "Will send to all CRM contacts that have an email address.",
            ),
    ]),
    // Section 3: Sending account
    (() => {
      const accts = dashboardState.cache.accounts?.smtp || [];
      if (accts.length === 0)
        return el("div", { class: "wizard-section" }, [
          el("div", { class: "wizard-section-title" }, "3. Sending account"),
          el("div", { class: "muted small" }, "No SMTP accounts configured. Add one in Accounts."),
          el(
            "button",
            {
              class: "btn btn-secondary btn-tiny",
              style: "margin-top: 6px;",
              type: "button",
              onclick: () => navigate("accounts"),
            },
            "Go to Accounts",
          ),
        ]);
      return el("div", { class: "wizard-section" }, [
        el("div", { class: "wizard-section-title" }, "3. Sending account"),
        el(
          "select",
          {
            class: "wizard-input",
            onchange: (e) => {
              emailWizard.accountId = e.target.value;
            },
          },
          [
            el("option", { value: "" }, "Auto-select best account"),
            ...accts.map((a) => {
              const o = el("option", { value: String(a.id) }, `${a.email} (${a.smtp_host})`);
              if (String(a.id) === emailWizard.accountId) o.selected = true;
              return o;
            }),
          ],
        ),
      ]);
    })(),
    el("div", { class: "wizard-actions" }, [
      el(
        "button",
        { class: "btn btn-primary", onclick: submitEmailCampaign },
        "\ud83d\ude80 Create & Start",
      ),
      el(
        "button",
        { class: "btn btn-secondary", onclick: () => submitEmailCampaign(false) },
        "Save as Draft",
      ),
      el("button", { class: "btn btn-secondary", onclick: () => navigate("campaigns") }, "Cancel"),
    ]),
  ]);
  setContentNode(content);
}

async function submitEmailCampaign(startImmediately = true) {
  if (!emailWizard.name || !emailWizard.subject || !emailWizard.body) {
    toast("Name, subject, and body are required", "err");
    return;
  }
  const body = {
    name: emailWizard.name,
    subject: emailWizard.subject,
    htmlBody: emailWizard.body,
    targetSource: emailWizard.targetSource,
    dailyLimit: emailWizard.dailyLimit,
    startImmediately,
    ...(emailWizard.accountId && { accountId: emailWizard.accountId }),
  };
  if (emailWizard.targetSource === "manual" && emailWizard.manualRecipients) {
    body.recipients = emailWizard.manualRecipients
      .split(",")
      .map((e) => e.trim())
      .filter(Boolean)
      .map((e) => ({ email: e, name: e.split("@")[0] }));
  }
  toast("Creating email campaign\u2026", "info");
  const result = await api("campaign-email-create", { body });
  if (result?.ok) {
    toast(
      `\u2705 Email campaign #${result.campaignId} created (${result.recipientCount} recipients, ${result.status})`,
      "ok",
      6000,
    );
    emailWizard = {
      name: "",
      subject: "",
      body: "",
      targetSource: "all_emails",
      manualRecipients: "",
      tag: "",
      dailyLimit: 50,
      accountId: "",
    };
    navigate("campaigns");
  } else {
    toast(`\u274c ${result?.error || "creation failed"}`, "err");
  }
}

// ── X / Twitter dashboard ────────────────────────────────────────

async function twitterCmd(type, params = {}) {
  const { token, apiBase } = await chrome.storage.local.get(["token", "apiBase"]);
  if (!token) return { error: "Not authenticated" };
  const base = (apiBase || "http://localhost:9091").replace(/\/$/, "");
  const res = await fetch(`${base}/api/twitter/command`, {
    method: "POST",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({ type, ...params }),
    signal: AbortSignal.timeout(30000),
  });
  return res.json();
}

let xPostText = "";

async function renderXDashboard() {
  showLoading("Loading X accounts\u2026");
  const accountsData = await api("accounts-all").catch(() => ({}));
  const xAccounts = (accountsData?.twitter || []).filter((a) => a.status === "active");

  if (xAccounts.length === 0) {
    setContent(
      `<div class="empty-state"><div class="empty-state-icon">\ud83d\udc26</div><div class="empty-state-title">No X accounts connected</div><div class="empty-state-hint">Visit x.com in Chrome while logged in. The Nexus extension auto-syncs your session.</div></div>`,
    );
    return;
  }

  // Fetch stats for first active account
  let stats = null;
  const primaryAccount = xAccounts[0];
  const username = primaryAccount.account_identifier || primaryAccount.display_name;
  try {
    stats = await twitterCmd("twitter:daily-stats", { username });
  } catch {}

  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "X / Twitter"),
        el(
          "div",
          { class: "page-subtitle" },
          `${xAccounts.length} account${xAccounts.length > 1 ? "s" : ""} connected`,
        ),
      ]),
    ]),

    // Accounts overview
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Connected accounts"),
      el(
        "div",
        {},
        xAccounts.map((a) =>
          el("div", { class: "channel-row" }, [
            el("div", { class: "channel-icon" }, "\u2705"),
            el(
              "div",
              { class: "channel-name truncate" },
              `@${a.account_identifier || a.display_name || "?"}`,
            ),
            el("div", { class: "channel-bar" }, [
              el("div", {
                class: `channel-bar-fill ${(a.health_score || 0) >= 70 ? "healthy" : "degraded"}`,
                style: `width: ${a.health_score || 0}%`,
              }),
            ]),
            el(
              "div",
              { class: "channel-detail" },
              `day ${a.warmup_day || "?"} \u00b7 ${a.outreach_sent_today || 0}/${a.max_daily_outreach || 0} today`,
            ),
          ]),
        ),
      ),
    ]),

    // Post a tweet
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Post a tweet"),
      el(
        "textarea",
        {
          class: "wizard-textarea",
          placeholder: "What's happening?",
          id: "x-post-text",
          oninput: (e) => {
            xPostText = e.target.value;
            const counter = $("#x-char-counter");
            if (counter) {
              counter.textContent = `${e.target.value.length}/280`;
              counter.classList.toggle("over", e.target.value.length > 280);
            }
          },
        },
        xPostText,
      ),
      el("div", { class: "row", style: "justify-content: space-between; margin-top: 8px;" }, [
        el(
          "div",
          { class: `char-counter${xPostText.length > 280 ? " over" : ""}`, id: "x-char-counter" },
          `${xPostText.length}/280`,
        ),
        el("div", { class: "row", style: "gap: 8px;" }, [
          el(
            "select",
            { class: "filter-select", id: "x-post-account" },
            xAccounts.map((a) =>
              el(
                "option",
                { value: a.account_identifier || a.display_name },
                `@${a.account_identifier || a.display_name}`,
              ),
            ),
          ),
          el(
            "button",
            {
              class: "btn btn-primary",
              onclick: async () => {
                const text = xPostText.trim();
                if (!text) {
                  toast("Write something first", "err");
                  return;
                }
                if (text.length > 280) {
                  toast("Tweet too long", "err");
                  return;
                }
                const acct = $("#x-post-account")?.value;
                toast("Posting\u2026", "info");
                const result = await twitterCmd("twitter:http-post", { username: acct, text });
                if (result?.success || result?.tweetId) {
                  toast(`\u2705 Posted!${result.tweetId ? ` ID: ${result.tweetId}` : ""}`, "ok");
                  xPostText = "";
                  const ta = $("#x-post-text");
                  if (ta) ta.value = "";
                } else {
                  toast(`\u274c ${result?.error || "Post failed"}`, "err");
                }
              },
            },
            "\ud83d\udc26 Post",
          ),
        ]),
      ]),
    ]),

    // Quick actions
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Quick actions"),
      el(
        "div",
        {
          style:
            "display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px;",
        },
        [
          xActionCard("Like feed posts", "Like recent posts from your home timeline", async () => {
            const acct = xAccounts[0]?.account_identifier;
            toast("Engaging feed\u2026", "info", 15000);
            const r = await twitterCmd("twitter:http-engage-feed", { username: acct });
            toast(r?.error ? `\u274c ${r.error}` : "\u2705 Feed engaged", r?.error ? "err" : "ok");
          }),
          xActionCard("Follow by username", "Enter a handle to follow", async () => {
            const handle = prompt("Username to follow (without @):");
            if (!handle) return;
            const acct = xAccounts[0]?.account_identifier;
            toast(`Following @${handle}\u2026`, "info");
            const r = await twitterCmd("twitter:http-follow-by-username", {
              username: acct,
              targetUsername: handle.replace(/^@/, ""),
            });
            toast(
              r?.success !== false
                ? `\u2705 Followed @${handle}`
                : `\u274c ${r?.error || "Failed"}`,
              r?.success !== false ? "ok" : "err",
            );
          }),
          xActionCard("View daily stats", "Today's engagement numbers", async () => {
            const acct = xAccounts[0]?.account_identifier;
            toast("Loading stats\u2026", "info");
            const r = await twitterCmd("twitter:daily-stats", { username: acct });
            if (r) {
              const entries = Object.entries(r).slice(0, 8);
              for (const [k, v] of entries)
                toast(`${k}: ${JSON.stringify(v).slice(0, 50)}`, "info", 6000);
            }
          }),
          xActionCard("Engage hashtag", "Like + reply to posts under a hashtag", async () => {
            const hashtag = prompt("Hashtag (without #):");
            if (!hashtag) return;
            const acct = xAccounts[0]?.account_identifier;
            toast(`Engaging #${hashtag}\u2026`, "info", 20000);
            const r = await twitterCmd("twitter:http-engage-hashtag", {
              username: acct,
              hashtag: hashtag.replace(/^#/, ""),
            });
            toast(
              r?.error ? `\u274c ${r.error}` : `\u2705 Engaged #${hashtag}`,
              r?.error ? "err" : "ok",
            );
          }),
        ],
      ),
    ]),

    // Stats (if available)
    stats
      ? el("div", { class: "card" }, [
          el("div", { class: "card-title" }, `Today's stats (@${username})`),
          el(
            "div",
            {},
            Object.entries(stats)
              .slice(0, 10)
              .map(([k, v]) =>
                el("div", { class: "field-row" }, [
                  el("div", { class: "field-label" }, k),
                  el(
                    "div",
                    { class: "field-value mono" },
                    typeof v === "object" ? JSON.stringify(v).slice(0, 60) : String(v),
                  ),
                ]),
              ),
          ),
        ])
      : null,
  ]);
  setContentNode(content);
}

function xActionCard(title, desc, onclick) {
  return el(
    "div",
    {
      class: "card",
      style: "padding: 12px; cursor: pointer; transition: border-color 120ms;",
      onclick,
      onmouseenter: (e) => {
        e.currentTarget.style.borderColor = "var(--accent)";
      },
      onmouseleave: (e) => {
        e.currentTarget.style.borderColor = "var(--border)";
      },
    },
    [
      el("div", { class: "primary-cell", style: "font-size: 12px; margin-bottom: 4px;" }, title),
      el("div", { class: "muted small" }, desc),
    ],
  );
}

// ── Deals pipeline ──────────────────────────────────────────────

async function renderDeals() {
  showLoading("Loading deals\u2026");

  let data = {};
  try {
    data = await api("crm-deals");
    if (data?.error) {
      showError(data.error, renderDeals);
      return;
    }
  } catch (err) {
    showError(err.message, renderDeals);
    return;
  }

  const { pipeline = {}, stats = {}, deals = [] } = data;
  const stageOrder = ["prospect", "qualified", "proposal", "negotiation", "won", "lost"];
  const stageIcons = {
    prospect: "\ud83c\udf31",
    qualified: "\ud83c\udfaf",
    proposal: "\ud83d\udcdd",
    negotiation: "\ud83e\udd1d",
    won: "\u2705",
    lost: "\u274c",
  };
  const stageColors = {
    prospect: "#7c6bff",
    qualified: "#f9ab00",
    proposal: "#e8710a",
    negotiation: "#1557b0",
    won: "#0d652d",
    lost: "#d93025",
  };

  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "Deal Pipeline"),
        el(
          "div",
          { class: "page-subtitle" },
          `${stats.total_deals || 0} deals | Pipeline: $${fmtNum(stats.pipeline_value || 0)} | Won: $${fmtNum(stats.won_value || 0)}${stats.winRate ? ` | Win rate: ${stats.winRate}%` : ""}`,
        ),
      ]),
      el("div", { style: "display:flex;gap:8px;" }, [
        el(
          "button",
          { class: "btn btn-primary", onclick: () => openNewDealDrawer() },
          "\u2795 New Deal",
        ),
        el(
          "button",
          { class: "btn btn-secondary", onclick: () => navigate("operations") },
          "\u2795 New Campaign",
        ),
      ]),
    ]),

    // Pipeline funnel — real deal stages
    el(
      "div",
      { class: "funnel" },
      stageOrder.map((stage) => {
        const d = pipeline[stage] || { count: 0, total_value: 0 };
        const maxCount = Math.max(...stageOrder.map((s) => pipeline[s]?.count || 0), 1);
        return funnelStage(
          `${stageIcons[stage] || ""} ${stage}`,
          d.count,
          maxCount,
          d.total_value ? `$${fmtNum(d.total_value)}` : null,
        );
      }),
    ),

    // Deal list
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, `All Deals (${deals.length})`),
      deals.length > 0
        ? el("div", { class: "table-wrap" }, [
            el("table", {}, [
              el("thead", {}, [
                el("tr", {}, [
                  el("th", {}, "Deal"),
                  el("th", {}, "Stage"),
                  el("th", {}, "Value"),
                  el("th", {}, "Probability"),
                  el("th", {}, "Updated"),
                ]),
              ]),
              el(
                "tbody",
                {},
                deals.slice(0, 25).map((d) =>
                  el("tr", { class: d.auto_created && !d.reviewed ? "highlight-row" : "" }, [
                    el("td", {}, [
                      el("span", { class: "bold" }, esc(d.title || "Untitled")),
                      d.auto_created && !d.reviewed
                        ? el(
                            "span",
                            { class: "badge badge-warn", style: "margin-left:6px;" },
                            "\u26a1 Auto",
                          )
                        : null,
                    ]),
                    el(
                      "td",
                      {},
                      el(
                        "span",
                        {
                          class: "badge",
                          style: `background:${stageColors[d.stage] || "#5f6368"}22;color:${stageColors[d.stage] || "#5f6368"}`,
                        },
                        d.stage || "?",
                      ),
                    ),
                    el("td", {}, d.value_usd ? `$${fmtNum(d.value_usd)}` : "\u2014"),
                    el("td", {}, d.probability ? `${d.probability}%` : "\u2014"),
                    el("td", { class: "muted small" }, d.updated_at?.slice(0, 10) || ""),
                  ]),
                ),
              ),
            ]),
          ])
        : el(
            "div",
            { class: "muted" },
            "No deals yet. Deals are created from outreach replies or manually.",
          ),
    ]),

    // Quick actions
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Actions"),
      el("div", { class: "row", style: "flex-wrap: wrap; gap: 8px;" }, [
        el(
          "button",
          { class: "btn btn-secondary", onclick: () => navigate("campaigns") },
          "\ud83d\udce3 Campaigns",
        ),
        el(
          "button",
          { class: "btn btn-secondary", onclick: () => navigate("signals") },
          "\ud83d\udd25 Signals",
        ),
        el(
          "button",
          { class: "btn btn-secondary", onclick: () => navigate("vc") },
          "\ud83c\udfdb VC Outreach",
        ),
        el(
          "button",
          { class: "btn btn-secondary", onclick: () => navigate("crm") },
          "\ud83d\uddc2 CRM",
        ),
      ]),
    ]),
  ]);
  setContentNode(content);
}

function funnelStage(label, count, total, subtitle) {
  const pct = total > 0 ? Math.round((count / total) * 100) : 0;
  return el("div", { class: "funnel-stage" }, [
    el("span", { class: "funnel-count" }, fmtNum(count)),
    el("span", { class: "funnel-label" }, label),
    subtitle ? el("span", { class: "funnel-subtitle muted small" }, subtitle) : null,
    el("div", { class: "funnel-bar" }, [
      el("div", { class: "funnel-bar-fill", style: `width: ${pct}%` }),
    ]),
  ]);
}

// ── Manual "New deal" form (drawer) ──────────────────────────────
function openNewDealDrawer() {
  const STAGES = [
    ["prospect", "Prospect"],
    ["qualified", "Qualified"],
    ["proposal", "Proposal"],
    ["negotiation", "Negotiation"],
    ["won", "Won"],
    ["lost", "Lost"],
    ["churned", "Churned"],
  ];

  const form = el("div", { style: "display:flex;flex-direction:column;gap:10px;padding:12px 0;" });

  const titleInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "Deal title (required)",
    autofocus: "autofocus",
  });
  const stageSelect = el(
    "select",
    { class: "filter-select", style: "width:100%;" },
    STAGES.map(([v, label]) => {
      const o = el("option", { value: v }, label);
      if (v === "prospect") o.selected = true;
      return o;
    }),
  );
  const valueInput = el("input", {
    class: "wizard-input",
    type: "number",
    min: "0",
    step: "1000",
    placeholder: "Deal value (USD, optional)",
  });
  const probInput = el("input", {
    class: "wizard-input",
    type: "number",
    min: "0",
    max: "100",
    step: "5",
    placeholder: "Probability % (0–100, optional)",
  });
  const closeInput = el("input", {
    class: "wizard-input",
    type: "date",
  });
  const sourceInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "Source (e.g. inbound, referral, manual)",
    value: "manual",
  });
  const companyIdInput = el("input", {
    class: "wizard-input",
    type: "number",
    min: "1",
    placeholder: "Company ID (optional)",
  });
  const contactIdInput = el("input", {
    class: "wizard-input",
    type: "number",
    min: "1",
    placeholder: "Contact ID (optional)",
  });
  const notesInput = el("textarea", {
    class: "wizard-textarea",
    rows: "3",
    placeholder: "Notes (optional)",
  });

  const labeled = (label, inputEl, hint) =>
    el("div", {}, [
      el("label", { class: "wizard-label" }, label),
      inputEl,
      hint ? el("div", { class: "muted small", style: "margin-top:2px;" }, hint) : null,
    ]);

  form.appendChild(labeled("Title *", titleInput));
  form.appendChild(labeled("Stage", stageSelect));
  form.appendChild(
    el("div", { style: "display:flex;gap:8px;" }, [
      el("div", { style: "flex:1;" }, [labeled("Value (USD)", valueInput)]),
      el("div", { style: "flex:1;" }, [labeled("Probability %", probInput)]),
    ]),
  );
  form.appendChild(labeled("Expected close date", closeInput));
  form.appendChild(labeled("Source", sourceInput));
  form.appendChild(
    el("div", { style: "display:flex;gap:8px;" }, [
      el("div", { style: "flex:1;" }, [
        labeled("Company ID", companyIdInput, "From CRM → Companies"),
      ]),
      el("div", { style: "flex:1;" }, [
        labeled("Contact ID", contactIdInput, "From CRM → Contacts"),
      ]),
    ]),
  );
  form.appendChild(labeled("Notes", notesInput));

  const errLine = el("div", { class: "muted small", style: "color:var(--red);min-height:14px;" });
  form.appendChild(errLine);

  const saveBtn = el("button", { class: "btn btn-primary", type: "button" }, "Create deal");
  const cancelBtn = el(
    "button",
    { class: "btn btn-secondary", type: "button", onclick: () => closeDrawer() },
    "Cancel",
  );
  form.appendChild(el("div", { class: "wizard-actions" }, [saveBtn, cancelBtn]));

  saveBtn.addEventListener("click", async () => {
    const title = titleInput.value.trim();
    if (!title) {
      errLine.textContent = "Title is required.";
      titleInput.focus();
      return;
    }
    const payload = {
      title,
      stage: stageSelect.value,
      valueUsd: valueInput.value || null,
      probability: probInput.value || null,
      expectedCloseDate: closeInput.value || null,
      source: sourceInput.value.trim() || "manual",
      notes: notesInput.value.trim() || null,
      companyId: companyIdInput.value || null,
      contactId: contactIdInput.value || null,
    };
    saveBtn.textContent = "Saving…";
    saveBtn.disabled = true;
    errLine.textContent = "";
    try {
      const r = await api("crm-deals-create", { body: payload });
      if (r?.error) {
        errLine.textContent = r.error;
        saveBtn.textContent = "Create deal";
        saveBtn.disabled = false;
        return;
      }
      toast(`Deal #${r.dealId} created`, "ok");
      closeDrawer();
      renderDeals();
    } catch (err) {
      errLine.textContent = err.message;
      saveBtn.textContent = "Create deal";
      saveBtn.disabled = false;
    }
  });

  openDrawer("New Deal", form);
  setTimeout(() => titleInput.focus(), 100);
}

// ── Manual "New contact" form (drawer) ──────────────────────────
function openNewContactDrawer() {
  const form = el("div", { style: "display:flex;flex-direction:column;gap:10px;padding:12px 0;" });
  const labeled = (label, inputEl, hint) =>
    el("div", {}, [
      el("label", { class: "wizard-label" }, label),
      inputEl,
      hint ? el("div", { class: "muted small", style: "margin-top:2px;" }, hint) : null,
    ]);

  const nameInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "Display name (required)",
    autofocus: "autofocus",
  });
  const emailInput = el("input", {
    class: "wizard-input",
    type: "email",
    placeholder: "you@company.com",
  });
  const companyInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "Company name",
  });
  const roleInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "Role / title",
  });
  const xInput = el("input", { class: "wizard-input", type: "text", placeholder: "@handle" });
  const liInput = el("input", {
    class: "wizard-input",
    type: "url",
    placeholder: "https://linkedin.com/in/…",
  });
  const tgInput = el("input", { class: "wizard-input", type: "text", placeholder: "@username" });
  const phoneInput = el("input", { class: "wizard-input", type: "tel", placeholder: "+1 555…" });
  const sectorInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "Sector / industry",
  });
  const notesInput = el("textarea", {
    class: "wizard-textarea",
    rows: "3",
    placeholder: "Notes (optional)",
  });

  form.appendChild(labeled("Name *", nameInput));
  form.appendChild(
    el("div", { style: "display:flex;gap:8px;" }, [
      el("div", { style: "flex:1;" }, [labeled("Email", emailInput)]),
      el("div", { style: "flex:1;" }, [labeled("Phone", phoneInput)]),
    ]),
  );
  form.appendChild(
    el("div", { style: "display:flex;gap:8px;" }, [
      el("div", { style: "flex:1;" }, [labeled("Company", companyInput)]),
      el("div", { style: "flex:1;" }, [labeled("Role", roleInput)]),
    ]),
  );
  form.appendChild(labeled("Sector", sectorInput));
  form.appendChild(labeled("X / Twitter handle", xInput));
  form.appendChild(labeled("LinkedIn URL", liInput));
  form.appendChild(labeled("Telegram username", tgInput));
  form.appendChild(labeled("Notes", notesInput));

  const errLine = el("div", { class: "muted small", style: "color:var(--red);min-height:14px;" });
  form.appendChild(errLine);

  const saveBtn = el("button", { class: "btn btn-primary", type: "button" }, "Create contact");
  const cancelBtn = el(
    "button",
    { class: "btn btn-secondary", type: "button", onclick: () => closeDrawer() },
    "Cancel",
  );
  form.appendChild(el("div", { class: "wizard-actions" }, [saveBtn, cancelBtn]));

  saveBtn.addEventListener("click", async () => {
    const displayName = nameInput.value.trim();
    if (!displayName) {
      errLine.textContent = "Name is required.";
      nameInput.focus();
      return;
    }
    saveBtn.textContent = "Saving…";
    saveBtn.disabled = true;
    errLine.textContent = "";
    try {
      const r = await api("crm-contacts-create", {
        body: {
          displayName,
          email: emailInput.value.trim() || null,
          phone: phoneInput.value.trim() || null,
          company: companyInput.value.trim() || null,
          role: roleInput.value.trim() || null,
          sector: sectorInput.value.trim() || null,
          twitterHandle: xInput.value.trim() || null,
          linkedinUrl: liInput.value.trim() || null,
          telegramUsername: tgInput.value.trim() || null,
          notes: notesInput.value.trim() || null,
        },
      });
      if (r?.error) {
        errLine.textContent = r.error;
        saveBtn.textContent = "Create contact";
        saveBtn.disabled = false;
        return;
      }
      toast(`Contact #${r.contactId} created`, "ok");
      closeDrawer();
      if (typeof renderLiveMap === "function") renderLiveMap();
    } catch (err) {
      errLine.textContent = err.message;
      saveBtn.textContent = "Create contact";
      saveBtn.disabled = false;
    }
  });

  openDrawer("New Contact", form);
  setTimeout(() => nameInput.focus(), 100);
}

// ── Manual "New company" form (drawer) ──────────────────────────
function openNewCompanyDrawer() {
  const form = el("div", { style: "display:flex;flex-direction:column;gap:10px;padding:12px 0;" });
  const labeled = (label, inputEl, hint) =>
    el("div", {}, [
      el("label", { class: "wizard-label" }, label),
      inputEl,
      hint ? el("div", { class: "muted small", style: "margin-top:2px;" }, hint) : null,
    ]);

  const nameInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "Company name (required)",
    autofocus: "autofocus",
  });
  const websiteInput = el("input", {
    class: "wizard-input",
    type: "url",
    placeholder: "https://acme.xyz",
  });
  const sectorInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "DeFi · Infra · Marketing · …",
  });
  const stageInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "seed · series_a · public",
  });
  const countryInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "Country / HQ",
  });
  const xInput = el("input", { class: "wizard-input", type: "text", placeholder: "@handle" });
  const liInput = el("input", {
    class: "wizard-input",
    type: "url",
    placeholder: "https://linkedin.com/company/…",
  });
  const tokenInput = el("input", { class: "wizard-input", type: "text", placeholder: "$TICKER" });
  const raisedInput = el("input", {
    class: "wizard-input",
    type: "text",
    placeholder: "e.g. $25M Series A",
  });
  const descInput = el("textarea", {
    class: "wizard-textarea",
    rows: "3",
    placeholder: "Short description",
  });
  const notesInput = el("textarea", {
    class: "wizard-textarea",
    rows: "2",
    placeholder: "Internal CRM notes",
  });
  const targetInput = el("input", { type: "checkbox", style: "vertical-align:middle;" });

  form.appendChild(labeled("Name *", nameInput));
  form.appendChild(labeled("Website", websiteInput));
  form.appendChild(
    el("div", { style: "display:flex;gap:8px;" }, [
      el("div", { style: "flex:1;" }, [labeled("Sector / category", sectorInput)]),
      el("div", { style: "flex:1;" }, [labeled("Stage", stageInput)]),
    ]),
  );
  form.appendChild(labeled("Country / HQ", countryInput));
  form.appendChild(
    el("div", { style: "display:flex;gap:8px;" }, [
      el("div", { style: "flex:1;" }, [labeled("X handle", xInput)]),
      el("div", { style: "flex:1;" }, [labeled("LinkedIn URL", liInput)]),
    ]),
  );
  form.appendChild(
    el("div", { style: "display:flex;gap:8px;" }, [
      el("div", { style: "flex:1;" }, [labeled("Token", tokenInput)]),
      el("div", { style: "flex:1;" }, [labeled("Total raised", raisedInput)]),
    ]),
  );
  form.appendChild(labeled("Description", descInput));
  form.appendChild(labeled("CRM notes", notesInput));
  form.appendChild(
    el("label", { style: "display:flex;align-items:center;gap:8px;font-size:12px;" }, [
      targetInput,
      el("span", {}, "Mark as target (priority for outreach)"),
    ]),
  );

  const errLine = el("div", { class: "muted small", style: "color:var(--red);min-height:14px;" });
  form.appendChild(errLine);

  const saveBtn = el("button", { class: "btn btn-primary", type: "button" }, "Create company");
  const cancelBtn = el(
    "button",
    { class: "btn btn-secondary", type: "button", onclick: () => closeDrawer() },
    "Cancel",
  );
  form.appendChild(el("div", { class: "wizard-actions" }, [saveBtn, cancelBtn]));

  saveBtn.addEventListener("click", async () => {
    const name = nameInput.value.trim();
    if (!name) {
      errLine.textContent = "Name is required.";
      nameInput.focus();
      return;
    }
    saveBtn.textContent = "Saving…";
    saveBtn.disabled = true;
    errLine.textContent = "";
    try {
      const r = await api("crm-companies-create", {
        body: {
          name,
          website: websiteInput.value.trim() || null,
          sector: sectorInput.value.trim() || null,
          stage: stageInput.value.trim() || null,
          country: countryInput.value.trim() || null,
          twitterHandle: xInput.value.trim() || null,
          linkedinUrl: liInput.value.trim() || null,
          tokenSymbol: tokenInput.value.trim() || null,
          totalRaised: raisedInput.value.trim() || null,
          description: descInput.value.trim() || null,
          notes: notesInput.value.trim() || null,
          is_target: targetInput.checked ? 1 : 0,
        },
      });
      if (r?.error) {
        errLine.textContent = r.error;
        saveBtn.textContent = "Create company";
        saveBtn.disabled = false;
        return;
      }
      toast(`Company #${r.companyId} created`, "ok");
      closeDrawer();
      if (typeof renderLiveMap === "function") renderLiveMap();
    } catch (err) {
      errLine.textContent = err.message;
      saveBtn.textContent = "Create company";
      saveBtn.disabled = false;
    }
  });

  openDrawer("New Company", form);
  setTimeout(() => nameInput.focus(), 100);
}

// ── Accounts page ───────────────────────────────────────────────

// ── Settings page ───────────────────────────────────────────────

let settingsTab = "account";

async function renderSettings() {
  showLoading("Loading settings\u2026");
  const data = await api("settings-get").catch(() => ({}));
  if (data?.error) {
    showError(data.error, renderSettings);
    return;
  }

  const tierBadges = {
    free: "badge-gray",
    pro: "badge-blue",
    premium: "badge-blue",
    lifetime: "badge-purple",
    enterprise: "badge-green",
    unlimited: "badge-green",
  };

  const header = pageHeader("Settings", "Subscription, AI keys, notifications, preferences");

  const tabs = el("div", { class: "tabs" }, [
    tabBtn("account", "Account", () => {
      settingsTab = "account";
      renderSettings();
    }),
    tabBtn("ai-keys", "AI Keys", () => {
      settingsTab = "ai-keys";
      renderSettings();
    }),
    tabBtn("notifications", "Notifications", () => {
      settingsTab = "notifications";
      renderSettings();
    }),
    tabBtn("preferences", "Preferences", () => {
      settingsTab = "preferences";
      renderSettings();
    }),
  ]);

  // Mark active tab \u2014 query after the next tick
  setTimeout(() => {
    $$(".tab").forEach((t) => {
      t.classList.toggle("active", t.getAttribute("data-tab") === settingsTab);
    });
  }, 0);

  let body;
  if (settingsTab === "ai-keys") {
    body = await renderSettingsAiKeys();
  } else if (settingsTab === "notifications") {
    body = renderSettingsNotifications(data);
  } else if (settingsTab === "preferences") {
    body = await renderSettingsPreferences(data);
  } else {
    body = renderSettingsAccount(data, tierBadges);
  }

  setContentNode(el("div", {}, [header, tabs, body]));
}

function renderSettingsAccount(data, tierBadges) {
  return el("div", {}, [
    // Subscription
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Subscription"),
      el("div", { class: "row", style: "gap: 12px; margin-bottom: 12px;" }, [
        el("div", {}, [
          el("div", { class: "stat-label" }, "Tier"),
          el("div", {}, [
            el(
              "span",
              { class: `badge ${tierBadges[data.tier] || "badge-gray"}` },
              (data.tier || "free").toUpperCase(),
            ),
          ]),
        ]),
        el("div", {}, [
          el("div", { class: "stat-label" }, "Budget used today"),
          el("div", { class: "mono" }, `${data.budget?.used || 0} / ${data.budget?.limit || 0}`),
        ]),
        el("div", {}, [
          el("div", { class: "stat-label" }, "Remaining"),
          el("div", { class: "mono" }, String(data.budget?.remaining || 0)),
        ]),
      ]),
      el(
        "button",
        {
          class: "btn btn-secondary btn-tiny",
          type: "button",
          onclick: () => navigate("premium"),
        },
        "View Premium \u2192",
      ),
    ]),

    // Quick toggles (legacy \u2014 these target server-side settings via settings-update)
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Workspace"),
      settingToggle("Test Mode", "test_mode", data.testMode),
      settingToggle("Live Feed", "live_feed_enabled", data.liveFeed),
      settingToggle("Luma Auto-Subscribe", "luma_autosub_enabled", data.lumaAutoSub),
    ]),

    // Experimental features (client-side toggles backed by chrome.storage.local)
    renderExperimentalFeaturesCard(),

    // Extension
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Extension"),
      el("div", { style: "display: flex; align-items: center; gap: 12px;" }, [
        el(
          "div",
          { class: "muted small" },
          `Paired: ${data.extensionPaired ? "\u2705 Yes" : "\u274c No"}`,
        ),
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            onclick: () => chrome.tabs.create({ url: chrome.runtime.getURL("popup.html") }),
          },
          "Open Popup",
        ),
      ]),
    ]),

    // Utility links
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Tools"),
      el("div", { class: "row", style: "flex-wrap: wrap; gap: 8px;" }, [
        el(
          "button",
          { class: "btn btn-secondary", type: "button", onclick: () => navigate("import") },
          "\ud83d\udce5 Import CSV",
        ),
        el(
          "button",
          { class: "btn btn-secondary", type: "button", onclick: () => navigate("scan") },
          "\ud83d\udd0d Company Scanner",
        ),
        el(
          "button",
          { class: "btn btn-secondary", type: "button", onclick: () => navigate("accounts") },
          "\ud83d\udc65 Manage Accounts",
        ),
        el(
          "button",
          { class: "btn btn-secondary", type: "button", onclick: () => navigate("admin") },
          "\ud83d\udd27 Admin Panel",
        ),
      ]),
    ]),
  ]);
}

// ── Admin panel ─────────────────────────────────────────────────

const TRIGGER_JOBS = [
  { id: "matching", label: "Person Matching", desc: "Score and deliver match suggestions" },
  { id: "scrape", label: "Funding Scrape", desc: "DeFiLlama + RootData + Jobs" },
  { id: "cryptorank", label: "CryptoRank", desc: "Funding rounds + company enrichment" },
  { id: "coingecko", label: "CoinGecko", desc: "Token data fetch" },
  { id: "outreach", label: "Outreach Cycle", desc: "Run multi-channel outreach for all users" },
  { id: "replies", label: "Reply Detection", desc: "Check for replies to outreach" },
  { id: "leads", label: "Lead Scoring", desc: "Re-score all leads" },
  { id: "members", label: "Member Scrape", desc: "Scrape all community members" },
  { id: "warmer", label: "Account Warmer", desc: "Warmup cycle for new accounts" },
  { id: "analytics", label: "Analytics Scan", desc: "Track engagement metrics" },
  { id: "crm", label: "CRM Sync", desc: "Google Sheets bidirectional sync" },
  { id: "digest", label: "Weekly Digest", desc: "Send digest DMs to users" },
  { id: "reminders", label: "Verification Reminders", desc: "Gate-keeping reminders" },
  { id: "stories", label: "Story Posting", desc: "Post scheduled stories" },
];

async function renderAdmin() {
  showLoading("Loading system health\u2026");
  const health = await api("admin-health").catch(() => ({}));

  const sys = health.system || {};
  const counts = health.counts || {};

  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "Admin Panel"),
        el("div", { class: "page-subtitle" }, "System health, background jobs, and diagnostics"),
      ]),
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: renderAdmin },
        "\u21bb Refresh",
      ),
    ]),

    // System health
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "\ud83c\udfe5 System Health"),
      el("div", { class: "stats-grid" }, [
        statCard("Uptime", sys.uptime || "?"),
        statCard("Memory", `${sys.memoryMB || 0} MB`, `Heap: ${sys.heapMB || 0} MB`),
        statCard("Database", `${sys.dbSizeMB || 0} MB`, `Schema v${sys.schemaVersion || "?"}`),
        statCard(
          "Errors (1h)",
          String(sys.errorsLastHour || 0),
          null,
          sys.errorsLastHour > 5 ? "warning" : null,
        ),
        statCard("Social Svc", sys.socialServiceReady ? "\u2705 Ready" : "\u274c Down"),
        statCard("Node", sys.nodeVersion || "?"),
      ]),
    ]),

    // Data counts
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "\ud83d\udcca Data Overview"),
      el("div", { class: "stats-grid" }, [
        statCard("Contacts", fmtNum(counts.shared_contacts || 0)),
        statCard("Companies", fmtNum(counts.companies || 0)),
        statCard("Outreach", fmtNum(counts.outreach_campaigns || 0)),
        statCard("DM Campaigns", fmtNum(counts.campaigns || 0)),
        statCard("Email Campaigns", fmtNum(counts.email_campaigns || 0)),
        statCard("Signals", fmtNum(counts.buying_signals || 0)),
        statCard("TG Accounts", String(counts.premium_accounts || 0)),
        statCard("Social Accounts", String(counts.social_accounts || 0)),
      ]),
    ]),

    // Background jobs
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "\u26a1 Background Jobs"),
      el(
        "div",
        { class: "muted small", style: "margin-bottom: 12px;" },
        "Click to run immediately. Jobs run in the background (10-60s).",
      ),
      el(
        "div",
        {
          style:
            "display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px;",
        },
        TRIGGER_JOBS.map((job) =>
          el(
            "div",
            {
              class: "card",
              style: "padding: 10px; cursor: pointer; transition: border-color 120ms; margin: 0;",
              onmouseenter: (e) => {
                e.currentTarget.style.borderColor = "var(--accent)";
              },
              onmouseleave: (e) => {
                e.currentTarget.style.borderColor = "var(--border)";
              },
              onclick: async (e) => {
                await withProgress(e.currentTarget.querySelector(".primary-cell"), async () => {
                  const result = await api("admin-trigger", { body: { job: job.id } });
                  if (result?.ok) toast(`\u2705 ${job.label} completed`, "ok");
                  else toast(`\u274c ${result?.error || "failed"}`, "err");
                });
              },
            },
            [
              el(
                "div",
                { class: "primary-cell", style: "font-size: 12px; margin-bottom: 2px;" },
                job.label,
              ),
              el("div", { class: "muted", style: "font-size: 10px;" }, job.desc),
            ],
          ),
        ),
      ),
    ]),

    // Quick links
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Quick Links"),
      el("div", { class: "row", style: "flex-wrap: wrap; gap: 8px;" }, [
        el(
          "button",
          { class: "btn btn-secondary", type: "button", onclick: () => navigate("import") },
          "\ud83d\udce5 Import CSV",
        ),
        el(
          "button",
          { class: "btn btn-secondary", type: "button", onclick: () => navigate("scan") },
          "\ud83d\udd0d Company Scanner",
        ),
        el(
          "button",
          { class: "btn btn-secondary", type: "button", onclick: () => navigate("accounts") },
          "\ud83d\udc65 Accounts",
        ),
        el(
          "button",
          { class: "btn btn-secondary", type: "button", onclick: () => navigate("settings") },
          "\u2699 Settings",
        ),
      ]),
    ]),
  ]);
  setContentNode(content);
}

function settingToggle(label, key, currentValue) {
  return el("div", { class: "toggle-row" }, [
    el("div", { class: "toggle-label" }, label),
    el("div", {
      class: `toggle${currentValue ? " on" : ""}`,
      onclick: async function () {
        const newValue = !currentValue;
        this.classList.toggle("on", newValue);
        const result = await api("settings-update", { body: { key, value: newValue ? "1" : "0" } });
        if (result?.ok) {
          toast(`${label}: ${newValue ? "ON" : "OFF"}`, "ok");
        } else {
          this.classList.toggle("on", currentValue); // revert
          toast(`Error: ${result?.error || "failed"}`, "err");
        }
      },
    }),
  ]);
}

// ── Connection check ─────────────────────────────────────────────

async function checkConnection() {
  try {
    const storage = await chrome.storage.local.get(["token", "apiBase"]);
    dashboardState.token = storage.token;
    dashboardState.apiBase = storage.apiBase || "http://localhost:9091";
    if (!storage.token) {
      setStatus(false, "Not paired");
      showNotPairedScreen();
      return false;
    }
    // Test connection
    const stats = await api("campaigns-stats");
    if (stats?.error) {
      setStatus(false, "Error: " + (stats.error || "").slice(0, 30));
      return false;
    }
    setStatus(true, "Connected");
    return true;
  } catch (err) {
    setStatus(false, "Offline");
    return false;
  }
}

function showNotPairedScreen() {
  const screen = el("div", { class: "empty-state" }, [
    el("div", { class: "empty-state-icon" }, "\ud83d\udd12"),
    el("div", { class: "empty-state-title" }, "Not paired"),
    el("div", { class: "empty-state-hint" }, [
      el("p", {}, "Connect this extension to your Nexus bot to get started:"),
      el(
        "ol",
        { style: "text-align: left; line-height: 2; margin: 12px auto; max-width: 360px;" },
        [
          el("li", {}, "Open Telegram and DM the Nexus bot: /pair"),
          el("li", {}, "Copy the pair code you receive"),
          el("li", {}, "Click the button below to open the extension popup"),
          el("li", {}, "Paste the code and click Connect"),
        ],
      ),
      el(
        "button",
        {
          class: "btn btn-primary",
          type: "button",
          style: "margin-top: 12px;",
          onclick: () => chrome.tabs.create({ url: chrome.runtime.getURL("popup.html") }),
        },
        "Open Extension Popup",
      ),
    ]),
  ]);
  setContentNode(screen);
}

// ── Init ─────────────────────────────────────────────────────────

// ── Auto-refresh ─────────────────────────────────────────────────

let autoRefreshTimer = null;
function startAutoRefresh() {
  if (autoRefreshTimer) return;
  autoRefreshTimer = setInterval(() => {
    // Skip the tick when the tab is hidden — refresh fires immediately on
    // visibilitychange below, so the user always sees fresh data when they
    // come back. Saves bandwidth + battery while the dashboard's in another tab.
    if (document.visibilityState !== "visible") return;
    const route = dashboardState.currentRoute;
    // Only auto-refresh data-heavy views, not wizards
    if (["home", "campaigns"].includes(route)) {
      handleRoute();
    }
  }, 30000);
  // Refresh-on-focus: when the user comes back to the tab after a while, refresh
  // immediately rather than waiting for the next 30s tick.
  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState !== "visible") return;
    const route = dashboardState.currentRoute;
    if (["home", "campaigns"].includes(route)) {
      handleRoute();
    }
  });
}

// ── Keyboard shortcuts ───────────────────────────────────────────

function setupKeyboardShortcuts() {
  let gPressed = false;
  let gTimer = null;

  document.addEventListener("keydown", (e) => {
    // Ignore if user is typing in an input/textarea
    const tag = e.target.tagName;
    if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || e.target.isContentEditable)
      return;

    if (e.key === "Escape") {
      closeDrawer();
      return;
    }

    if (e.key === "?" && !e.ctrlKey && !e.metaKey) {
      e.preventDefault();
      toggleKbdOverlay();
      return;
    }

    // "/" → focus the current view's search input. We pick the first
    // visible .filter-input on the page; works for CRM, Campaigns, Signals.
    if (e.key === "/" && !e.ctrlKey && !e.metaKey && !gPressed) {
      const search = focusFirstSearchInput();
      if (search) e.preventDefault();
      return;
    }

    // j / k — move the active CRM-row selection (visual highlight + scroll
    // into view). Enter activates the active row.
    if ((e.key === "j" || e.key === "ArrowDown") && !e.ctrlKey && !e.metaKey && !gPressed) {
      if (moveActiveRow(1)) e.preventDefault();
      return;
    }
    if ((e.key === "k" || e.key === "ArrowUp") && !e.ctrlKey && !e.metaKey && !gPressed) {
      if (moveActiveRow(-1)) e.preventDefault();
      return;
    }
    if (e.key === "Enter" && !e.ctrlKey && !e.metaKey && !gPressed) {
      if (activateActiveRow()) e.preventDefault();
      return;
    }

    if (e.key === "r" && !e.ctrlKey && !e.metaKey && !gPressed) {
      handleRoute(); // refresh
      return;
    }

    // Two-key combos: g then h/c/s/v/m/i/o/d/r
    if (e.key === "g" && !gPressed) {
      gPressed = true;
      clearTimeout(gTimer);
      gTimer = setTimeout(() => {
        gPressed = false;
      }, 800);
      return;
    }

    if (gPressed) {
      gPressed = false;
      clearTimeout(gTimer);
      // Direct route shortcuts.
      const shortcutMap = {
        h: "home",
        c: "campaigns",
        s: "signals",
        v: "vc",
        m: "crm",
        i: "inbox",
        d: "deals",
      };
      if (shortcutMap[e.key]) {
        e.preventDefault();
        navigate(shortcutMap[e.key]);
        return;
      }
      // CRM sub-tab shortcut: `g o` opens CRM > Companies.
      if (e.key === "o") {
        e.preventDefault();
        if (location.hash.replace(/^#/, "") !== "crm") navigate("crm");
        if (typeof crmTab !== "undefined") crmTab = "companies";
        if (typeof renderCrm === "function") setTimeout(renderCrm, 0);
      }
    }
  });
}

// Focus the first visible search input in the current view.
// Returns true if focus was set (the caller can preventDefault).
function focusFirstSearchInput() {
  const candidates = $$(".filter-input");
  for (const c of candidates) {
    if (c.offsetParent !== null) {
      c.focus();
      c.select && c.select();
      return true;
    }
  }
  return false;
}

// j/k row navigation — adds .row-active to a single tbody tr and scrolls
// it into view. We only navigate within the first visible CRM/Campaign
// table. `delta` is +1 (down) or -1 (up).
function moveActiveRow(delta) {
  const tbody = $(".table-wrap .table tbody");
  if (!tbody) return false;
  const rows = [...tbody.querySelectorAll("tr")].filter((r) => r.offsetParent !== null);
  if (rows.length === 0) return false;
  const currentIdx = rows.findIndex((r) => r.classList.contains("row-active"));
  let nextIdx = currentIdx + delta;
  if (currentIdx === -1) nextIdx = delta > 0 ? 0 : rows.length - 1;
  nextIdx = Math.max(0, Math.min(rows.length - 1, nextIdx));
  rows.forEach((r) => r.classList.remove("row-active"));
  const next = rows[nextIdx];
  next.classList.add("row-active");
  next.scrollIntoView({ block: "nearest", behavior: "smooth" });
  return true;
}

// Enter — activate the currently selected row (clicks it). The row's
// existing onclick handler opens the appropriate drawer.
function activateActiveRow() {
  const active = $(".table-wrap .table tbody tr.row-active");
  if (!active) return false;
  active.click();
  return true;
}

let kbdOverlayVisible = false;
function toggleKbdOverlay() {
  kbdOverlayVisible = !kbdOverlayVisible;
  let overlay = $("#kbd-overlay");
  if (!overlay) {
    overlay = el("div", {
      class: "kbd-overlay",
      id: "kbd-overlay",
      onclick: () => toggleKbdOverlay(),
    });
    const box = el("div", { class: "kbd-box", onclick: (ev) => ev.stopPropagation() });
    box.innerHTML = `
      <h3>Keyboard shortcuts</h3>
      <div class="kbd-row"><span>Go to Home</span><span><span class="kbd-key">g</span> then <span class="kbd-key">h</span></span></div>
      <div class="kbd-row"><span>Go to Campaigns</span><span><span class="kbd-key">g</span> then <span class="kbd-key">c</span></span></div>
      <div class="kbd-row"><span>Go to Signals</span><span><span class="kbd-key">g</span> then <span class="kbd-key">s</span></span></div>
      <div class="kbd-row"><span>Go to VC Outreach</span><span><span class="kbd-key">g</span> then <span class="kbd-key">v</span></span></div>
      <div class="kbd-row"><span>Go to CRM</span><span><span class="kbd-key">g</span> then <span class="kbd-key">m</span></span></div>
      <div class="kbd-row"><span>Go to CRM > Companies</span><span><span class="kbd-key">g</span> then <span class="kbd-key">o</span></span></div>
      <div class="kbd-row"><span>Go to Inbox</span><span><span class="kbd-key">g</span> then <span class="kbd-key">i</span></span></div>
      <div class="kbd-row"><span>Go to Deals</span><span><span class="kbd-key">g</span> then <span class="kbd-key">d</span></span></div>
      <div class="kbd-row"><span>Open command palette</span><span><span class="kbd-key">Ctrl</span><span class="kbd-key">K</span></span></div>
      <div class="kbd-row"><span>Focus search</span><span><span class="kbd-key">/</span></span></div>
      <div class="kbd-row"><span>Move row selection</span><span><span class="kbd-key">j</span> / <span class="kbd-key">k</span></span></div>
      <div class="kbd-row"><span>Open selected row</span><span><span class="kbd-key">↵</span></span></div>
      <div class="kbd-row"><span>Refresh current view</span><span><span class="kbd-key">r</span></span></div>
      <div class="kbd-row"><span>Close drawer / overlay</span><span><span class="kbd-key">Esc</span></span></div>
      <div class="kbd-row"><span>Show this overlay</span><span><span class="kbd-key">?</span></span></div>
    `;
    overlay.appendChild(box);
    document.body.appendChild(overlay);
  }
  overlay.classList.toggle("show", kbdOverlayVisible);
}

// ── Init ─────────────────────────────────────────────────────────

// ── Earn — community quests (the Circuit × Engine seam) ─────────────────
//
// The LOCAL view of the quest loop: what this machine's extension queued and
// drained, what the verifier proved, and the XP the Circuit reported awarding.
// Backed by GET /api/quests/summary (db/quest-jobs.js summarizeQuestsForOwner —
// the same source of truth as the Today tile and the Circuit's "My engine"
// passport panel). Browsing/claiming quests happens in the Circuit web app.

// The three "open the Circuit" buttons below all use this. Until r94 it pointed
// at thecircuit.vercel.app — a 644-byte unrelated stub — so every one of them
// sent the user to a blank page.
//
// It must carry the /home path, not just the origin: "/" on the Circuit is the
// PUBLIC MARKETING SPLASH ("One terminal for communities, quests & traders"),
// and the app lives at /home ("Dashboard · The Circuit"). Sending an already-
// paired operator to the splash is the same dead end by a subtler route. This
// is exactly what landing/vercel.json does for every other entry point, and
// circuit-origin.drift.test.js pins the two together.
const CIRCUIT_APP_URL = "https://thecircuit-app.vercel.app/home";

function _earnAgo(ts) {
  if (!ts) return "—";
  const s = Math.floor((Date.now() - ts) / 1000);
  if (s < 60) return "just now";
  if (s < 3600) return `${Math.floor(s / 60)}m ago`;
  if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
  return `${Math.floor(s / 86400)}d ago`;
}

async function renderEarn() {
  showLoading("Loading quests…");
  const data = await api("quests-summary").catch(() => null);

  const header = el("div", { class: "page-header" }, [
    el("div", {}, [
      el("div", { class: "page-title" }, "◆ Earn"),
      el(
        "div",
        { class: "page-subtitle" },
        "Community quests from the Circuit — completed from this machine, verified with receipts.",
      ),
    ]),
    el("div", { style: "display:flex;gap:8px;" }, [
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: renderEarn },
        "↻ Refresh",
      ),
      el(
        "button",
        {
          class: "btn btn-primary",
          type: "button",
          onclick: () => window.open(CIRCUIT_APP_URL, "_blank"),
        },
        "Browse quests ↗",
      ),
    ]),
  ]);

  if (!data || data.error || data.unauthorized) {
    setContentNode(
      el("div", {}, [
        header,
        el("div", { class: "empty-state", style: "margin-top:14px;" }, [
          el("div", { class: "empty-state-icon" }, "◆"),
          el("div", { class: "empty-state-title" }, "Quest summary unavailable"),
          el(
            "div",
            { class: "empty-state-hint" },
            data?.error ||
              "The bot may need a restart to load /api/quests/summary — or you're not paired yet.",
          ),
        ]),
      ]),
    );
    return;
  }

  const jobs = data.jobs || {};
  const watches = data.watches || {};
  const xp = data.xp || {};
  const queued = (jobs.pending || 0) + (jobs.dispatched || 0) + (jobs.running || 0);

  // ── KPI strip ─────────────────────────────────────────────────────
  const kpis = el(
    "div",
    {
      style:
        "display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:12px;",
    },
    [
      statCard("Queued", queued, queued > 0 ? "drains on this machine" : "nothing waiting", null),
      statCard("Awaiting proof", watches.watching || 0, "verifier is watching", null),
      statCard(
        "Verified today",
        data.verifiedToday || 0,
        (xp.today || 0) > 0 ? `+${xp.today} XP` : "",
        null,
      ),
      statCard("XP this week", xp.week || 0, `${xp.total || 0} all-time`, null),
    ],
  );

  // ── How it works (one compact card — mirrors the landing's loop) ──
  const loopCard = el("div", { class: "card", style: "padding:14px 16px;margin-bottom:12px;" }, [
    el(
      "div",
      {
        style:
          "font-size:11px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:8px;",
      },
      "How the loop works",
    ),
    el("div", { style: "font-size:12.5px;color:var(--text-1);line-height:1.7;" }, [
      el("div", {}, "1. Claim a quest on the Circuit (“Do it for me” queues it here)."),
      el(
        "div",
        {},
        "2. This extension drains it from your own logged-in account — within your daily caps and active hours.",
      ),
      el(
        "div",
        {},
        "3. The verifier proves it independently (the community's own account sees it) and the Circuit awards XP.",
      ),
    ]),
    el("div", { style: "margin-top:8px;display:flex;gap:12px;flex-wrap:wrap;" }, [
      el(
        "a",
        {
          href: CIRCUIT_APP_URL,
          target: "_blank",
          style: "font-size:12px;color:#7c6bff;text-decoration:none;",
        },
        "Open the Circuit ↗",
      ),
      el(
        "a",
        {
          href: "#",
          onclick: (e) => {
            e.preventDefault();
            navigate("health");
          },
          style: "font-size:12px;color:#7c6bff;text-decoration:none;",
        },
        "Seam health →",
      ),
    ]),
  ]);

  // ── Recent activity ───────────────────────────────────────────────
  const recent = Array.isArray(data.recent) ? data.recent : [];
  const statusColor = (s) =>
    s === "done" || s === "verified"
      ? "#22c55e"
      : s === "failed" || s === "rejected"
        ? "#f87171"
        : "#9ca3af";
  const recentCard = recent.length
    ? el("div", { class: "card", style: "padding:14px 16px;" }, [
        el(
          "div",
          {
            style:
              "font-size:11px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:10px;",
          },
          "Recent quest activity",
        ),
        el(
          "div",
          { style: "display:flex;flex-direction:column;gap:6px;" },
          recent.map((r) =>
            el(
              "div",
              {
                style:
                  "display:flex;align-items:center;gap:10px;font-size:12.5px;padding:6px 8px;border-radius:6px;background:var(--bg-card);",
              },
              [
                el(
                  "span",
                  { style: `color:${statusColor(r.status)};font-weight:600;width:64px;` },
                  r.status,
                ),
                el(
                  "span",
                  { style: "color:var(--text-2);width:70px;text-transform:capitalize;" },
                  r.platform || "—",
                ),
                el(
                  "span",
                  {
                    style:
                      "flex:1;color:#e8e8ef;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
                  },
                  `${r.action || "?"}${r.target ? ` · ${r.target}` : ""}`,
                ),
                r.awarded_xp
                  ? el(
                      "span",
                      { style: "color:var(--green);font-size:11px;font-weight:600;" },
                      `+${r.awarded_xp} XP`,
                    )
                  : null,
                el("span", { style: "color:var(--text-4);font-size:11px;" }, _earnAgo(r.at)),
              ],
            ),
          ),
        ),
      ])
    : null;

  // ── Empty state (seam never used) ─────────────────────────────────
  const everUsed =
    queued +
      (jobs.done || 0) +
      (jobs.failed || 0) +
      (watches.watching || 0) +
      (watches.verified || 0) +
      (watches.rejected || 0) +
      (watches.expired || 0) >
    0;
  const emptyState = !everUsed
    ? el("div", { class: "empty-state", style: "margin-top:14px;" }, [
        el("div", { class: "empty-state-icon" }, "◆"),
        el("div", { class: "empty-state-title" }, "No quests yet"),
        el(
          "div",
          { class: "empty-state-hint" },
          "Join a community on the Circuit and claim a quest — “I did it” quests verify automatically, “Do it for me” quests run from this machine.",
        ),
        el("div", { style: "margin-top:10px;" }, [
          el(
            "button",
            {
              class: "btn btn-primary",
              type: "button",
              onclick: () => window.open(CIRCUIT_APP_URL, "_blank"),
            },
            "Enter the Circuit ↗",
          ),
        ]),
      ])
    : null;

  setContentNode(
    el("div", {}, [
      header,
      kpis,
      loopCard,
      recentCard,
      emptyState,
      data.lastDrainAt
        ? el(
            "div",
            { class: "muted small", style: "margin-top:10px;" },
            `Last drain: ${_earnAgo(data.lastDrainAt)}`,
          )
        : null,
    ]),
  );
}

// ── Email Sequences ─────────────────────────────────────────────

async function renderEmailSequences() {
  showLoading("Loading sequences\u2026");
  const data = await api("email-sequences-list").catch(() => ({}));
  if (data?.error) {
    showError(data.error, renderEmailSequences);
    return;
  }
  const sequences = data.sequences || [];

  let formName = "",
    formSteps = [{ subject: "", body: "", delayDays: 2 }];
  function addStep() {
    formSteps.push({ subject: "", body: "", delayDays: 2 });
    renderEmailSequences();
  }

  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "Email Sequences"),
        el("div", { class: "page-subtitle" }, "Multi-step drip campaigns with timed delays"),
      ]),
    ]),

    // Existing sequences
    sequences.length > 0
      ? el("div", { class: "card" }, [
          el("div", { class: "card-title" }, `Sequences (${sequences.length})`),
          ...sequences.map((s) =>
            el("div", { class: "channel-row", style: "align-items: flex-start;" }, [
              el("div", { class: "channel-icon" }, s.status === "active" ? "\u25b6" : "\u23f8"),
              el("div", { style: "flex: 1;" }, [
                el("div", { class: "primary-cell" }, s.name),
                el(
                  "div",
                  { class: "muted", style: "font-size: 10px;" },
                  `${s.stepCount} steps \u00b7 ${s.enrollments?.active || 0} active \u00b7 ${s.enrollments?.completed || 0} completed \u00b7 ${s.enrollments?.replied || 0} replied`,
                ),
              ]),
              el(
                "button",
                {
                  class: "btn btn-tiny btn-secondary",
                  type: "button",
                  onclick: async () => {
                    await api("email-sequences-toggle", {
                      seqId: s.id,
                      body: { enabled: s.status !== "active" },
                    });
                    toast(s.status === "active" ? "Paused" : "Resumed", "ok");
                    renderEmailSequences();
                  },
                },
                s.status === "active" ? "\u23f8" : "\u25b6",
              ),
            ]),
          ),
        ])
      : null,

    // Create new sequence
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Create Sequence"),
      el("div", { class: "wizard-label" }, "Name"),
      el("input", {
        class: "wizard-input",
        placeholder: "e.g. 3-step intro sequence",
        oninput: (e) => {
          formName = e.target.value;
        },
      }),
      ...formSteps.map((step, i) =>
        el(
          "div",
          {
            style:
              "margin-top: 16px; padding: 12px; background: var(--bg-0); border-radius: var(--radius-md); border: 1px solid var(--border);",
          },
          [
            el(
              "div",
              {
                style:
                  "font-size: 11px; color: var(--text-4); font-weight: 600; margin-bottom: 8px;",
              },
              `Step ${i + 1}${i > 0 ? ` (after ${step.delayDays} days)` : " (immediate)"}`,
            ),
            i > 0
              ? el("div", { style: "margin-bottom: 8px;" }, [
                  el("span", { class: "wizard-label" }, "Delay (days): "),
                  el("input", {
                    class: "wizard-input",
                    type: "number",
                    value: String(step.delayDays),
                    style: "width: 60px;",
                    oninput: (e) => {
                      formSteps[i].delayDays = parseInt(e.target.value) || 2;
                    },
                  }),
                ])
              : null,
            el("div", { class: "wizard-label" }, "Subject"),
            el("input", {
              class: "wizard-input",
              placeholder: "Subject line",
              value: step.subject,
              oninput: (e) => {
                formSteps[i].subject = e.target.value;
              },
            }),
            el("div", { class: "wizard-label", style: "margin-top: 6px;" }, "Body"),
            el(
              "textarea",
              {
                class: "wizard-textarea",
                style: "min-height: 80px;",
                placeholder: "Email body\u2026 Use {name}, {company}, {role}",
                oninput: (e) => {
                  formSteps[i].body = e.target.value;
                },
              },
              step.body,
            ),
          ],
        ),
      ),
      el("div", { style: "display: flex; gap: 8px; margin-top: 12px;" }, [
        el(
          "button",
          { class: "btn btn-secondary", type: "button", onclick: addStep },
          "\u2795 Add Step",
        ),
        el(
          "button",
          {
            class: "btn btn-primary",
            type: "button",
            onclick: async () => {
              if (!formName || !formSteps[0]?.subject) {
                toast("Name and at least one subject required", "err");
                return;
              }
              const r = await api("email-sequences-create", {
                body: { name: formName, steps: formSteps },
              });
              if (r?.ok) {
                toast(`\u2705 Sequence created (${r.stepCount} steps)`, "ok");
                renderEmailSequences();
              } else toast(`\u274c ${r?.error || "Failed"}`, "err");
            },
          },
          "\ud83d\ude80 Create Sequence",
        ),
      ]),
    ]),
  ]);
  setContentNode(content);
}

// ── CSV Import ──────────────────────────────────────────────────

async function renderImport() {
  let csvText = "",
    filename = "",
    previewData = null;

  function renderImportContent() {
    const content = el("div", {}, [
      el("div", { class: "page-header" }, [
        el("div", {}, [
          el("div", { class: "page-title" }, "Import CSV"),
          el("div", { class: "page-subtitle" }, "Bulk import contacts and companies into your CRM"),
        ]),
      ]),

      // Upload area
      el("div", { class: "card" }, [
        el("div", { class: "card-title" }, "Upload CSV"),
        el(
          "div",
          { class: "muted small", style: "margin-bottom: 12px;" },
          "Paste CSV text or upload a file. Supported formats: conference speakers, contacts, companies, recently funded.",
        ),
        el(
          "textarea",
          {
            class: "wizard-textarea",
            style: "min-height: 150px; font-family: var(--font-mono); font-size: 11px;",
            placeholder: "name,email,company,role\nJohn Doe,john@acme.com,Acme Corp,CEO\n\u2026",
            oninput: (e) => {
              csvText = e.target.value;
            },
          },
          csvText,
        ),
        el("div", { style: "display: flex; gap: 8px; margin-top: 8px;" }, [
          el(
            "button",
            {
              class: "btn btn-primary",
              type: "button",
              onclick: async () => {
                if (!csvText.trim()) {
                  toast("Paste CSV data first", "err");
                  return;
                }
                toast("Previewing\u2026", "info");
                const r = await api("import-preview", { body: { csvText, filename } });
                if (r?.headers) {
                  previewData = r;
                  renderImportContent();
                } else toast(`\u274c ${r?.error || "Preview failed"}`, "err");
              },
            },
            "\ud83d\udd0d Preview",
          ),
          el("label", { class: "btn btn-secondary", style: "cursor: pointer;" }, [
            "Upload file",
            el("input", {
              type: "file",
              accept: ".csv,.txt",
              style: "display: none;",
              onchange: (e) => {
                const file = e.target.files[0];
                if (!file) return;
                filename = file.name;
                const reader = new FileReader();
                reader.onload = () => {
                  csvText = reader.result;
                  renderImportContent();
                };
                reader.readAsText(file);
              },
            }),
          ]),
        ]),
      ]),

      // Preview
      previewData
        ? el("div", { class: "card" }, [
            el(
              "div",
              { class: "card-title" },
              `Preview: ${previewData.rowCount} rows, ${previewData.headers.length} columns`,
            ),
            el("div", { class: "table-wrap" }, [
              el("table", { class: "table" }, [
                el("thead", {}, [
                  el(
                    "tr",
                    {},
                    previewData.headers.map((h) => el("th", {}, h)),
                  ),
                ]),
                el(
                  "tbody",
                  {},
                  previewData.preview.map((row) =>
                    el(
                      "tr",
                      {},
                      previewData.headers.map((h) =>
                        el("td", { class: "small" }, row[h] || "\u2014"),
                      ),
                    ),
                  ),
                ),
              ]),
            ]),
            el(
              "button",
              {
                class: "btn btn-primary",
                style: "margin-top: 12px;",
                type: "button",
                onclick: async () => {
                  toast("Importing\u2026", "info");
                  const r = await api("import-execute", { body: { csvText, filename } });
                  if (r?.ok) {
                    toast(
                      `\u2705 Imported ${r.contactsInserted || 0} contacts, ${r.companiesInserted || 0} companies`,
                      "ok",
                      6000,
                    );
                    previewData = null;
                    csvText = "";
                    renderImportContent();
                  } else toast(`\u274c ${r?.error || "Import failed"}`, "err");
                },
              },
              `\ud83d\ude80 Import ${previewData.rowCount} rows`,
            ),
          ])
        : null,
    ]);
    setContentNode(content);
  }
  renderImportContent();
}

// ── Company Scanner ─────────────────────────────────────────────

async function renderScan() {
  let scanUrl = "",
    scanResult = null;

  function renderScanContent() {
    const content = el("div", {}, [
      el("div", { class: "page-header" }, [
        el("div", {}, [
          el("div", { class: "page-title" }, "Company Scanner"),
          el("div", { class: "page-subtitle" }, "Extract company info from any website"),
        ]),
      ]),
      el("div", { class: "card" }, [
        el("div", { class: "card-title" }, "Scan URL"),
        el("div", { style: "display: flex; gap: 8px;" }, [
          el("input", {
            class: "filter-input",
            placeholder: "https://company.com",
            value: scanUrl,
            oninput: (e) => {
              scanUrl = e.target.value;
            },
          }),
          el(
            "button",
            {
              class: "btn btn-primary",
              type: "button",
              onclick: async () => {
                if (!scanUrl.trim()) {
                  toast("Enter a URL", "err");
                  return;
                }
                toast("Scanning\u2026", "info");
                const r = await api("scan-url", { body: { url: scanUrl.trim() } });
                if (r?.url) {
                  scanResult = r;
                  renderScanContent();
                } else toast(`\u274c ${r?.error || "Scan failed"}`, "err");
              },
            },
            "\ud83d\udd0d Scan",
          ),
        ]),
      ]),
      scanResult
        ? el("div", { class: "card" }, [
            el("div", { class: "card-title" }, "Results"),
            fieldRow("Title", scanResult.title),
            fieldRow(
              "Description",
              scanResult.description ? scanResult.description.slice(0, 200) : null,
            ),
            fieldRow("Twitter", scanResult.twitter ? `@${scanResult.twitter}` : null),
            fieldRow("LinkedIn", scanResult.linkedin),
            scanResult.emails?.length > 0
              ? el("div", {}, [
                  el(
                    "h4",
                    {
                      style:
                        "margin-top: 12px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-4); font-weight: 600;",
                    },
                    "Emails found",
                  ),
                  ...scanResult.emails.map((e) =>
                    el(
                      "div",
                      { style: "padding: 3px 0; font-size: 12px; color: var(--text-2);" },
                      e,
                    ),
                  ),
                ])
              : null,
          ])
        : null,
    ]);
    setContentNode(content);
  }
  renderScanContent();
}

// ── Account detail views (Phase 3) ──────────────────────────────

// Shared account detail page builder
async function renderAccountDetail(
  platformKey,
  accountsKey,
  findFn,
  titleFn,
  infoFn,
  actionsFn,
  params,
) {
  if (!dashboardState.cache.accounts)
    dashboardState.cache.accounts = await api("accounts-all").catch(() => ({}));
  const accts = dashboardState.cache.accounts?.[accountsKey] || [];
  const id = decodeURIComponent(params?.accountId || "");
  const account = accts.find((a) => findFn(a, id));
  if (!account) {
    showError(`Account "${id}" not found`, () => handleRoute());
    return;
  }

  showLoading("Loading account stats\u2026");
  const stats = await api("account-stats", { platform: platformKey, id: String(account.id) }).catch(
    () => ({}),
  );

  const todayUsed = stats.todayActions || 0;
  const todayLimit = stats.dailyLimit || 0;
  const todayPct = todayLimit > 0 ? Math.min(100, Math.round((todayUsed / todayLimit) * 100)) : 0;

  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, titleFn(account)),
        el("div", { class: "page-subtitle" }, `${platformKey} \u2014 ${account.status}`),
      ]),
      el("div", {}, [statusDot(account.status)]),
    ]),

    // Stats
    el("div", { class: "stats-grid" }, [
      statCard("Health", `${account.health_score || 100}%`, [
        el("div", { class: "channel-bar", style: "margin-top: 6px;" }, [
          el("div", {
            class: `channel-bar-fill ${(account.health_score || 100) >= 70 ? "healthy" : "degraded"}`,
            style: `width: ${account.health_score || 100}%`,
          }),
        ]),
      ]),
      statCard("Warmup", `Day ${stats.warmupDay || account.warmup_day || 0}`),
      statCard("Today", todayLimit > 0 ? `${todayUsed}/${todayLimit}` : String(todayUsed), [
        todayLimit > 0
          ? el("div", { class: "channel-bar", style: "margin-top: 6px;" }, [
              el("div", {
                class: `channel-bar-fill ${todayPct < 80 ? "healthy" : "degraded"}`,
                style: `width: ${todayPct}%`,
              }),
            ])
          : null,
      ]),
      statCard("Campaigns", String((stats.activeCampaigns || []).length)),
    ]),

    // Quick actions
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Quick Actions"),
      el("div", { style: "display: flex; gap: 8px; flex-wrap: wrap;" }, actionsFn(account)),
    ]),

    // Active campaigns \u2014 each row drills into the visual campaign-detail view.
    (stats.activeCampaigns || []).length > 0
      ? el("div", { class: "card" }, [
          el("div", { class: "card-title" }, `Active Campaigns (${stats.activeCampaigns.length})`),
          ...stats.activeCampaigns.map((c) =>
            el(
              "div",
              {
                class: "channel-row",
                style: "cursor: pointer;",
                title: "Click to see what this campaign is doing",
                onclick: () => navigate(`campaigns/${c.type || "dm"}/${c.id}`),
              },
              [
                el("div", { class: "channel-icon" }, "\u25b6"),
                el("div", { style: "flex: 1;" }, [
                  el(
                    "div",
                    { class: "primary-cell", style: "font-size: 12px;" },
                    c.name || `Campaign #${c.id}`,
                  ),
                  c.sent_count != null
                    ? el(
                        "div",
                        { class: "muted", style: "font-size: 10px;" },
                        `${c.sent_count}/${c.total_recipients || "?"} sent`,
                      )
                    : null,
                ]),
                el("span", { class: "badge badge-green", style: "font-size: 9px;" }, c.status),
              ],
            ),
          ),
        ])
      : null,

    // Recent activity
    (stats.recentActivity || []).length > 0
      ? el("div", { class: "card" }, [
          el("div", { class: "card-title" }, "Recent Activity"),
          ...stats.recentActivity.slice(0, 15).map((e) =>
            el(
              "div",
              {
                style:
                  "display: flex; gap: 8px; padding: 4px 0; border-bottom: 1px solid var(--border); font-size: 11px;",
              },
              [
                el(
                  "span",
                  {
                    class: "mono",
                    style: "font-size: 9px; color: var(--text-4); white-space: nowrap;",
                  },
                  fmtDate(e.created_at),
                ),
                el("span", { class: "badge badge-gray", style: "font-size: 9px;" }, e.event_type),
                el(
                  "span",
                  { style: "flex: 1; color: var(--text-2); word-break: break-word;" },
                  (e.detail || "").slice(0, 100),
                ),
              ],
            ),
          ),
        ])
      : null,

    // Account info
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Account Info"),
      ...infoFn(account),
    ]),
  ]);
  setContentNode(content);
}

async function renderXAccount(params) {
  return renderAccountDetail(
    "twitter",
    "twitter",
    (a, id) => String(a.id) === id || a.account_identifier === id,
    (a) => `@${a.account_identifier}`,
    (a) => [
      fieldRow("Handle", `@${a.account_identifier}`),
      fieldRow("Status", a.status),
      fieldRow("Display name", a.display_name || "\u2014"),
      fieldRow("Max daily", String(a.max_daily_outreach || 0)),
      a.cooldown_until ? fieldRow("Cooldown", fmtDate(a.cooldown_until)) : null,
    ],
    () => [
      el(
        "button",
        { class: "btn btn-primary", type: "button", onclick: () => navigate("x") },
        "\ud83d\udc26 Post & Engage",
      ),
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: () => navigate("campaigns") },
        "\ud83d\udce3 Campaigns",
      ),
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: () => navigate("signals") },
        "\ud83d\udd25 Signals",
      ),
    ],
    params,
  );
}

async function renderTgAccount(params) {
  return renderAccountDetail(
    "telegram",
    "telegram",
    (a, id) => String(a.id) === id || a.label === id || a.phone === id,
    (a) => a.label || a.phone,
    (a) => [
      fieldRow("Phone", a.phone),
      fieldRow("Label", a.label || "\u2014"),
      fieldRow("Premium", a.has_telegram_premium ? "Yes" : "No"),
      fieldRow("Created", fmtDate(a.created_at)),
    ],
    () => [
      el(
        "button",
        { class: "btn btn-primary", type: "button", onclick: () => navigate("new-dm-campaign") },
        "\ud83d\udce8 New DM Campaign",
      ),
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: () => navigate("campaigns") },
        "\ud83d\udce3 Campaigns",
      ),
    ],
    params,
  );
}

async function renderLiAccount(params) {
  return renderAccountDetail(
    "linkedin",
    "linkedin",
    (a, id) => String(a.id) === id || a.identifier === id,
    (a) => a.identifier,
    (a) => [
      fieldRow("Identifier", a.identifier),
      fieldRow("Status", a.status),
      fieldRow("Validated", a.last_validated_at ? fmtDate(a.last_validated_at) : "never"),
      fieldRow("Proxy", a.proxy_id ? `#${a.proxy_id}` : "None"),
      fieldRow("Created", fmtDate(a.created_at)),
    ],
    (a) => [
      el(
        "button",
        { class: "btn btn-primary", type: "button", onclick: () => navigate("campaigns") },
        "\ud83d\udce3 Campaigns",
      ),
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: () => navigate("signals") },
        "\ud83d\udd25 Signals",
      ),
      el(
        "button",
        {
          class: "btn btn-secondary",
          type: "button",
          onclick: (e) => _forceSync(e, "sync-linkedin", "LinkedIn"),
        },
        "\u21bb Re-sync",
      ),
    ],
    params,
  );
}

async function renderEmailAccount(params) {
  return renderAccountDetail(
    "email",
    "smtp",
    (a, id) => String(a.id) === id || a.email === id,
    (a) => a.email,
    (a) => [
      fieldRow("Email", a.email),
      fieldRow("SMTP", `${a.smtp_host}:${a.smtp_port || 587}`),
      fieldRow("Status", a.status),
      fieldRow("Display name", a.display_name || "\u2014"),
    ],
    () => [
      el(
        "button",
        { class: "btn btn-primary", type: "button", onclick: () => navigate("new-email-campaign") },
        "\u2709\ufe0f New Email Campaign",
      ),
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: () => navigate("email/warmup") },
        "\ud83d\udd25 Warmup",
      ),
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: () => navigate("campaigns") },
        "\ud83d\udce3 Campaigns",
      ),
    ],
    params,
  );
}

// ── Init ────────────────────────────────────────────────────────

// ── Connection health strip (topbar) ───────────────────────────────────
// Always-visible per-platform session status: X / LinkedIn / Luma. A red dot is
// the #1 reason "nothing happens" (expired session). Click a pill to LIVE-test
// it; ↻ to reconnect (pull fresh cookies from your logged-in browser tab).
async function renderConnectionStrip() {
  const mount = document.getElementById("conn-strip");
  if (!mount) return;
  let data;
  try {
    data = await apiCached("accounts-all");
  } catch {
    return;
  }
  const platforms = [
    { key: "twitter", label: "𝕏", active: (data.twitter || []).some((a) => a.status === "active") },
    { key: "linkedin", label: "in", active: (data.linkedin || []).length > 0 },
    { key: "luma", label: "luma", active: !!(data.luma && data.luma.status === "active") },
  ];
  mount.innerHTML = "";
  for (const p of platforms) {
    const dot = el("span", {
      class: "conn-dot",
      style: `display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:4px;background:${p.active ? "#3fb950" : "#f85149"};`,
    });
    const pill = el(
      "button",
      {
        title: `${p.key}: ${p.active ? "connected" : "NOT connected"} — click to live-test`,
        style:
          "display:inline-flex;align-items:center;gap:2px;background:transparent;border:1px solid var(--border-soft);border-radius:6px;color:#bbb;font-size:11px;padding:2px 7px;cursor:pointer;",
        onclick: () => _connTest(p.key),
      },
      [dot, el("span", {}, p.label)],
    );
    const recon = el(
      "button",
      {
        title: `Reconnect ${p.key} — pull fresh cookies from your logged-in browser tab`,
        style:
          "background:transparent;border:none;color:#6ea8ff;font-size:12px;cursor:pointer;padding:0 2px;margin:0 8px 0 2px;",
        onclick: () => _connReconnect(p.key),
      },
      "↻",
    );
    mount.appendChild(
      el("span", { style: "display:inline-flex;align-items:center;" }, [pill, recon]),
    );
  }
  mount.appendChild(
    el(
      "button",
      {
        title: "Add an account — opens a fresh-fingerprint login window on this machine",
        style:
          "background:transparent;border:1px dashed #3a3a48;border-radius:6px;color:#7fb;font-size:11px;padding:2px 8px;cursor:pointer;margin-left:4px;",
        onclick: () => _openAddAccountModal(),
      },
      "+ Add",
    ),
  );
}

// Manual-login modal: pick a platform (+ optional proxy / custom URL), open a
// fresh-footprint login window on this machine, capture + store the session.
function _openAddAccountModal() {
  const sel = el(
    "select",
    { class: "nx-input", id: "ml-platform" },
    ["twitter", "linkedin", "luma", "gmail", "other"].map((p) =>
      el(
        "option",
        { value: p },
        p === "twitter" ? "X (Twitter)" : p === "other" ? "Other (custom URL)" : p,
      ),
    ),
  );
  const proxyInput = el("input", {
    class: "nx-input",
    id: "ml-proxy",
    type: "text",
    placeholder: "Proxy (optional) — http://user:pass@host:port",
  });
  const urlRow = el("div", { id: "ml-url-row", style: "display:none;margin-top:8px;" }, [
    el("label", { class: "nx-label" }, "Login URL"),
    el("input", {
      class: "nx-input",
      id: "ml-url",
      type: "text",
      placeholder: "https://site.com/login",
    }),
    el("label", { class: "nx-label", style: "margin-top:6px;" }, "Label"),
    el("input", { class: "nx-input", id: "ml-label", type: "text", placeholder: "account name" }),
  ]);
  sel.addEventListener("change", () => {
    urlRow.style.display = sel.value === "other" ? "block" : "none";
  });

  const status = el("div", {
    id: "ml-status",
    class: "muted",
    style: "font-size:12px;margin-top:10px;min-height:18px;",
  });
  const body = el("div", {}, [
    el(
      "p",
      { class: "nx-hint", style: "margin:0 0 10px;" },
      "A separate login window opens on this machine with its own fingerprint (+ proxy if set). Log in by hand — it captures the session automatically. Avoids multi-account fingerprint correlation.",
    ),
    el("label", { class: "nx-label" }, "Platform"),
    sel,
    el("label", { class: "nx-label", style: "margin-top:8px;" }, "Proxy"),
    proxyInput,
    urlRow,
    status,
  ]);

  const startBtn = el(
    "button",
    {
      class: "btn btn-primary",
      onclick: async () => {
        const platform = sel.value;
        const proxyUrl = (proxyInput.value || "").trim() || null;
        const loginUrl =
          platform === "other"
            ? (document.getElementById("ml-url").value || "").trim() || null
            : null;
        const identifier =
          platform === "other"
            ? (document.getElementById("ml-label").value || "").trim() || null
            : null;
        if (platform === "other" && !loginUrl) {
          status.textContent = "Enter a login URL.";
          return;
        }
        status.textContent = "Opening login window on this machine…";
        try {
          const r = await api("manual-login", {
            body: { platform, proxyUrl, loginUrl, identifier },
          });
          if (!r?.loginId) {
            status.textContent = `Failed: ${r?.error || "no loginId"}`;
            return;
          }
          status.textContent = "Login window opened — finish login there. Waiting…";
          _pollManualLogin(r.loginId, status);
        } catch (e) {
          status.textContent = `Error: ${e.message}`;
        }
      },
    },
    "Open login window →",
  );

  openModal({ title: "Add account (manual login)", body, actions: [startBtn], maxWidth: "440px" });
}

async function _pollManualLogin(loginId, statusEl) {
  for (let i = 0; i < 110; i++) {
    await new Promise((r) => setTimeout(r, 3000));
    let s;
    try {
      s = await api("manual-login-status", {
        queryString: `?loginId=${encodeURIComponent(loginId)}`,
      });
    } catch {
      continue;
    }
    if (s?.status === "waiting") {
      statusEl.textContent = "Waiting for you to finish login… " + ".".repeat((i % 4) + 1);
      continue;
    }
    if (s?.status === "success") {
      const res = s.result || {};
      statusEl.textContent = `Captured ${res.platform} session: ${res.identifier || "(saved)"} — stored in ${s.persisted?.stored || "DB"}.`;
      toast(
        `Added ${res.platform} account${res.identifier ? ` @${res.identifier}` : ""}`,
        "ok",
        5000,
      );
      invalidateApiCache("accounts-all");
      renderConnectionStrip();
      return;
    }
    statusEl.textContent = `${s?.status || "error"}: ${s?.error || "login not completed"}`;
    return;
  }
  statusEl.textContent = "⏱ Timed out waiting for login.";
}

async function _connTest(platform) {
  toast(`Testing ${platform}…`, "ok", 1500);
  try {
    const r = await api("connections-test", { body: { platform } });
    toast(
      `${platform}: ${r.ok ? "🟢" : "🔴"} ${r.detail || (r.ok ? "OK" : "failed")}`,
      r.ok ? "ok" : "err",
      6000,
    );
  } catch (e) {
    toast(`${platform} test error: ${e.message}`, "err", 6000);
  }
  invalidateApiCache("accounts-all");
  renderConnectionStrip();
}

async function _connReconnect(platform) {
  const act = { twitter: "sync-twitter", linkedin: "sync-linkedin", luma: "sync-luma" }[platform];
  if (!act) return;
  toast(
    `Reconnecting ${platform}… make sure you're logged into ${platform} in a browser tab.`,
    "ok",
    3500,
  );
  try {
    const r = await new Promise((resolve) => {
      chrome.runtime.sendMessage({ action: act }, (resp) =>
        resolve(
          chrome.runtime.lastError ? { error: chrome.runtime.lastError.message } : resp || {},
        ),
      );
    });
    if (r.error) toast(`${platform} reconnect: ${r.error}`, "err", 6000);
    else if (r.ok === false)
      toast(`${platform}: couldn't refresh — log into ${platform} first, then retry.`, "err", 6000);
    else toast(`${platform}: reconnected ✓`, "ok", 4000);
  } catch (e) {
    toast(`${platform} reconnect error: ${e.message}`, "err", 6000);
  }
  setTimeout(() => {
    invalidateApiCache("accounts-all");
    renderConnectionStrip();
  }, 2500);
}

// Last-resort net for anything that escapes a renderer.
//
// There was none. A throw inside a render function, or a rejected promise
// nobody awaited, left the LAST view on screen with no message — so the
// dashboard looked like it had loaded fine while the thing you asked for had
// simply not happened. That is the exact failure docs/CPO-REVIEW-2026-07.md
// identifies as the product's credibility problem, and it was invisible by
// construction: 123 `.catch(() => ({}))` sites across dashboard*.js turn a
// failed fetch into a view indistinguishable from "no data".
//
// This does not fix those call sites. It guarantees that when something does
// blow up, the user is told, instead of the UI quietly lying.
function installGlobalErrorNet() {
  let lastShownAt = 0;
  const report = (what, err) => {
    const msg = String(err?.message || err || "Something went wrong");
    // Never let the net itself become the noise: at most one toast every 4s.
    const now = Date.now();
    if (now - lastShownAt < 4000) return;
    lastShownAt = now;
    console.error(`[dashboard] ${what}:`, err);
    toast(`Something went wrong: ${msg}`, "err", 6000);
  };

  window.addEventListener("unhandledrejection", (e) => report("unhandled rejection", e?.reason));
  window.addEventListener("error", (e) => {
    // Resource load failures (a missing <img>) also fire this with no `error`;
    // they are not renderer faults and must not raise a toast.
    if (!e?.error) return;
    report("uncaught error", e.error);
  });
}

async function init() {
  // Installed FIRST — before checkConnection, so a failure during startup is
  // reported rather than leaving a blank page with no explanation.
  installGlobalErrorNet();

  const online = await checkConnection();
  if (!online) return;

  // ── CRITICAL PATH ── Listeners/intervals the user might hit immediately.
  // Refresh button, settings, route changes, header actions, sidebar collapse —
  // all need to be live before/during the async data fetches.
  $("#drawer-backdrop").addEventListener("click", closeDrawer);
  $("#drawer-close").addEventListener("click", closeDrawer);
  $("#refresh-btn").addEventListener("click", () => handleRoute());
  // (gear retired — Settings now lives in the top-bar avatar menu; see wireTopbarActions)
  wireTopbarActions();
  window.addEventListener("hashchange", handleRoute);

  // Kick off the three async data-loaders in PARALLEL. They're independent:
  //   - buildSidebar fetches accounts-all
  //   - refreshInboxBadge fetches inbox-replies
  //   - handleRoute renders the current page (which may fetch its own data)
  // Previously sequential — saved ~200-400ms on cold load by overlapping.
  const dataReady = Promise.all([
    buildSidebar(),
    refreshInboxBadge(),
    refreshNotifBadge(),
    renderWorkspaceSwitcher(),
    handleRoute(),
    renderConnectionStrip(),
  ]);

  // ── DEFERRED ── Wire low-priority work after first paint settles.
  // Keyboard shortcuts, command-palette listener, polling timers — none of
  // these are needed for the user's first interaction. Pushing them into idle
  // time gives the main thread breathing room during the busy initial render.
  scheduleIdle(() => {
    setupKeyboardShortcuts();
    wireCommandPaletteShortcut();
    startAutoRefresh();
    startInboxBadgePolling();
    setInterval(renderConnectionStrip, 60_000);
  });

  await dataReady;
}

// Cross-browser idle scheduler. Falls back to setTimeout(..., 50) on engines
// that don't support requestIdleCallback (e.g., older Safari). 50ms gives the
// browser time to commit the first paint before we run additional setup.
function scheduleIdle(fn) {
  if (typeof requestIdleCallback === "function") {
    requestIdleCallback(fn, { timeout: 1500 });
  } else {
    setTimeout(fn, 50);
  }
}

// ── Lightweight client-side telemetry (batched) ────────────────────
// Records IA-related user actions to chrome.storage.local with a rolling
// 1000-event window. Used to compare classic vs v2 sidebar engagement
// during the phased rollout. Inspect via DevTools:
//   chrome.storage.local.get("_telemetryLog").then(r => console.table(r._telemetryLog))
//
// BATCHING: each logTelemetry() call appends to an in-memory queue and is
// completely synchronous (no awaits in the user's interaction path). The queue
// flushes:
//   - 5s after the most recent event (debounced — quiet sessions don't flush)
//   - immediately when the tab loses visibility (page hide / tab switch)
//   - immediately on beforeunload (best-effort; storage.local may not finish)
// Without batching, every click did 2 awaited storage round-trips.

let _telemetryQueue = []; // in-memory pending events (newest last)
let _telemetryFlushTimer = null;
let _navIaV2Cached = null; // cached so we don't re-fetch on every event
const TELEMETRY_FLUSH_MS = 5000;
const TELEMETRY_MAX_LOG = 1000;

// Sync, never throws. Schedules a flush; doesn't block the caller.
function logTelemetry(category, action, props = {}) {
  try {
    _telemetryQueue.push({
      ts: Date.now(),
      category,
      action,
      route: (location.hash || "#home").slice(1).split("?")[0],
      ...props,
    });
    // Debounce — collapse bursts of clicks into one storage write.
    if (_telemetryFlushTimer) clearTimeout(_telemetryFlushTimer);
    _telemetryFlushTimer = setTimeout(_flushTelemetry, TELEMETRY_FLUSH_MS);
  } catch {
    // Telemetry must never break the app.
  }
}

async function _flushTelemetry() {
  _telemetryFlushTimer = null;
  if (_telemetryQueue.length === 0) return;
  const batch = _telemetryQueue.splice(0, _telemetryQueue.length); // drain
  try {
    // Lazy-fetch the IA flag once per process — it's only flipped via Settings,
    // and the user reloads after toggling, so caching is safe.
    if (_navIaV2Cached === null) {
      const stored = await chrome.storage.local.get(["_navIaV2"]);
      _navIaV2Cached = stored._navIaV2 === true;
    }
    for (const e of batch) e.iaV2 = _navIaV2Cached;

    const stored = await chrome.storage.local.get(["_telemetryLog"]);
    const log = stored._telemetryLog || [];
    log.push(...batch);
    while (log.length > TELEMETRY_MAX_LOG) log.shift();
    await chrome.storage.local.set({ _telemetryLog: log });
  } catch {
    // On error, drop the batch — better than retrying forever and pinning storage.
  }
}

// Flush on tab hide / unload so we don't lose events from quick visits.
// `visibilitychange` fires reliably; `beforeunload` is best-effort.
document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") _flushTelemetry();
});
window.addEventListener("beforeunload", () => {
  _flushTelemetry();
});

// ── Workspace / client switcher (multi-tenant) ─────────────────────────
// Lists the agency user's workspaces, shows the active one, and lets you switch
// (stored in chrome.storage.local.workspaceId → injected as X-Workspace-Id by
// background.js) or create a new client. Switching scopes ALL data to that client.
async function _switchWorkspace(wsId, label) {
  await chrome.storage.local.set({ workspaceId: wsId });
  invalidateApiCache(); // all cached reads belonged to the old client
  const lbl = $("#topbar-ws-label");
  if (lbl && label) lbl.textContent = label;
  const menu = $("#topbar-ws-menu");
  if (menu) menu.hidden = true;
  // Refresh everything that depends on the active client.
  try {
    await Promise.all([
      buildSidebar(),
      refreshInboxBadge(),
      refreshNotifBadge(),
      renderConnectionStrip(),
      handleRoute(),
    ]);
  } catch {}
  renderWorkspaceSwitcher();
}

async function _createWorkspaceFlow() {
  const name = (window.prompt("Nombre del nuevo cliente / workspace:") || "").trim();
  if (!name) return;
  const r = await api("workspace-create", { name }).catch((e) => ({ error: e.message }));
  if (r?.id) {
    await _switchWorkspace(r.id, name);
  } else {
    alert(r?.message || r?.error || "No se pudo crear el workspace.");
  }
}

// Edit a client's brand voice/offer — feeds the AI drafter for that client.
async function _editWorkspaceBrandFlow(wsId, wsName) {
  const cur = (await api("workspace-brand-get", { id: wsId }).catch(() => ({}))).brand || {};
  const voice = window.prompt(
    `Voz/tono de marca para ${wsName}\n(cómo debe sonar la IA al escribir para este cliente):`,
    cur.voice || "",
  );
  if (voice === null) return; // cancelled
  const offer = window.prompt(
    `Oferta / posicionamiento de ${wsName}\n(qué ofrece, en una frase — la IA lo usará):`,
    cur.offer || "",
  );
  if (offer === null) return;
  const brand = { ...cur, name: wsName, voice: voice.trim(), offer: (offer || "").trim() };
  const r = await api("workspace-brand-set", { id: wsId, brand }).catch((e) => ({
    error: e.message,
  }));
  if (r?.ok) {
    invalidateApiCache();
    alert("Marca guardada. La IA usará esta voz al escribir para este cliente.");
  } else alert(r?.message || r?.error || "No se pudo guardar la marca.");
}

async function renderWorkspaceSwitcher() {
  const btn = $("#topbar-ws-btn");
  const label = $("#topbar-ws-label");
  const menu = $("#topbar-ws-menu");
  if (!btn || !label || !menu) return;
  let data;
  try {
    data = await api("workspaces-list");
  } catch {
    return;
  }
  if (!data || data.error) {
    label.textContent = "Workspace";
    return;
  }

  const workspaces = data.workspaces || [];
  const active = data.active; // effective owner this request
  const activeWs =
    workspaces.find((w) => w.id === active) || workspaces.find((w) => w.isDefault) || workspaces[0];
  label.textContent = activeWs ? activeWs.name.replace(/\s*\(default\)$/, "") : "Workspace";

  // Hide the switcher entirely if there's only the default workspace (solo operator).
  const wrap = btn.closest(".topbar-ws-wrap");
  if (wrap) wrap.style.display = workspaces.length <= 1 ? "none" : "";

  menu.innerHTML = "";
  for (const w of workspaces) {
    const isActive = w.id === (activeWs && activeWs.id);
    const item = el(
      "div",
      {
        class: `topbar-account-menu-item${isActive ? " active" : ""}`,
        onclick: () => _switchWorkspace(w.id, w.name.replace(/\s*\(default\)$/, "")),
      },
      [
        el("span", { class: "topbar-account-dot online" }),
        el("span", {}, w.isDefault ? `${w.name.replace(/\s*\(default\)$/, "")} · tú` : w.name),
      ],
    );
    menu.appendChild(item);
  }
  menu.appendChild(el("div", { class: "topbar-account-menu-divider" }));
  if (workspaces.length > 1) {
    menu.appendChild(
      el(
        "div",
        {
          class: "topbar-account-menu-item",
          onclick: () => {
            menu.hidden = true;
            navigate("agency");
          },
        },
        [el("span", {}, "Vista de agencia")],
      ),
    );
  }
  // Edit brand for the active client (not the personal default workspace).
  if (activeWs && !activeWs.isDefault) {
    const nm = activeWs.name.replace(/\s*\(default\)$/, "");
    menu.appendChild(
      el(
        "div",
        {
          class: "topbar-account-menu-item",
          onclick: () => {
            menu.hidden = true;
            _editWorkspaceBrandFlow(activeWs.id, nm);
          },
        },
        [el("span", {}, `Marca de ${nm}`)],
      ),
    );
  }
  menu.appendChild(
    el(
      "div",
      {
        class: "topbar-account-menu-item",
        onclick: () => _createWorkspaceFlow(),
      },
      [el("span", {}, "Nuevo cliente")],
    ),
  );
}

// ── Agency overview (multi-client cockpit) ─────────────────────────────
async function renderAgencyOverview() {
  showLoading("Cargando vista de agencia…");
  const header = el("div", { class: "page-header" }, [
    el("div", {}, [
      el("div", { class: "page-title" }, "Agencia — rendimiento por cliente"),
      el(
        "div",
        { class: "page-subtitle" },
        "Funnel de cada cliente (workspace) en los últimos 30 días + total",
      ),
    ]),
    el(
      "button",
      { class: "btn btn-secondary", type: "button", onclick: () => renderAgencyOverview() },
      "↻ Refresh",
    ),
  ]);
  let data;
  try {
    data = await api("agency-overview", { queryString: "?days=30" });
  } catch (err) {
    data = { error: err.message };
  }
  if (!data || data.error) {
    setContentNode(
      el("div", {}, [
        header,
        el("div", { class: "card" }, [
          el("div", { class: "muted" }, `No se pudo cargar: ${data?.error || "error"}.`),
        ]),
      ]),
    );
    return;
  }
  const clients = data.clients || [];
  const t = data.total || {};
  const cols = ["Cliente", "Enviados", "Respuestas", "Reuniones", "Deals", "Ganados", "% Resp."];
  const headRow = el(
    "tr",
    {},
    cols.map((c) =>
      el(
        "th",
        {
          style:
            "text-align:left;padding:6px 10px;border-bottom:1px solid var(--border-soft);font-size:11px;color:var(--text-3);",
        },
        c,
      ),
    ),
  );
  const rows = clients.map((c) =>
    el(
      "tr",
      {
        style: "cursor:pointer;border-bottom:1px solid var(--bg-card);",
        onclick: () => _switchWorkspace(c.id, c.name),
      },
      [
        el(
          "td",
          { style: "padding:6px 10px;font-size:12px;" },
          c.name + (c.isDefault ? " · tú" : ""),
        ),
        ...["sent", "replied", "meetings", "deals", "won"].map((k) =>
          el("td", { style: "padding:6px 10px;font-size:12px;" }, String(c[k] || 0)),
        ),
        el("td", { style: "padding:6px 10px;font-size:12px;" }, `${c.replyRate || 0}%`),
      ],
    ),
  );
  const totalRow = el("tr", { style: "border-top:2px solid var(--border-soft);font-weight:600;" }, [
    el("td", { style: "padding:6px 10px;font-size:12px;" }, "TOTAL"),
    ...["sent", "replied", "meetings", "deals", "won"].map((k) =>
      el("td", { style: "padding:6px 10px;font-size:12px;" }, String(t[k] || 0)),
    ),
    el("td", { style: "padding:6px 10px;font-size:12px;" }, `${t.replyRate || 0}%`),
  ]);
  const table = el("table", { style: "width:100%;border-collapse:collapse;" }, [
    el("thead", {}, [headRow]),
    el(
      "tbody",
      {},
      clients.length
        ? [...rows, totalRow]
        : [
            el("tr", {}, [
              el(
                "td",
                { colspan: "7", class: "muted", style: "padding:12px;" },
                "Aún no hay clientes con actividad. Crea un cliente desde el selector de workspace.",
              ),
            ]),
          ],
    ),
  ]);
  setContentNode(
    el("div", {}, [
      header,
      el("div", { class: "card" }, [table]),
      el(
        "div",
        { class: "muted", style: "font-size:11px;margin-top:8px;" },
        "Clic en un cliente para cambiar a su workspace.",
      ),
    ]),
  );
}

async function openTgStrategy(strategyId) {
  const r = await api("strategy-deeplink", { strategyId });
  if (r?.deeplink) {
    chrome.tabs.create({ url: r.deeplink });
  } else {
    toast("Couldn't open Telegram", "err");
  }
}

function openTgCommand(command) {
  const botUsername = "onchainsocialbot";
  chrome.tabs.create({
    url: `https://t.me/${botUsername}?start=${encodeURIComponent(command.replace("/", ""))}`,
  });
}

// ═════════════════════════════════════════════════════════════════
// ICP Scoring (throxy-style three-gate + empirical ensemble)
// ═════════════════════════════════════════════════════════════════

const ICP_BAND_BADGE = {
  bin: { cls: "badge-red", label: "Bin" },
  inbound: { cls: "badge-gray", label: "Inbound" },
  outbound: { cls: "badge-blue", label: "Outbound" },
  abm: { cls: "badge-green", label: "ABM" },
};

const ICP_BAND_ORDER = ["abm", "outbound", "inbound", "bin"];

const icpState = {
  tab: "overview", // 'overview' | 'score' | 'config'
  config: null, // cached declarative config
  status: null, // cached band histogram
  adhoc: {}, // "Score a company" wizard inputs
  adhocResult: null,
  companies: { rows: [], band: null, loading: false },
  editingCompany: null, // if set, wizard is editing this company's persisted signals
};

function renderIcpBandBadge(band) {
  const info = ICP_BAND_BADGE[band] || ICP_BAND_BADGE.bin;
  return el("span", { class: `badge ${info.cls}` }, info.label);
}

function renderIcpScoreBar10(score, band) {
  const s = Math.max(0, Math.min(10, Number(score) || 0));
  const cls =
    band === "abm"
      ? "score-hot"
      : band === "outbound"
        ? "score-high"
        : band === "inbound"
          ? "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: ${s * 10}%;` }),
    ]),
    el("span", { class: "score-num" }, s.toFixed(1)),
  ]);
}

async function renderIcp() {
  $("#topbar-title") && ($("#topbar-title").textContent = "ICP Scoring");
  const content = $("#content");
  content.innerHTML = "";

  // Sub-tab hint from URL (e.g. #icp/score opened from the content-launcher FAB)
  const hashParts = location.hash.replace(/^#/, "").split("/");
  if (
    hashParts[0] === "icp" &&
    hashParts[1] &&
    ["overview", "score", "config"].includes(hashParts[1])
  ) {
    icpState.tab = hashParts[1];
  }

  // Pending pre-fill from content-launcher (LinkedIn FAB → "Open in dashboard")
  try {
    const { _pendingIcpWizard } = await chrome.storage.local.get(["_pendingIcpWizard"]);
    if (_pendingIcpWizard && Date.now() - (_pendingIcpWizard.capturedAt || 0) < 5 * 60_000) {
      const ctx = _pendingIcpWizard.ctx || {};
      icpState.adhoc = {
        name: ctx.name,
        sector: ctx.sector,
        ecosystem: ctx.ecosystem,
        crypto_category: ctx.crypto_category,
        tvl_band: ctx.tvl_band,
        has_token: ctx.has_token,
        backed_by_tier_1: ctx.backed_by_tier_1,
        is_rugged: ctx.is_rugged,
        differentiation_notes: ctx.differentiation_notes,
      };
      icpState.adhocResult = _pendingIcpWizard.result || null;
      icpState.tab = "score";
      await chrome.storage.local.remove("_pendingIcpWizard");
    }
  } catch {
    /* non-fatal */
  }

  // Load config once per mount
  if (!icpState.config) {
    try {
      const resp = await api("icp-config-get");
      if (resp?.error) throw new Error(resp.error);
      icpState.config = resp.config;
      icpState._defaults = resp.defaults;
      icpState._bandCopy = resp.band_copy;
      icpState._empirical = resp.empirical;
      icpState._taxonomy = resp.taxonomy || {};
    } catch (err) {
      content.appendChild(emptyState("🎯", "Couldn't load ICP config", err.message));
      return;
    }
  }

  const wrap = el("div", {});
  wrap.appendChild(
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "ICP Scoring"),
        el(
          "div",
          { class: "page-subtitle" },
          "Rank any company 0–10. Built on your declarative gates + won-deal signals.",
        ),
      ]),
      el("div", {}, [
        el(
          "button",
          {
            class: "btn btn-secondary",
            type: "button",
            onclick: async () => {
              toast("Rescoring all companies…", "info");
              const r = await api("icp-rescore-all").catch((e) => ({ error: e.message }));
              if (r?.error) toast(`Rescore failed: ${r.error}`, "err");
              else {
                icpState.status = null; // force reload
                toast("Rescore queued", "ok");
                setTimeout(() => renderIcp(), 2500);
              }
            },
          },
          "↻ Rescore all",
        ),
      ]),
    ]),
  );

  // Tabs
  const tabBtnIcp = (id, label, cb) =>
    el(
      "button",
      {
        class: `tab${icpState.tab === id ? " active" : ""}`,
        type: "button",
        onclick: cb,
      },
      label,
    );

  wrap.appendChild(
    el("div", { class: "tabs" }, [
      tabBtnIcp("overview", "Overview", () => {
        icpState.tab = "overview";
        renderIcp();
      }),
      tabBtnIcp("score", "Score a company", () => {
        icpState.tab = "score";
        renderIcp();
      }),
      tabBtnIcp("config", "My ICP config", () => {
        icpState.tab = "config";
        renderIcp();
      }),
    ]),
  );

  const body = el("div", { class: "icp-body" });
  wrap.appendChild(body);
  content.appendChild(wrap);

  if (icpState.tab === "overview") await renderIcpOverview(body);
  else if (icpState.tab === "score") renderIcpScoreWizard(body);
  else if (icpState.tab === "config") renderIcpConfigEditor(body);
}

async function renderIcpOverview(body) {
  body.appendChild(el("div", { class: "loading" }, "Loading overview…"));

  // Load status + top companies
  const [status, top] = await Promise.all([
    icpState.status ? Promise.resolve(icpState.status) : api("icp-status"),
    api("icp-companies-list", { queryString: "?band=abm&limit=10" }),
  ]);
  if (status?.error) {
    body.innerHTML = `<div class="empty-state"><div class="empty-state-title">Error</div><div class="empty-state-hint">${escapeHtml(status.error)}</div></div>`;
    return;
  }
  icpState.status = status;

  body.innerHTML = "";

  // Histogram
  const histMap = Object.fromEntries((status.histogram || []).map((h) => [h.band, h.count]));
  const totalScored = status.scored_companies || 0;
  const totalAll = status.total_companies || 0;

  const histWrap = el("div", { class: "wizard-section" }, [
    el("div", { class: "wizard-section-title" }, "Portfolio by band"),
  ]);
  for (const band of ICP_BAND_ORDER) {
    const count = histMap[band] || 0;
    const pct = totalScored > 0 ? Math.round((count / totalScored) * 100) : 0;
    const info = ICP_BAND_BADGE[band];
    const bandCopy = (status.band_copy && status.band_copy[band]) || {};
    const row = el(
      "div",
      { class: "filter-row", style: "align-items:center;gap:12px;margin-bottom:8px;" },
      [
        el("div", { style: "min-width:90px;" }, [renderIcpBandBadge(band)]),
        el("div", { class: "score-bar-wrap", style: "flex:1;" }, [
          el("div", { class: "score-bar" }, [
            el("div", {
              class: `score-bar-fill score-${band === "abm" ? "hot" : band === "outbound" ? "high" : band === "inbound" ? "mid" : "low"}`,
              style: `width:${pct}%;`,
            }),
          ]),
          el("span", { class: "score-num" }, `${count}`),
        ]),
        el("div", { class: "muted small", style: "min-width:220px;" }, bandCopy.blurb || ""),
      ],
    );
    histWrap.appendChild(row);
  }
  const unscored = histMap.unscored || 0;
  if (unscored > 0) {
    histWrap.appendChild(
      el(
        "div",
        { class: "muted small", style: "margin-top:8px;" },
        `${unscored} unscored companies (${totalAll - totalScored} total pending). Save an ICP config to score them.`,
      ),
    );
  }
  histWrap.appendChild(
    el(
      "div",
      { class: "muted small", style: "margin-top:8px;" },
      `Empirical confidence: ${Math.round((status.empirical_confidence || 0) * 100)}% ` +
        `· last run: ${status.last_scored_at ? fmtDate(status.last_scored_at) : "never"}`,
    ),
  );

  body.appendChild(histWrap);

  // Top 10 ABM list
  const topList = top?.companies || [];
  const topWrap = el("div", { class: "wizard-section" }, [
    el("div", { class: "wizard-section-title" }, `Top-10 ABM targets`),
  ]);
  if (topList.length === 0) {
    topWrap.appendChild(
      el(
        "div",
        { class: "muted small" },
        "No ABM-tier companies yet. Set your ICP config → rescore.",
      ),
    );
  } else {
    const tbl = el("table", { class: "table" });
    tbl.appendChild(
      el("thead", {}, [
        el("tr", {}, [
          el("th", {}, "#"),
          el("th", {}, "Company"),
          el("th", {}, "Sector"),
          el("th", {}, "Score"),
          el("th", {}, "Band"),
          el("th", {}, "Scored"),
        ]),
      ]),
    );
    const tbody = el("tbody", {});
    topList.forEach((c, i) => {
      tbody.appendChild(
        el(
          "tr",
          {
            style: "cursor:pointer;",
            onclick: () => {
              icpState.tab = "score";
              icpState.editingCompany = c;
              icpState.adhoc = {};
              icpState.adhocResult = null;
              renderIcp();
            },
          },
          [
            el("td", { class: "muted mono" }, String(i + 1).padStart(2, "0")),
            el("td", {}, el("span", { class: "primary-cell" }, c.name || "—")),
            el("td", {}, c.sector ? el("span", { class: "badge badge-purple" }, c.sector) : "—"),
            el("td", {}, [renderIcpScoreBar10(c.icp_score, c.icp_band)]),
            el("td", {}, [renderIcpBandBadge(c.icp_band)]),
            el(
              "td",
              { class: "mono muted small" },
              c.icp_scored_at ? fmtDate(c.icp_scored_at) : "—",
            ),
          ],
        ),
      );
    });
    tbl.appendChild(tbody);
    topWrap.appendChild(tbl);
  }
  body.appendChild(topWrap);
}

// ═════════════════════════════════════════════════════════════════
// 🛰 Live System Map (v2) — situational awareness at a glance
// ═════════════════════════════════════════════════════════════════
// Layout (top → bottom):
//   1. Hero strip       — 5 KPI cards with sparklines
//   2. Pipeline flow    — Sent → Replied → Meetings → Deals → Won w/ drop-off %
//   3. Platform lanes   — one row per channel: accounts, sent/cap, last inbound, campaigns
//   4. Three columns:
//        a. Automations  — Running NOW / Failed / Stale callouts on top, then categorized
//        b. Event stream — events/min sparkline + filtered scrollable feed
//        c. Queues       — horizontal stacked bars + active campaigns
// All polled every 5s; pausable. Trend deltas computed client-side from history snapshots.

const liveMapState = {
  data: null,
  history: [], // ring buffer of past snapshots for trends
  pollHandle: null,
  paused: false,
  lastFetchAt: null,
  eventFilter: "all",
  expandedCategories: new Set(),
  expandedJob: null,
  density: "comfortable", // 'comfortable' | 'compact'
  timeRangeSec: 86_400, // 300 / 3600 / 86_400 / 604_800 / 0 (= all)
  expandedLane: null, // 'x' | 'linkedin' | 'telegram' | 'email' — opens drawer
  notifiedEventIds: new Set(), // debounce: only toast each event once
  dismissedInsights: new Set(), // user-dismissed insight IDs (session-only)
  triggeringJob: null, // name of job currently being run-now
  homeData: null, // dashboard-home snapshot (budget, today, lifecycle)
  soundEnabled: false, // opt-in audio cue for high-priority events
  commandPalette: {
    open: false,
    query: "",
    results: [],
    selectedIndex: 0,
    searchHandle: null, // debounce timer
  },
  notificationLog: [], // ring buffer of past 50 toasted events
  showNotifLog: false, // bell popover open?
  // ── round 6 additions ────────────
  savedViews: [], // user-defined view presets (name + state snapshot)
  showViewMenu: false, // 📌 Views dropdown open?
  pinnedEvents: [], // up to 10 pinned event IDs (manual pin from ⋯ menu)
  mutedKinds: new Set(), // event kinds the user has muted
  mutedPlatforms: new Set(), // event platforms the user has muted
  eventRowMenuOpen: null, // event id with the ⋯ menu currently open
  // ── round 7 additions ────────────
  aiBriefing: null, // { headline, briefing, generatedAt, ttlSec }
  aiBriefingLoading: false,
  briefingCollapsed: false,
};

// Keys we persist across page reloads (via chrome.storage.local).
const PERSISTED_KEYS = ["density", "timeRangeSec", "eventFilter", "soundEnabled"];

async function loadLiveMapPrefs() {
  try {
    const stored = await chrome.storage.local.get([
      "_liveMapPrefs",
      "_liveMapNotifLog",
      "_liveMapViews",
      "_liveMapPinned",
      "_liveMapMuted",
    ]);
    if (stored._liveMapPrefs) {
      for (const k of PERSISTED_KEYS) {
        if (stored._liveMapPrefs[k] !== undefined) liveMapState[k] = stored._liveMapPrefs[k];
      }
    }
    if (Array.isArray(stored._liveMapNotifLog)) {
      liveMapState.notificationLog = stored._liveMapNotifLog;
      for (const n of stored._liveMapNotifLog) liveMapState.notifiedEventIds.add(n.evId);
    }
    if (Array.isArray(stored._liveMapViews)) liveMapState.savedViews = stored._liveMapViews;
    if (Array.isArray(stored._liveMapPinned)) liveMapState.pinnedEvents = stored._liveMapPinned;
    if (stored._liveMapMuted) {
      liveMapState.mutedKinds = new Set(stored._liveMapMuted.kinds || []);
      liveMapState.mutedPlatforms = new Set(stored._liveMapMuted.platforms || []);
    }
  } catch {
    /* storage unavailable — use defaults */
  }
}

function persistLiveMapPrefs() {
  try {
    const prefs = {};
    for (const k of PERSISTED_KEYS) prefs[k] = liveMapState[k];
    chrome.storage.local.set({ _liveMapPrefs: prefs });
  } catch {
    /* ignore */
  }
}

function persistNotifLog() {
  try {
    chrome.storage.local.set({ _liveMapNotifLog: liveMapState.notificationLog.slice(-50) });
  } catch {
    /* ignore */
  }
}

function persistViews() {
  try {
    chrome.storage.local.set({ _liveMapViews: liveMapState.savedViews });
  } catch {}
}

function persistPinnedAndMuted() {
  try {
    chrome.storage.local.set({
      _liveMapPinned: liveMapState.pinnedEvents,
      _liveMapMuted: {
        kinds: [...liveMapState.mutedKinds],
        platforms: [...liveMapState.mutedPlatforms],
      },
    });
  } catch {}
}

const HISTORY_MAX = 24; // 24 snapshots × 5s = 2 min of trend data
const NOTIFY_KINDS = new Set(["reply_received", "inbound", "deal_won", "signal"]);
const TIME_RANGES = [
  ["5m", 300],
  ["1h", 3_600],
  ["24h", 86_400],
  ["7d", 604_800],
  ["All", 0],
];

const CATEGORY_META = {
  outreach: { icon: "megaphone", color: "var(--accent)" },
  listening: { icon: "signal", color: "var(--blue)" },
  icp: { icon: "target", color: "var(--accent)" },
  enrichment: { icon: "bolt", color: "var(--yellow)" },
  email: { icon: "mail", color: "var(--blue)" },
  briefing: { icon: "chart", color: "var(--text-3)" },
  cleanup: { icon: "trash", color: "var(--text-4)" },
  intelligence: { icon: "search", color: "var(--accent)" },
  scraping: { icon: "globe", color: "var(--text-3)" },
  uncategorized: { icon: "•", color: "var(--text-4)" },
};

const KIND_COLOR = {
  outreach_sent: "var(--blue)",
  reply_received: "var(--green)",
  inbound: "var(--green)",
  signal: "var(--yellow)",
  deal: "var(--accent)",
  deal_won: "var(--green)",
  deal_lost: "var(--red)",
  enrichment: "var(--text-3)",
  meeting: "var(--accent)",
  event: "var(--text-4)",
};

// Format a minutes interval as "every 15 min" / "every 2h" / "every 3 days".
function fmtIntervalMin(mins) {
  const m = Number(mins);
  if (!Number.isFinite(m) || m <= 0) return "";
  if (m < 60) return `every ${m} min`;
  if (m < 1440) {
    const h = Math.round(m / 60);
    return `every ${h}h`;
  }
  const days = Math.round(m / 1440);
  return `every ${days} day${days === 1 ? "" : "s"}`;
}

function fmtRel(iso) {
  if (!iso) return "—";
  const ts = Date.parse(iso);
  if (!ts) return "—";
  const sec = Math.round((Date.now() - ts) / 1000);
  if (sec < 0) return `in ${Math.abs(sec)}s`;
  if (sec < 60) return `${sec}s ago`;
  if (sec < 3600) return `${Math.round(sec / 60)}m ago`;
  if (sec < 86_400) return `${Math.round(sec / 3600)}h ago`;
  return `${Math.round(sec / 86_400)}d ago`;
}

function fmtRelFuture(iso) {
  if (!iso) return "—";
  const ts = Date.parse(iso);
  if (!ts) return "—";
  const sec = Math.round((ts - Date.now()) / 1000);
  if (sec <= 0) return "due now";
  if (sec < 60) return `in ${sec}s`;
  if (sec < 3600) return `in ${Math.round(sec / 60)}m`;
  if (sec < 86_400) return `in ${Math.round(sec / 3600)}h`;
  return `in ${Math.round(sec / 86_400)}d`;
}

// ── INSIGHTS / ALERTS — auto-detected anomalies and "what needs attention" ──
function computeInsights(data) {
  const insights = [];
  const events = data.recentEvents || [];
  const jobs = data.jobs || [];
  const queues = data.queues || {};
  const accounts = data.accounts || {};
  const funnel = data.funnel || {};

  const now = Date.now();
  const oneHourAgo = now - 3_600_000;
  const dayAgo = now - 86_400_000;

  // 1. Recent failed jobs
  const failed = jobs.filter(
    (j) => j.lastError && j.lastRunAt && Date.parse(j.lastRunAt) > oneHourAgo,
  );
  if (failed.length > 0) {
    insights.push({
      id: "failed-jobs",
      level: "high",
      icon: "alert",
      title: `${failed.length} job${failed.length > 1 ? "s" : ""} failed in the last hour`,
      detail:
        failed
          .slice(0, 3)
          .map((j) => j.name)
          .join(", ") + (failed.length > 3 ? `, +${failed.length - 3}` : ""),
    });
  }

  // 2. Enrichment queue growing
  const enrichTrend = trendFromHistory((s) => s.enrichPending);
  if (enrichTrend != null && enrichTrend > 5) {
    insights.push({
      id: "enrich-growing",
      level: "medium",
      icon: "trend",
      title: `Enrichment queue growing — +${enrichTrend} in last ${liveMapState.history.length * 5}s`,
      detail: `${queues.enrichment?.pending || 0} pending now. Consider triggering icp-signal-enrich manually.`,
    });
  }

  // 3. Account near daily cap
  const allAccounts = [
    ...(accounts.x || []).map((a) => ({ ...a, _platform: "X" })),
    ...(accounts.linkedin || []).map((a) => ({ ...a, _platform: "LinkedIn" })),
    ...(accounts.telegram || []).map((a) => ({ ...a, _platform: "Telegram" })),
    ...(accounts.email || []).map((a) => ({ ...a, _platform: "Email" })),
  ];
  for (const a of allAccounts) {
    const sent = a.outreach_sent_today || a.daily_actions_count || 0;
    const cap = a.max_daily_outreach || a.daily_actions || 0;
    if (cap > 0 && sent / cap >= 0.9) {
      insights.push({
        id: `cap-${a.id}-${a._platform}`,
        level: "medium",
        icon: "signal",
        title: `${a._platform} account near cap`,
        detail: `${a.account_identifier || a.identifier || a.email || `acct-${a.id}`} at ${sent}/${cap} (${Math.round((sent / cap) * 100)}%)`,
      });
    }
    if ((a.status || "").toLowerCase() === "rate_limited") {
      insights.push({
        id: `rl-${a.id}-${a._platform}`,
        level: "high",
        icon: "stop",
        title: `${a._platform} account rate-limited`,
        detail: `${a.account_identifier || a.identifier || a.email || `acct-${a.id}`} — sends paused`,
      });
    }
    if ((a.status || "").toLowerCase() === "logged_out") {
      insights.push({
        id: `lo-${a.id}-${a._platform}`,
        level: "high",
        icon: "lock",
        title: `${a._platform} account logged out`,
        detail: `${a.account_identifier || a.identifier || a.email || `acct-${a.id}`} — re-sync session`,
      });
    }
  }

  // 4. Recent high-priority inbound (replies in last hour)
  const recentReplies = events.filter(
    (e) => (e.kind === "reply_received" || e.kind === "inbound") && Date.parse(e.ts) > oneHourAgo,
  );
  if (recentReplies.length >= 3) {
    insights.push({
      id: "replies-hot",
      level: "good",
      icon: "flame",
      title: `${recentReplies.length} replies in the last hour`,
      detail: `Inbox is hot — respond fast to keep momentum.`,
      drillTo: { type: "route", route: "inbox" },
    });
  }

  // 5. New signals worth pursuing
  const newSignals = events.filter((e) => e.kind === "signal" && Date.parse(e.ts) > oneHourAgo);
  if (newSignals.length > 0) {
    insights.push({
      id: "new-signals",
      level: "good",
      icon: "target",
      title: `${newSignals.length} new buying signal${newSignals.length > 1 ? "s" : ""}`,
      detail: newSignals[0].who
        ? `Latest: ${newSignals[0].who}`
        : "Review and pursue before they cool off.",
      drillTo: { type: "route", route: "signals" },
    });
  }

  // 6. No inbound on a platform for 24h+ (per platform)
  for (const platform of ["x", "linkedin", "telegram", "email"]) {
    const matches = (e) => {
      const p = (e.platform || "").toLowerCase();
      if (platform === "x") return p === "x" || p === "twitter";
      return p === platform;
    };
    const lastInbound = events.find(
      (e) => matches(e) && (e.kind === "reply_received" || e.kind === "inbound"),
    );
    const platformAccountCount = (accounts[platform] || []).length;
    if (platformAccountCount === 0) continue; // skip platforms with no accounts
    if (!lastInbound) {
      insights.push({
        id: `no-in-${platform}`,
        level: "low",
        icon: "pause",
        title: `No ${platform} inbound in 2 days`,
        detail: `Channel is quiet — outbound may need refreshing.`,
      });
    }
  }

  // 7. Deals about to close (high probability, expected close <7d)
  // (we don't have this in the snapshot; leaving as placeholder for future)

  // Filter dismissed
  return insights.filter((i) => !liveMapState.dismissedInsights.has(i.id));
}

function renderLiveMapInsights(insights) {
  const colors = {
    high: "var(--red)",
    medium: "var(--yellow)",
    low: "var(--text-3)",
    good: "var(--green)",
  };
  return el("div", { class: "card", style: "padding:10px;" }, [
    el(
      "div",
      { style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;" },
      [
        el(
          "div",
          {
            class: "muted",
            style: "font-size:10px;text-transform:uppercase;letter-spacing:0.06em;",
          },
          `Needs your attention (${insights.length})`,
        ),
        el(
          "button",
          {
            type: "button",
            class: "btn btn-tiny btn-secondary",
            onclick: () => {
              for (const ins of insights) liveMapState.dismissedInsights.add(ins.id);
              renderLiveMap();
            },
          },
          "Dismiss all",
        ),
      ],
    ),
    el(
      "div",
      { style: "display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:6px;" },
      insights.map((ins) =>
        el(
          "div",
          {
            style: `padding:8px;background:var(--bg-2);border-radius:6px;border-left:3px solid ${colors[ins.level] || "var(--text-4)"};display:flex;gap:8px;align-items:flex-start;cursor:${ins.drillTo ? "pointer" : "default"};`,
            onclick: (e) => {
              if (e.target.closest(".insight-dismiss")) return;
              if (!ins.drillTo) return;
              if (ins.drillTo.type === "route") navigate(ins.drillTo.route);
            },
          },
          [
            el("span", { style: "font-size:16px;line-height:1;flex-shrink:0;" }, ins.icon),
            el("div", { style: "flex:1;min-width:0;" }, [
              el("div", { style: "font-size:12px;font-weight:600;line-height:1.3;" }, ins.title),
              ins.detail
                ? el(
                    "div",
                    { class: "muted small", style: "margin-top:2px;line-height:1.3;" },
                    ins.detail,
                  )
                : null,
            ]),
            el(
              "button",
              {
                type: "button",
                class: "insight-dismiss",
                style:
                  "background:none;border:none;color:var(--text-4);cursor:pointer;font-size:14px;padding:0 4px;",
                onclick: (e) => {
                  e.stopPropagation();
                  liveMapState.dismissedInsights.add(ins.id);
                  renderLiveMap();
                },
              },
              "×",
            ),
          ],
        ),
      ),
    ),
  ]);
}

// Maintain a small ring buffer of past snapshots so we can compute trend deltas + sparklines.
function pushHistory(snap) {
  liveMapState.history.push({
    at: Date.now(),
    eventCount: (snap.recentEvents || []).length,
    enrichPending: snap.queues?.enrichment?.pending || 0,
    seqActive: snap.queues?.sequences?.active || 0,
    running: snap.heartbeat?.running || 0,
  });
  if (liveMapState.history.length > HISTORY_MAX) liveMapState.history.shift();
}

// ── Topbar (heartbeat + pause/refresh + time range + density) ───
function renderLiveMapTopbar(data) {
  const hb = data.heartbeat || {};
  const home = liveMapState.homeData || {};
  const budget = home.budget || {};
  const budgetUsed = budget.utilization || 0;
  const budgetColor =
    budgetUsed >= 90 ? "var(--red)" : budgetUsed >= 70 ? "var(--yellow)" : "var(--green)";

  return el("div", { class: "page-header", style: "margin-bottom:0;flex-wrap:wrap;gap:10px;" }, [
    el("div", {}, [
      el("div", { class: "page-title", style: "display:flex;align-items:center;gap:10px;" }, [
        el("span", {}, "Dashboard"),
        renderSystemMoodRing(data),
      ]),
      el("div", { class: "page-subtitle" }, [
        el(
          "span",
          { class: "muted small" },
          `${hb.registered || 0} jobs · uptime ${Math.floor((hb.uptimeSec || 0) / 60)}min · last poll ${fmtRel(liveMapState.lastFetchAt)}`,
        ),
        budget.limit
          ? el(
              "span",
              {
                class: "badge",
                style: `background:${budgetColor}22;color:${budgetColor};margin-left:8px;font-size:10px;`,
                title: `Daily budget: ${budget.used || 0} / ${budget.limit || 0} (${budget.tier || "?"})`,
              },
              `Budget ${budgetUsed}%`,
            )
          : null,
      ]),
    ]),
    el("div", { style: "display:flex;gap:6px;align-items:center;flex-wrap:wrap;" }, [
      // Quick actions
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny btn-primary",
          title: "Add a new deal",
          onclick: () => openNewDealDrawer(),
        },
        "+ Deal",
      ),
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny btn-secondary",
          title: "Add a new contact to CRM",
          onclick: () => openNewContactDrawer(),
        },
        "+ Contact",
      ),
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny btn-secondary",
          title: "Add a new company to CRM",
          onclick: () => openNewCompanyDrawer(),
        },
        "+ Company",
      ),
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny btn-primary",
          title: "Create a new campaign",
          onclick: () => navigate("operations"),
        },
        "+ Campaign",
      ),
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny btn-secondary",
          title: "Search anything (Ctrl+K)",
          onclick: () => openCommandPalette(),
        },
        "Search",
      ),
      // 📌 Saved views dropdown
      renderSavedViewsButton(),
      el("span", { style: "width:1px;background:var(--bg-3);margin:0 6px;align-self:stretch;" }),
      // Time range chips
      el("div", { class: "muted small", style: "margin-right:4px;" }, "Range"),
      ...TIME_RANGES.map(([label, sec]) =>
        el(
          "button",
          {
            type: "button",
            class: `btn btn-tiny ${liveMapState.timeRangeSec === sec ? "btn-primary" : "btn-secondary"}`,
            onclick: () => {
              liveMapState.timeRangeSec = sec;
              persistLiveMapPrefs();
              renderLiveMap();
            },
          },
          label,
        ),
      ),
      el("span", { style: "width:1px;background:var(--bg-3);margin:0 6px;align-self:stretch;" }),
      // Density toggle
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny btn-secondary",
          title: "Toggle density",
          onclick: () => {
            liveMapState.density = liveMapState.density === "compact" ? "comfortable" : "compact";
            persistLiveMapPrefs();
            renderLiveMap();
          },
        },
        liveMapState.density === "compact" ? "▤ Compact" : "▦ Comfy",
      ),
      // Sound toggle
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny btn-secondary",
          title: "Toggle sound notifications for replies / deals / signals",
          onclick: () => {
            liveMapState.soundEnabled = !liveMapState.soundEnabled;
            if (liveMapState.soundEnabled) playNotificationSound("test"); // confirm with a chirp
            persistLiveMapPrefs();
            renderLiveMap();
          },
        },
        liveMapState.soundEnabled ? "Sound" : "Mute",
      ),
      // 🔔 Notifications history bell — shows count of recent notifications
      renderNotifBellButton(),
      // 🚨 Emergency stop — pauses ALL active campaigns in one shot
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny",
          style:
            "background:var(--red-soft);color:var(--red);border:1px solid var(--red);font-weight:600;",
          title: "Pause every active campaign across outreach + email + DM",
          onclick: async () => {
            const ok = confirm(
              "Emergency Stop\n\n" +
                "This will pause EVERY active campaign across outreach, email, and DM.\n" +
                "You can resume them individually afterwards from the dashboard.\n\n" +
                "Proceed?",
            );
            if (!ok) return;
            const r = await api("emergency-stop").catch((e) => ({ error: e.message }));
            if (r?.error) toast(`${r.error}`, "err");
            else {
              const p = r.paused || {};
              toast(
                `Stopped — outreach ${p.outreach || 0} · email ${p.email || 0} · DM ${p.dm || 0}`,
                "ok",
                8000,
              );
              liveMapState.data = null;
              renderLiveMap();
            }
          },
        },
        "Stop all",
      ),
      // Live / pause
      el(
        "span",
        { class: liveMapState.paused ? "badge badge-yellow" : "badge badge-green" },
        liveMapState.paused ? "⏸ paused" : "live",
      ),
      el(
        "button",
        {
          class: "btn btn-tiny btn-secondary",
          type: "button",
          onclick: () => {
            liveMapState.paused = !liveMapState.paused;
            renderLiveMap();
          },
        },
        liveMapState.paused ? "▶ Resume" : "⏸ Pause",
      ),
      el(
        "button",
        {
          class: "btn btn-tiny btn-secondary",
          type: "button",
          onclick: async () => {
            liveMapState.data = null;
            await renderLiveMap();
          },
        },
        "↻",
      ),
    ]),
  ]);
}

// Filter events by current time range. timeRangeSec=0 means "all".
function eventsInRange(events) {
  const sec = liveMapState.timeRangeSec || 0;
  if (sec === 0) return events;
  const cutoff = Date.now() - sec * 1000;
  return events.filter((e) => Date.parse(e.ts) >= cutoff);
}

// ── HERO STRIP — 5 KPI cards with sparklines ────────────────────
function renderLiveMapHeroStrip(data) {
  const events = eventsInRange(data.recentEvents || []);
  const hb = data.heartbeat || {};
  const queues = data.queues || {};
  const accounts = data.accounts || {};
  const home = liveMapState.homeData || {};
  const today = home.todayActions || {};

  // events/hour: bin recent events into 60 1-min buckets
  const epm = computeEventsPerMinute(events, 60);
  const eventsLastHour = epm.reduce((s, n) => s + n, 0);
  const eventsPrev = epm.slice(0, 30).reduce((s, n) => s + n, 0);
  const eventsCurr = epm.slice(30).reduce((s, n) => s + n, 0);
  const eventsTrend =
    eventsPrev === 0
      ? null
      : Math.round(((eventsCurr - eventsPrev) / Math.max(eventsPrev, 1)) * 100);

  // replies (last 24h) — kind in {reply_received, inbound}
  const replies24h = events.filter(
    (e) =>
      (e.kind === "reply_received" || e.kind === "inbound") &&
      Date.parse(e.ts) > Date.now() - 86_400_000,
  ).length;

  // queue trend from history
  const queueTrend = trendFromHistory((s) => s.enrichPending);
  const queueDelta =
    queueTrend === null
      ? ""
      : queueTrend > 0
        ? `+${queueTrend} ▲`
        : queueTrend < 0
          ? `${queueTrend} ▼`
          : "0 ─";

  // Today: total actions across all channels (from dashboard-home)
  const todayTotal = today.total || 0;
  const todayBreakdown = `TG ${today.telegram || 0} · LI ${today.linkedin || 0} · X ${today.twitter || 0} · ✉ ${today.email || 0}`;

  // Click handlers: each KPI drills into the most relevant view-state.
  const drillEvents = () => {
    liveMapState.timeRangeSec = 3_600;
    liveMapState.eventFilter = "all";
    persistLiveMapPrefs();
    renderLiveMap();
    $("#topbar-title")?.scrollIntoView({ behavior: "smooth" });
  };
  const drillRunning = () => {
    /* scroll to automations column */
    const card = document.querySelector(".live-map-cron-anchor");
    card?.scrollIntoView({ behavior: "smooth", block: "start" });
  };
  const drillReplies = () => {
    liveMapState.timeRangeSec = 86_400;
    liveMapState.eventFilter = "inbound";
    persistLiveMapPrefs();
    renderLiveMap();
  };
  const drillEnrich = () => {
    liveMapState.eventFilter = "enrichment";
    persistLiveMapPrefs();
    renderLiveMap();
  };
  const drillToday = () => {
    liveMapState.timeRangeSec = 86_400;
    liveMapState.eventFilter = "outreach_sent";
    persistLiveMapPrefs();
    renderLiveMap();
  };

  return el("div", { style: "display:grid;grid-template-columns:repeat(5,1fr);gap:10px;" }, [
    kpiCard(
      "Events / hour",
      String(eventsLastHour),
      eventsTrend,
      sparkSvg(epm, "var(--accent)"),
      eventsLastHour === 0 ? "muted" : "active",
      drillEvents,
      "Show all events from the last hour",
    ),
    kpiCard(
      "Running now",
      String(hb.running || 0),
      null,
      runningPulseSvg(hb.running || 0),
      hb.running > 0 ? "active" : "muted",
      drillRunning,
      "Scroll to the cron job list",
    ),
    kpiCard(
      "Replies (24h)",
      String(replies24h),
      null,
      sparkSvg(computeRepliesPerHour(events, 24), "var(--green)"),
      replies24h > 0 ? "good" : "muted",
      drillReplies,
      "Filter event stream to inbound / replies only",
    ),
    kpiCard(
      "Enrich queue",
      String(queues.enrichment?.pending || 0),
      queueDelta || null,
      sparkSvg(
        historySeries((s) => s.enrichPending),
        "var(--yellow)",
      ),
      (queues.enrichment?.pending || 0) === 0 ? "good" : queueTrend > 0 ? "warn" : "active",
      drillEnrich,
      "Filter event stream to enrichment events",
    ),
    kpiCardWithSubtitle(
      "Today's actions",
      String(todayTotal),
      todayBreakdown,
      todayTotal > 0 ? "active" : "muted",
      drillToday,
      "Filter event stream to outbound sends (24h)",
    ),
  ]);
}

// Variant of kpiCard that shows a subtitle line instead of a sparkline (when the value is one
// composite number that decomposes naturally — e.g. today's actions split by channel).
function kpiCardWithSubtitle(label, value, subtitle, status, onClick, title) {
  const valueColor =
    status === "bad"
      ? "var(--red)"
      : status === "warn"
        ? "var(--yellow)"
        : status === "good"
          ? "var(--green)"
          : status === "active"
            ? "var(--accent)"
            : "var(--text-1)";
  const interactive = typeof onClick === "function";
  return el(
    "div",
    {
      class: "card",
      style: `padding:12px;display:flex;flex-direction:column;gap:4px;min-height:88px;${interactive ? "cursor:pointer;transition:background 0.1s;" : ""}`,
      title: title || "",
      onclick: interactive ? onClick : undefined,
      onmouseenter: interactive
        ? (e) => {
            e.currentTarget.style.background = "var(--bg-2)";
          }
        : undefined,
      onmouseleave: interactive
        ? (e) => {
            e.currentTarget.style.background = "";
          }
        : undefined,
    },
    [
      el(
        "div",
        { class: "muted", style: "font-size:10px;text-transform:uppercase;letter-spacing:0.06em;" },
        label,
      ),
      el(
        "div",
        { style: `font-size:24px;font-weight:700;color:${valueColor};line-height:1;` },
        value,
      ),
      el("div", { class: "muted small mono", style: "margin-top:auto;font-size:10px;" }, subtitle),
    ],
  );
}

function kpiCard(label, value, delta, sparkOrSvgHtml, status, onClick, title) {
  const valueColor =
    status === "bad"
      ? "var(--red)"
      : status === "warn"
        ? "var(--yellow)"
        : status === "good"
          ? "var(--green)"
          : status === "active"
            ? "var(--accent)"
            : "var(--text-1)";
  const interactive = typeof onClick === "function";
  return el(
    "div",
    {
      class: "card",
      style: `padding:12px;display:flex;flex-direction:column;gap:4px;min-height:88px;${interactive ? "cursor:pointer;transition:background 0.1s;" : ""}`,
      title: title || "",
      onclick: interactive ? onClick : undefined,
      onmouseenter: interactive
        ? (e) => {
            e.currentTarget.style.background = "var(--bg-2)";
          }
        : undefined,
      onmouseleave: interactive
        ? (e) => {
            e.currentTarget.style.background = "";
          }
        : undefined,
    },
    [
      el(
        "div",
        { class: "muted", style: "font-size:10px;text-transform:uppercase;letter-spacing:0.06em;" },
        label,
      ),
      el("div", { style: "display:flex;align-items:baseline;gap:8px;" }, [
        el(
          "div",
          { style: `font-size:24px;font-weight:700;color:${valueColor};line-height:1;` },
          value,
        ),
        delta
          ? el(
              "div",
              {
                class: "muted small",
                style: `color:${String(delta).includes("▲") ? "var(--green)" : String(delta).includes("▼") ? "var(--red)" : "var(--text-4)"};font-weight:600;`,
              },
              delta,
            )
          : null,
      ]),
      el("div", { style: "margin-top:auto;height:24px;", html: sparkOrSvgHtml || "" }),
    ],
  );
}

// ── PIPELINE STRIP — two complementary funnels side by side ─────
//   Activity (last 7d, event-derived):   Sent → Replied → Meetings → Deals → Won
//   Lifecycle (current state, contact-stage): Lead → Contacted → Replied → Warm → Hot → Converted
// Activity reveals conversion velocity. Lifecycle reveals current pipeline shape.
function renderLiveMapPipelineStrip(data) {
  const f = data.funnel || {};
  const home = liveMapState.homeData || {};

  const activityStages = [
    { label: "Sent", value: f.sent || 0, color: "var(--text-3)" },
    { label: "Replied", value: f.replied || 0, color: "var(--blue)" },
    { label: "Meetings", value: f.meetings || 0, color: "var(--accent)" },
    { label: "Deals", value: f.deals || 0, color: "var(--accent)" },
    { label: "Won", value: f.won || 0, color: "var(--green)" },
  ];

  // Lifecycle from dashboard-home — counts of contacts at each conversation_stage
  const cf = home.funnel || {};
  const lifecycleStages = [
    { label: "Lead", value: (cf.unknown || 0) + (cf.cold || 0), color: "var(--text-3)" },
    { label: "Contacted", value: cf.contacted || 0, color: "var(--blue)" },
    { label: "Replied", value: cf.replied || 0, color: "var(--blue)" },
    { label: "Warm", value: cf.warm || 0, color: "var(--yellow)" },
    { label: "Hot", value: cf.hot || 0, color: "var(--accent)" },
    { label: "Converted", value: cf.customer || 0, color: "var(--green)" },
  ];

  const renderFunnel = (title, subtitle, stages) =>
    el("div", { class: "card", style: "padding:12px;" }, [
      el(
        "div",
        {
          style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;",
        },
        [
          el(
            "div",
            {
              class: "muted",
              style: "font-size:10px;text-transform:uppercase;letter-spacing:0.06em;",
            },
            title,
          ),
          el("div", { class: "muted small" }, subtitle),
        ],
      ),
      el(
        "div",
        { style: "display:flex;align-items:center;gap:0;" },
        stages.flatMap((s, i) => {
          const out = [renderPipelineNode(s, stages[0].value)];
          if (i < stages.length - 1) {
            const next = stages[i + 1];
            const dropPct = s.value > 0 ? Math.round((next.value / s.value) * 100) : 0;
            out.push(renderPipelineArrow(dropPct));
          }
          return out;
        }),
      ),
    ]);

  return el("div", { style: "display:grid;grid-template-columns:1fr 1fr;gap:12px;" }, [
    renderFunnel(
      "Activity · last 7 days",
      `Reply ${f.replyRate || 0}% · Deal ${f.dealRate || 0}% · Win ${f.winRate || 0}%`,
      activityStages,
    ),
    renderFunnel(
      "Contact lifecycle · current",
      `${fmtNum(home.funnelTotal || 0)} contacts in pipeline`,
      lifecycleStages,
    ),
  ]);
}

function renderPipelineNode(stage, maxValue) {
  const widthPct = maxValue > 0 ? Math.max(15, Math.round((stage.value / maxValue) * 100)) : 15;
  return el("div", { style: "flex:1;text-align:center;padding:0 4px;" }, [
    el(
      "div",
      { style: "font-size:20px;font-weight:700;color:var(--text-1);line-height:1;" },
      String(stage.value),
    ),
    el("div", { class: "muted", style: "font-size:11px;margin:4px 0;" }, stage.label),
    el("div", { style: "height:4px;background:var(--bg-3);border-radius:2px;overflow:hidden;" }, [
      el("div", { style: `height:100%;background:${stage.color};width:${widthPct}%;` }),
    ]),
  ]);
}

function renderPipelineArrow(dropPct) {
  const color = dropPct >= 30 ? "var(--green)" : dropPct >= 10 ? "var(--yellow)" : "var(--red)";
  return el(
    "div",
    {
      style: "display:flex;flex-direction:column;align-items:center;padding:0 8px;min-width:60px;",
    },
    [
      el(
        "div",
        { class: "mono", style: `font-size:10px;color:${color};font-weight:600;` },
        `${dropPct}%`,
      ),
      el("div", { style: "color:var(--text-4);font-size:18px;line-height:1;" }, "→"),
    ],
  );
}

// ── PLATFORM LANES — one row per channel ─────────────────────────
function renderLiveMapPlatformLanes(data) {
  const accounts = data.accounts || {};
  const events = data.recentEvents || [];
  const camps = data.campaigns || {};

  const lanes = [
    {
      key: "x",
      icon: "bird",
      name: "X / Twitter",
      list: accounts.x || [],
      camps: camps.outreach?.active || 0,
    },
    {
      key: "linkedin",
      icon: "link",
      name: "LinkedIn",
      list: accounts.linkedin || [],
      camps: camps.outreach?.active || 0,
    },
    {
      key: "telegram",
      icon: "send",
      name: "Telegram",
      list: accounts.telegram || [],
      camps: camps.dm?.active || 0,
    },
    {
      key: "email",
      icon: "mail",
      name: "Email",
      list: accounts.email || [],
      camps: camps.email?.active || 0,
    },
  ];

  return el("div", { class: "card", style: "padding:10px;" }, [
    el(
      "div",
      {
        class: "muted",
        style: "font-size:10px;text-transform:uppercase;letter-spacing:0.06em;margin-bottom:8px;",
      },
      "Platform health",
    ),
    el(
      "div",
      { style: "display:grid;grid-template-columns:repeat(4,1fr);gap:8px;" },
      lanes.map((lane) => renderPlatformLane(lane, events)),
    ),
  ]);
}

function renderPlatformLane(lane, events) {
  // Aggregate today's send/cap across this platform's accounts
  let sentTotal = 0,
    capTotal = 0;
  const dotColors = [];
  for (const a of lane.list) {
    sentTotal += a.outreach_sent_today || a.daily_actions_count || 0;
    capTotal += a.max_daily_outreach || a.daily_actions || 0;
    const status = (a.status || "").toLowerCase();
    dotColors.push(
      status === "active"
        ? "var(--green)"
        : status === "rate_limited"
          ? "var(--yellow)"
          : "var(--red)",
    );
  }

  // Match platform aliases (twitter → x)
  const matchPlatform = (p) => {
    const pn = (p || "").toLowerCase();
    if (lane.key === "x") return pn === "x" || pn === "twitter";
    return pn === lane.key;
  };
  const platEvents = events.filter((e) => matchPlatform(e.platform));
  const lastInbound = platEvents.find((e) => e.kind === "reply_received" || e.kind === "inbound");
  const lastOutbound = platEvents.find((e) => e.kind === "outreach_sent");

  // Issue detection
  const issues = [];
  if (lane.list.length === 0) issues.push("no accounts");
  else {
    const hasActive = lane.list.some((a) => (a.status || "").toLowerCase() === "active");
    if (!hasActive) issues.push("no active accts");
    if (capTotal > 0 && sentTotal / capTotal >= 0.9) issues.push("near daily cap");
    const noInboundDays = lastInbound
      ? Math.floor((Date.now() - Date.parse(lastInbound.ts)) / 86_400_000)
      : 99;
    if (noInboundDays >= 1) issues.push(`no inbound ${noInboundDays}d`);
  }

  const accentColor = issues.length > 0 ? "var(--red)" : "var(--green)";

  return el(
    "div",
    {
      style: `padding:8px;background:var(--bg-2);border-radius:6px;border-left:3px solid ${accentColor};cursor:pointer;transition:background 0.1s;`,
      onmouseenter: (e) => {
        e.currentTarget.style.background = "var(--bg-3)";
      },
      onmouseleave: (e) => {
        e.currentTarget.style.background = "var(--bg-2)";
      },
      onclick: () => {
        liveMapState.expandedLane = lane.key;
        renderLiveMap();
      },
      title: `Click to expand ${lane.name} deep dive`,
    },
    [
      el("div", { style: "display:flex;align-items:center;gap:6px;margin-bottom:4px;" }, [
        el("span", { style: "font-size:14px;" }, lane.icon),
        el("span", { style: "font-weight:600;font-size:12px;flex:1;" }, lane.name),
        el("span", { class: "muted small mono" }, `${lane.list.length} acct`),
        el("span", { class: "muted small" }, "↗"),
      ]),
      el(
        "div",
        { style: "display:flex;gap:3px;margin-bottom:4px;" },
        dotColors.length > 0
          ? dotColors.map((c) =>
              el("span", { style: `width:8px;height:8px;border-radius:50%;background:${c};` }),
            )
          : [el("span", { class: "muted small" }, "—")],
      ),
      capTotal > 0
        ? el("div", { style: "margin-bottom:4px;" }, [
            el("div", { style: "display:flex;font-size:10px;justify-content:space-between;" }, [
              el("span", { class: "muted" }, "today"),
              el("span", { class: "mono" }, `${sentTotal}/${capTotal}`),
            ]),
            el(
              "div",
              { style: "height:4px;background:var(--bg-3);border-radius:2px;overflow:hidden;" },
              [
                el("div", {
                  style: `height:100%;background:var(--accent);width:${Math.min(100, Math.round((sentTotal / capTotal) * 100))}%;`,
                }),
              ],
            ),
          ])
        : null,
      el("div", { class: "muted", style: "font-size:10px;line-height:1.5;" }, [
        lastInbound ? el("div", {}, `↘ in: ${fmtRel(lastInbound.ts)}`) : el("div", {}, "↘ in: —"),
        lastOutbound
          ? el("div", {}, `↗ out: ${fmtRel(lastOutbound.ts)}`)
          : el("div", {}, "↗ out: —"),
        lane.camps > 0
          ? el(
              "div",
              { style: "color:var(--accent);" },
              `${lane.camps} active campaign${lane.camps > 1 ? "s" : ""}`,
            )
          : null,
      ]),
      issues.length > 0
        ? el(
            "div",
            {
              class: "badge badge-red",
              style: "font-size:9px;margin-top:4px;display:inline-block;",
            },
            `${issues.join(" · ")}`,
          )
        : null,
    ],
  );
}

// ── COLUMN 1 — AUTOMATIONS (smart sections + categorized) ───────
function renderLiveMapAutomations(data) {
  const jobs = data.jobs || [];
  if (jobs.length === 0) {
    return el("div", { class: "card", style: "padding:12px;" }, [
      el(
        "div",
        { class: "muted small" },
        "No jobs registered yet — they'll appear here as soon as the scheduler fires for the first time.",
      ),
    ]);
  }

  const buckets = bucketJobs(jobs);
  const col = el("div", { class: "card", style: "padding:10px;max-height:75vh;overflow-y:auto;" });

  // Smart sections at top
  if (buckets.runningNow.length > 0) {
    col.appendChild(renderJobSection("▶ Running now", buckets.runningNow, "var(--blue)"));
  }
  if (buckets.failed.length > 0) {
    col.appendChild(renderJobSection("Recently failed", buckets.failed, "var(--red)"));
  }
  if (buckets.stale.length > 0) {
    col.appendChild(renderJobSection("⏳ Stale (no run >30min)", buckets.stale, "var(--yellow)"));
  }
  if (buckets.runningNow.length + buckets.failed.length + buckets.stale.length > 0) {
    col.appendChild(
      el(
        "div",
        {
          class: "muted",
          style:
            "font-size:10px;text-transform:uppercase;letter-spacing:0.06em;margin:14px 0 6px;border-top:1px solid var(--bg-3);padding-top:10px;",
        },
        `All ${jobs.length} jobs by category`,
      ),
    );
  }

  // Categorized list — auto-expand any category with active/failed jobs
  const order = [
    "outreach",
    "listening",
    "icp",
    "enrichment",
    "email",
    "intelligence",
    "scraping",
    "briefing",
    "cleanup",
    "uncategorized",
  ];
  for (const cat of order) {
    const list = buckets.byCategory.get(cat);
    if (!list || list.length === 0) continue;
    const hasIssue = list.some((j) => j.isRunning || j.lastError);
    const isOpen =
      liveMapState.expandedCategories.has(cat) ||
      (hasIssue && !liveMapState.expandedCategories.has(`__collapsed:${cat}`));

    const meta = CATEGORY_META[cat] || CATEGORY_META.uncategorized;
    col.appendChild(
      el(
        "div",
        {
          style:
            "display:flex;align-items:center;gap:6px;padding:6px 4px;cursor:pointer;border-bottom:1px solid var(--bg-3);margin-top:4px;",
          onclick: () => {
            if (isOpen) {
              liveMapState.expandedCategories.delete(cat);
              liveMapState.expandedCategories.add(`__collapsed:${cat}`);
            } else {
              liveMapState.expandedCategories.add(cat);
              liveMapState.expandedCategories.delete(`__collapsed:${cat}`);
            }
            renderLiveMap();
          },
        },
        [
          el("span", { style: `color:${meta.color};` }, meta.icon),
          el(
            "span",
            { style: "font-weight:600;text-transform:capitalize;flex:1;font-size:12px;" },
            cat,
          ),
          hasIssue
            ? el(
                "span",
                { class: "badge badge-red", style: "font-size:9px;" },
                `${list.filter((j) => j.lastError).length}⚠`,
              )
            : null,
          el("span", { class: "muted small" }, String(list.length)),
          el("span", { class: "muted" }, isOpen ? "▾" : "▸"),
        ],
      ),
    );

    if (isOpen) {
      // Sort by recent activity within category
      const sorted = [...list].sort((a, b) => {
        if (a.isRunning !== b.isRunning) return a.isRunning ? -1 : 1;
        if (!!a.lastError !== !!b.lastError) return a.lastError ? -1 : 1;
        return Date.parse(b.lastRunAt || 0) - Date.parse(a.lastRunAt || 0);
      });
      for (const job of sorted) col.appendChild(renderLiveMapJobChip(job));
    }
  }
  return col;
}

function renderJobSection(title, jobs, accent) {
  const wrap = el(
    "div",
    {
      style: `margin-bottom:8px;padding-top:6px;border-left:3px solid ${accent};padding-left:8px;`,
    },
    [
      el(
        "div",
        {
          style: `font-size:11px;font-weight:700;color:${accent};margin-bottom:4px;text-transform:uppercase;letter-spacing:0.04em;`,
        },
        title,
      ),
    ],
  );
  for (const job of jobs) wrap.appendChild(renderLiveMapJobChip(job));
  return wrap;
}

function bucketJobs(jobs) {
  const runningNow = [];
  const failed = [];
  const stale = [];
  const byCategory = new Map();
  const STALE_MS = 30 * 60 * 1000;
  for (const j of jobs) {
    if (j.isRunning) runningNow.push(j);
    else if (j.lastError) failed.push(j);
    else if (j.lastRunAt && Date.now() - Date.parse(j.lastRunAt) > STALE_MS && j.runCount > 0)
      stale.push(j);

    const cat = j.category || "uncategorized";
    if (!byCategory.has(cat)) byCategory.set(cat, []);
    byCategory.get(cat).push(j);
  }
  // Most recent first within callouts
  failed.sort((a, b) => Date.parse(b.lastRunAt || 0) - Date.parse(a.lastRunAt || 0));
  stale.sort((a, b) => Date.parse(a.lastRunAt || 0) - Date.parse(b.lastRunAt || 0));
  return { runningNow, failed, stale, byCategory };
}

function renderLiveMapJobChip(job) {
  // Paused jobs override the normal status color (yellow border, dimmed)
  const dotColor = job.paused
    ? "var(--yellow)"
    : job.isRunning
      ? "var(--blue)"
      : job.lastError
        ? "var(--red)"
        : job.lastRunAt
          ? "var(--green)"
          : "var(--text-4)";
  const isExpanded = liveMapState.expandedJob === job.name;
  const isTriggering = liveMapState.triggeringJob === job.name;

  const runNowBtn =
    job.triggerable && !job.paused
      ? el(
          "button",
          {
            type: "button",
            class: "btn btn-tiny btn-primary",
            style: "padding:1px 6px;font-size:10px;flex-shrink:0;",
            title: `Run ${job.name} now`,
            disabled: isTriggering || job.isRunning,
            onclick: async (e) => {
              e.stopPropagation();
              liveMapState.triggeringJob = job.name;
              renderLiveMap();
              try {
                const r = await api("admin-trigger", { body: { job: job.name } });
                if (r?.error) toast(`${job.name}: ${r.error}`, "err");
                else {
                  toast(`▶ ${job.name} triggered`, "ok");
                  liveMapState.data = null; // force a fresh snapshot so isRunning / lastRunAt update
                }
              } catch (err) {
                toast(`${err.message}`, "err");
              } finally {
                liveMapState.triggeringJob = null;
                renderLiveMap();
              }
            },
          },
          isTriggering ? "..." : "▶",
        )
      : null;

  // ⏸/▶ pause toggle — applies to EVERY registered job (cron-only, doesn't depend on triggerable)
  const pauseBtn = el(
    "button",
    {
      type: "button",
      class: "btn btn-tiny btn-secondary",
      style: `padding:1px 6px;font-size:10px;flex-shrink:0;${job.paused ? "background:var(--yellow-soft);color:var(--yellow);border:1px solid var(--yellow);" : ""}`,
      title: job.paused
        ? `Resume ${job.name} (cron will fire normally)`
        : `Pause ${job.name} (cron still ticks but handler skipped)`,
      onclick: async (e) => {
        e.stopPropagation();
        const action = job.paused ? "resume" : "pause";
        const r = await api("job-control", { jobName: job.name, body: { action } });
        if (r?.error) toast(`${r.error}`, "err");
        else {
          toast(`${action === "pause" ? "⏸" : "▶"} ${job.name} ${action}d`, "ok");
          liveMapState.data = null;
          renderLiveMap();
        }
      },
    },
    job.paused ? "▶" : "⏸",
  );

  const styleOpacity = job.paused ? "opacity:0.55;" : "";

  return el(
    "div",
    {
      style: `padding:6px 8px;margin:3px 0;border-radius:6px;background:var(--bg-2);cursor:pointer;border-left:3px solid ${dotColor};${styleOpacity}`,
      onclick: () => {
        liveMapState.expandedJob = isExpanded ? null : job.name;
        renderLiveMap();
      },
    },
    [
      el("div", { style: "display:flex;align-items:center;gap:6px;" }, [
        el("span", {
          style: `width:8px;height:8px;border-radius:50%;background:${dotColor};${job.isRunning && !job.paused ? "animation:pulse 1.4s ease-in-out infinite;" : ""}flex-shrink:0;`,
        }),
        el(
          "span",
          {
            style:
              "flex:1;font-size:12px;font-weight:600;font-family:var(--font-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
          },
          job.name,
        ),
        job.paused
          ? el("span", { class: "badge badge-yellow", style: "font-size:9px;" }, "PAUSED")
          : null,
        el("span", { class: "muted small mono" }, fmtRel(job.lastRunAt)),
        pauseBtn,
        runNowBtn,
      ]),
      el(
        "div",
        {
          class: "muted small",
          style: "display:flex;gap:8px;margin-top:2px;font-size:10px;flex-wrap:wrap;",
        },
        [
          job.isRunning ? el("span", { style: "color:var(--blue);" }, "● running") : null,
          job.lastDurationMs != null ? el("span", {}, `${job.lastDurationMs}ms`) : null,
          job.runCount ? el("span", {}, `${job.runCount}×`) : null,
          job.errorCount ? el("span", { style: "color:var(--red);" }, `${job.errorCount}⚠`) : null,
          job.nextRunAt ? el("span", {}, `next ${fmtRelFuture(job.nextRunAt)}`) : null,
        ].filter(Boolean),
      ),
      isExpanded && job.lastError
        ? el(
            "div",
            {
              style:
                "margin-top:6px;padding:6px;background:var(--red-soft);color:var(--red);border-radius:4px;font-size:10px;font-family:var(--font-mono);white-space:pre-wrap;",
            },
            job.lastError,
          )
        : null,
      isExpanded && job.cron
        ? el("div", { class: "muted small mono", style: "margin-top:4px;" }, `cron: ${job.cron}`)
        : null,
    ],
  );
}

// ── Sparkline / SVG helpers ───────────────────────────────────────
function sparkSvg(values, color) {
  if (!values || values.length === 0) return "";
  const w = 120,
    h = 24;
  const max = Math.max(...values, 1);
  const step = w / Math.max(values.length - 1, 1);
  const pts = values
    .map((v, i) => `${(i * step).toFixed(1)},${(h - (v / max) * h).toFixed(1)}`)
    .join(" ");
  const area = `0,${h} ${pts} ${w},${h}`;
  return `<svg width="100%" height="${h}" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none">
    <polygon points="${area}" fill="${color}" opacity="0.15"/>
    <polyline points="${pts}" fill="none" stroke="${color}" stroke-width="1.5"/>
  </svg>`;
}

function sparkBarsSvg(values, color) {
  if (!values || values.length === 0) return "";
  const w = 360,
    h = 28;
  const max = Math.max(...values, 1);
  const bw = w / values.length;
  let svg = `<svg width="100%" height="${h}" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none">`;
  values.forEach((v, i) => {
    const bh = max > 0 ? (v / max) * (h - 2) : 0;
    svg += `<rect x="${i * bw + 0.5}" y="${h - bh}" width="${Math.max(bw - 1, 0.5)}" height="${bh}" fill="${color}" opacity="${v > 0 ? 0.85 : 0.2}"/>`;
  });
  svg += `</svg>`;
  return svg;
}

function runningPulseSvg(running) {
  if (running === 0) return `<div style="font-size:10px;color:var(--text-4);">idle</div>`;
  return `<div style="display:flex;gap:4px;align-items:center;height:24px;">
    ${[...Array(running)].map(() => `<span style="width:8px;height:8px;border-radius:50%;background:var(--blue);animation:pulse 1.4s ease-in-out infinite;"></span>`).join("")}
    <span style="font-size:10px;color:var(--text-4);margin-left:4px;">in flight</span>
  </div>`;
}

function healthBarsSvg(pct) {
  const w = 120,
    h = 24,
    bars = 10;
  const lit = Math.round((pct / 100) * bars);
  let svg = `<svg width="100%" height="${h}" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none">`;
  const bw = w / bars;
  for (let i = 0; i < bars; i++) {
    const color =
      i >= lit
        ? "var(--bg-3)"
        : pct >= 80
          ? "var(--green)"
          : pct >= 50
            ? "var(--yellow)"
            : "var(--red)";
    svg += `<rect x="${i * bw + 1}" y="2" width="${bw - 2}" height="${h - 4}" fill="${color}" rx="1"/>`;
  }
  svg += `</svg>`;
  return svg;
}

// ── Data computations ────────────────────────────────────────────
function computeEventsPerMinute(events, mins) {
  const buckets = new Array(mins).fill(0);
  const now = Date.now();
  for (const ev of events) {
    const ts = Date.parse(ev.ts);
    if (!ts) continue;
    const ageMin = Math.floor((now - ts) / 60_000);
    if (ageMin >= 0 && ageMin < mins) buckets[mins - 1 - ageMin] += 1;
  }
  return buckets;
}

function computeRepliesPerHour(events, hours) {
  const buckets = new Array(hours).fill(0);
  const now = Date.now();
  for (const ev of events) {
    if (ev.kind !== "reply_received" && ev.kind !== "inbound") continue;
    const ts = Date.parse(ev.ts);
    if (!ts) continue;
    const ageHr = Math.floor((now - ts) / 3_600_000);
    if (ageHr >= 0 && ageHr < hours) buckets[hours - 1 - ageHr] += 1;
  }
  return buckets;
}

function trendFromHistory(extractor) {
  const h = liveMapState.history;
  if (h.length < 2) return null;
  return extractor(h[h.length - 1]) - extractor(h[0]);
}

function historySeries(extractor) {
  return liveMapState.history.map(extractor);
}

// ── Multi-platform stacked throughput data + SVG ────────────────
const PLATFORM_COLOR = {
  x: "var(--accent)",
  twitter: "var(--accent)",
  linkedin: "var(--blue)",
  telegram: "var(--green)",
  email: "var(--yellow)",
  deals: "var(--text-3)",
  system: "var(--text-4)",
};

function normalizePlatform(p) {
  const pn = (p || "").toLowerCase();
  if (pn === "twitter") return "x";
  return pn || "system";
}

// Returns { perPlatform: { platform: number[] }, totals: number[] } where each array has `mins` slots.
function computeStackedEventsPerMinute(events, mins) {
  const perPlatform = {};
  const totals = new Array(mins).fill(0);
  const now = Date.now();
  for (const ev of events) {
    const ts = Date.parse(ev.ts);
    if (!ts) continue;
    const ageMin = Math.floor((now - ts) / 60_000);
    if (ageMin < 0 || ageMin >= mins) continue;
    const slot = mins - 1 - ageMin;
    const p = normalizePlatform(ev.platform);
    if (!perPlatform[p]) perPlatform[p] = new Array(mins).fill(0);
    perPlatform[p][slot] += 1;
    totals[slot] += 1;
  }
  return { perPlatform, totals };
}

function stackedSparkBarsSvg({ perPlatform, totals }) {
  const w = 360,
    h = 36;
  const max = Math.max(...totals, 1);
  const bw = w / totals.length;
  const platforms = Object.keys(perPlatform);
  let svg = `<svg width="100%" height="${h}" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none">`;
  for (let i = 0; i < totals.length; i++) {
    let yCursor = h;
    for (const p of platforms) {
      const v = perPlatform[p][i] || 0;
      if (v === 0) continue;
      const segH = (v / max) * (h - 2);
      yCursor -= segH;
      const color = PLATFORM_COLOR[p] || "var(--text-4)";
      svg += `<rect x="${i * bw + 0.5}" y="${yCursor}" width="${Math.max(bw - 1, 0.5)}" height="${segH}" fill="${color}" opacity="0.85"/>`;
    }
  }
  svg += `</svg>`;
  return svg;
}

// ── Platform deep-dive drawer ───────────────────────────────────
function renderPlatformLaneDrawer(data, laneKey) {
  const accounts = data.accounts || {};
  const events = data.recentEvents || [];
  const camps = data.campaigns || {};

  const meta = {
    x: {
      icon: "bird",
      name: "X / Twitter",
      list: accounts.x || [],
      camps: camps.outreach || {},
      eventMatch: (p) => p === "x" || p === "twitter",
    },
    linkedin: {
      icon: "link",
      name: "LinkedIn",
      list: accounts.linkedin || [],
      camps: camps.outreach || {},
      eventMatch: (p) => p === "linkedin",
    },
    telegram: {
      icon: "send",
      name: "Telegram",
      list: accounts.telegram || [],
      camps: camps.dm || {},
      eventMatch: (p) => p === "telegram",
    },
    email: {
      icon: "mail",
      name: "Email",
      list: accounts.email || [],
      camps: camps.email || {},
      eventMatch: (p) => p === "email",
    },
  }[laneKey];
  if (!meta) return null;

  const platEvents = events.filter((e) => meta.eventMatch((e.platform || "").toLowerCase()));
  const inbound = platEvents
    .filter((e) => e.kind === "reply_received" || e.kind === "inbound")
    .slice(0, 20);
  const outbound = platEvents.filter((e) => e.kind === "outreach_sent").slice(0, 20);

  return el(
    "div",
    {
      style:
        "position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:1000;display:flex;justify-content:flex-end;",
      onclick: (e) => {
        if (e.target === e.currentTarget) {
          liveMapState.expandedLane = null;
          renderLiveMap();
        }
      },
    },
    [
      el(
        "div",
        {
          style:
            "background:var(--bg-1);width:560px;max-width:95vw;height:100%;overflow-y:auto;padding:16px;box-shadow:-4px 0 20px rgba(0,0,0,0.4);",
        },
        [
          el(
            "div",
            {
              style:
                "display:flex;align-items:center;gap:8px;margin-bottom:14px;border-bottom:1px solid var(--bg-3);padding-bottom:10px;",
            },
            [
              el("span", { style: "font-size:22px;" }, meta.icon),
              el("div", { style: "flex:1;" }, [
                el("div", { style: "font-size:18px;font-weight:700;" }, `${meta.name} deep dive`),
                el(
                  "div",
                  { class: "muted small" },
                  `${meta.list.length} account(s) · ${platEvents.length} events in window`,
                ),
              ]),
              el(
                "button",
                {
                  type: "button",
                  class: "btn btn-secondary",
                  onclick: () => {
                    liveMapState.expandedLane = null;
                    renderLiveMap();
                  },
                },
                "× Close",
              ),
            ],
          ),

          // Accounts detail
          el("div", { class: "wizard-section-title", style: "margin-bottom:6px;" }, "Accounts"),
          meta.list.length === 0
            ? el(
                "div",
                { class: "muted small", style: "padding:10px;" },
                "No accounts connected for this platform.",
              )
            : el(
                "div",
                {},
                meta.list.map((a) => {
                  const status = (a.status || "").toLowerCase();
                  const sent = a.outreach_sent_today || a.daily_actions_count || 0;
                  const cap = a.max_daily_outreach || a.daily_actions || 0;
                  const handle =
                    a.account_identifier ||
                    a.identifier ||
                    a.email ||
                    a.phone ||
                    a.label ||
                    `acct-${a.id}`;
                  const dotCol =
                    status === "active"
                      ? "var(--green)"
                      : status === "rate_limited"
                        ? "var(--yellow)"
                        : "var(--red)";
                  return el(
                    "div",
                    {
                      style:
                        "padding:10px;background:var(--bg-2);border-radius:6px;margin-bottom:6px;border-left:3px solid " +
                        dotCol +
                        ";",
                    },
                    [
                      el(
                        "div",
                        { style: "display:flex;align-items:center;gap:8px;margin-bottom:4px;" },
                        [
                          el("span", {
                            style: `width:10px;height:10px;border-radius:50%;background:${dotCol};`,
                          }),
                          el(
                            "span",
                            { style: "font-weight:600;font-family:var(--font-mono);flex:1;" },
                            handle,
                          ),
                          el(
                            "span",
                            { class: "badge", style: `background:${dotCol}22;color:${dotCol};` },
                            status || "unknown",
                          ),
                        ],
                      ),
                      cap > 0
                        ? el("div", { style: "margin:6px 0;" }, [
                            el(
                              "div",
                              {
                                style:
                                  "display:flex;justify-content:space-between;font-size:11px;margin-bottom:2px;",
                              },
                              [
                                el("span", { class: "muted" }, "today's sends"),
                                el(
                                  "span",
                                  { class: "mono" },
                                  `${sent}/${cap} (${Math.round((sent / cap) * 100)}%)`,
                                ),
                              ],
                            ),
                            el(
                              "div",
                              {
                                style:
                                  "height:6px;background:var(--bg-3);border-radius:3px;overflow:hidden;",
                              },
                              [
                                el("div", {
                                  style: `height:100%;background:var(--accent);width:${Math.min(100, Math.round((sent / cap) * 100))}%;`,
                                }),
                              ],
                            ),
                          ])
                        : null,
                      el(
                        "div",
                        {
                          class: "muted small",
                          style: "display:flex;gap:10px;flex-wrap:wrap;font-size:10px;",
                        },
                        [
                          a.warmup_day ? el("span", {}, `warmup d${a.warmup_day}`) : null,
                          a.health_score != null
                            ? el("span", {}, `health ${a.health_score}`)
                            : null,
                          a.cooldown_until
                            ? el(
                                "span",
                                { style: "color:var(--yellow);" },
                                `cooldown until ${fmtDate(a.cooldown_until)}`,
                              )
                            : null,
                          a.last_validated_at
                            ? el("span", {}, `validated ${fmtRel(a.last_validated_at)}`)
                            : null,
                          a.proxy_id ? el("span", {}, `proxy #${a.proxy_id}`) : null,
                        ].filter(Boolean),
                      ),
                      renderAccountControlBar(a, laneKey, cap),
                    ],
                  );
                }),
              ),

          // Recent inbound — click opens the event-detail modal with full
          // text + platform-aware deep links + CRM jumps.
          el(
            "div",
            { class: "wizard-section-title", style: "margin:14px 0 6px;" },
            `Recent inbound (${inbound.length})`,
          ),
          inbound.length === 0
            ? el(
                "div",
                { class: "muted small", style: "padding:10px;" },
                "Nothing inbound recently.",
              )
            : el(
                "div",
                {},
                inbound.map((ev) =>
                  el(
                    "div",
                    {
                      style:
                        "padding:6px 8px;background:var(--bg-2);border-radius:4px;margin-bottom:4px;border-left:2px solid var(--green);font-size:11px;cursor:pointer;transition:background 100ms;",
                      onmouseenter: (e) => {
                        e.currentTarget.style.background = "var(--bg-3)";
                      },
                      onmouseleave: (e) => {
                        e.currentTarget.style.background = "var(--bg-2)";
                      },
                      title: "Click to view full message + open on platform",
                      onclick: () => openEventDetailModal(ev),
                    },
                    [
                      el("div", { style: "display:flex;gap:6px;" }, [
                        el("span", { class: "muted mono small" }, fmtRel(ev.ts)),
                        ev.who ? el("span", { style: "font-weight:600;" }, ev.who) : null,
                      ]),
                      ev.what
                        ? el(
                            "div",
                            { class: "muted small", style: "margin-top:2px;" },
                            ev.what.slice(0, 200),
                          )
                        : null,
                    ],
                  ),
                ),
              ),

          // Recent outbound
          el(
            "div",
            { class: "wizard-section-title", style: "margin:14px 0 6px;" },
            `Recent outbound (${outbound.length})`,
          ),
          outbound.length === 0
            ? el("div", { class: "muted small", style: "padding:10px;" }, "Nothing sent recently.")
            : el(
                "div",
                {},
                outbound.slice(0, 10).map((ev) =>
                  el(
                    "div",
                    {
                      style:
                        "padding:6px 8px;background:var(--bg-2);border-radius:4px;margin-bottom:4px;border-left:2px solid var(--blue);font-size:11px;cursor:pointer;transition:background 100ms;",
                      onmouseenter: (e) => {
                        e.currentTarget.style.background = "var(--bg-3)";
                      },
                      onmouseleave: (e) => {
                        e.currentTarget.style.background = "var(--bg-2)";
                      },
                      title: "Click to view what we sent + jump to platform",
                      onclick: () => openEventDetailModal(ev),
                    },
                    [
                      el("div", { style: "display:flex;gap:6px;" }, [
                        el("span", { class: "muted mono small" }, fmtRel(ev.ts)),
                        ev.who ? el("span", { style: "font-weight:600;" }, ev.who) : null,
                      ]),
                      ev.what
                        ? el(
                            "div",
                            { class: "muted small", style: "margin-top:2px;" },
                            ev.what.slice(0, 200),
                          )
                        : null,
                    ],
                  ),
                ),
              ),

          // Active campaigns shortcut
          meta.camps?.active > 0
            ? el(
                "div",
                {
                  style:
                    "margin-top:14px;padding:10px;background:var(--accent-soft);border-radius:6px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;",
                  onclick: () => {
                    liveMapState.expandedLane = null;
                    navigate("campaigns");
                  },
                },
                [
                  el(
                    "span",
                    { style: "font-weight:600;" },
                    `${meta.camps.active} active ${meta.name} campaigns`,
                  ),
                  el("span", {}, "↗ Open"),
                ],
              )
            : null,
        ],
      ),
    ],
  );
}

// ── 🤖 AI Daily Briefing card ─────────────────────────────────
async function loadAiBriefing(force = false) {
  liveMapState.aiBriefingLoading = true;
  try {
    const r = await api(force ? "ai-briefing-refresh" : "ai-briefing");
    if (r?.error) {
      liveMapState.aiBriefing = { error: r.error };
    } else {
      liveMapState.aiBriefing = r;
    }
  } catch (err) {
    liveMapState.aiBriefing = { error: err.message };
  } finally {
    liveMapState.aiBriefingLoading = false;
    if (
      location.hash.replace(/^#/, "").startsWith("home") ||
      location.hash === "" ||
      location.hash === "#"
    ) {
      renderLiveMap();
    }
  }
}

function renderAiBriefingCard() {
  const b = liveMapState.aiBriefing;
  const collapsed = liveMapState.briefingCollapsed;

  // Loading or empty state
  if (!b && !collapsed) {
    return el(
      "div",
      {
        class: "card",
        style:
          "padding:14px;background:linear-gradient(135deg, var(--accent-soft) 0%, var(--bg-2) 100%);border-left:3px solid var(--accent);",
      },
      [
        el("div", { style: "display:flex;align-items:center;gap:8px;" }, [
          el("span", { style: "font-size:18px;" }, icon("bot")),
          el(
            "span",
            { class: "muted small" },
            liveMapState.aiBriefingLoading
              ? "Claude is reading the snapshot…"
              : "Loading briefing…",
          ),
        ]),
      ],
    );
  }
  if (b?.error) {
    return el("div", { class: "card", style: "padding:10px;border-left:3px solid var(--red);" }, [
      el("div", { class: "muted small" }, `Briefing unavailable: ${b.error}`),
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny btn-secondary",
          style: "margin-top:6px;",
          onclick: () => {
            liveMapState.aiBriefing = null;
            loadAiBriefing(true);
            renderLiveMap();
          },
        },
        "↻ Retry",
      ),
    ]);
  }
  if (!b) return el("div", {});

  const ageMin = b.generatedAt
    ? Math.round((Date.now() - Date.parse(b.generatedAt)) / 60_000)
    : null;

  return el(
    "div",
    {
      class: "card",
      style:
        "padding:14px;background:linear-gradient(135deg, var(--accent-soft) 0%, var(--bg-2) 100%);border-left:3px solid var(--accent);position:relative;",
    },
    [
      el(
        "div",
        {
          style:
            "display:flex;align-items:center;gap:10px;margin-bottom:" +
            (collapsed ? "0" : "8px") +
            ";",
        },
        [
          el("span", { style: "font-size:14px;flex-shrink:0;" }, icon("bot")),
          el("div", { style: "flex:1;font-weight:700;font-size:14px;" }, b.headline || "Briefing"),
          el(
            "button",
            {
              type: "button",
              class: "btn btn-tiny btn-secondary",
              style: "padding:1px 6px;font-size:10px;",
              title: liveMapState.aiBriefingLoading ? "Refreshing…" : "Force regenerate",
              disabled: liveMapState.aiBriefingLoading,
              onclick: (e) => {
                e.stopPropagation();
                loadAiBriefing(true);
              },
            },
            liveMapState.aiBriefingLoading ? "…" : "↻",
          ),
          el(
            "button",
            {
              type: "button",
              class: "btn btn-tiny btn-secondary",
              style: "padding:1px 6px;font-size:10px;",
              title: collapsed ? "Expand briefing" : "Collapse briefing",
              onclick: () => {
                liveMapState.briefingCollapsed = !collapsed;
                renderLiveMap();
              },
            },
            collapsed ? "▼" : "▲",
          ),
        ],
      ),
      !collapsed && b.briefing
        ? el("div", { style: "font-size:13px;line-height:1.5;color:var(--text-2);" }, b.briefing)
        : null,
      !collapsed && ageMin != null
        ? el(
            "div",
            { class: "muted small", style: "margin-top:8px;font-size:10px;" },
            `Generated ${ageMin}min ago${b.cached ? " (cached)" : ""} · refresh every 30min`,
          )
        : null,
    ],
  );
}

// ── 🎭 System Mood Ring ────────────────────────────────────────
// Single emoji + 1-line vibe computed from the snapshot. Replaces the dry "X jobs"
// subtitle as a glanceable system-mood indicator.
function renderSystemMoodRing(data) {
  const events = data.recentEvents || [];
  const jobs = data.jobs || [];
  const queues = data.queues || {};
  const accts = data.accounts || {};

  const oneHourAgo = Date.now() - 3_600_000;
  const repliesHr = events.filter(
    (e) => (e.kind === "reply_received" || e.kind === "inbound") && Date.parse(e.ts) > oneHourAgo,
  ).length;
  const sentHr = events.filter(
    (e) => e.kind === "outreach_sent" && Date.parse(e.ts) > oneHourAgo,
  ).length;
  const failedJobs = jobs.filter(
    (j) => j.lastError && j.lastRunAt && Date.parse(j.lastRunAt) > oneHourAgo,
  ).length;

  const allAccts = [
    ...(accts.x || []),
    ...(accts.linkedin || []),
    ...(accts.telegram || []),
    ...(accts.email || []),
  ];
  const downAccts = allAccts.filter((a) => {
    const s = (a.status || "").toLowerCase();
    return s === "logged_out" || s === "expired" || s === "suspended" || s === "rate_limited";
  }).length;

  const queuePending = queues.enrichment?.pending || 0;
  const totalRecent = events.length;

  // Mood ring rules — first match wins
  let mood = { emoji: "😌", text: "Steady", color: "var(--green)" };
  if (failedJobs >= 3 || downAccts >= 2)
    mood = {
      emoji: "🚨",
      text: `Critical — ${failedJobs} job fails, ${downAccts} accts down`,
      color: "var(--red)",
    };
  else if (failedJobs > 0 || downAccts > 0)
    mood = {
      emoji: "⚠",
      text: `Degraded — ${failedJobs ? `${failedJobs} job fail` : ""}${failedJobs && downAccts ? " · " : ""}${downAccts ? `${downAccts} acct issue` : ""}`,
      color: "var(--yellow)",
    };
  else if (repliesHr >= 3)
    mood = { emoji: "🔥", text: `Hot — ${repliesHr} replies in last hour`, color: "var(--accent)" };
  else if (queuePending > 50)
    mood = {
      emoji: "📈",
      text: `Backlogged — ${queuePending} in enrich queue`,
      color: "var(--yellow)",
    };
  else if (totalRecent === 0)
    mood = { emoji: "💤", text: "Quiet — no events in window", color: "var(--text-3)" };
  else if (sentHr > 0 || repliesHr > 0)
    mood = {
      emoji: "✅",
      text: `Steady — ${sentHr} sent · ${repliesHr} repl${repliesHr === 1 ? "y" : "ies"} (1h)`,
      color: "var(--green)",
    };

  return el(
    "span",
    {
      style: `display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:500;color:${mood.color};background:${mood.color}11;padding:3px 10px;border-radius:12px;border:1px solid ${mood.color}33;`,
      title: "System mood, computed live from current snapshot",
    },
    [el("span", { style: "font-size:14px;" }, mood.emoji), el("span", {}, mood.text)],
  );
}

// ── 🗓 Activity heatmap — 7 days × 24 hours grid ─────────────
function renderActivityHeatmap(data) {
  const events = data.recentEvents || [];
  const cells = computeActivityHeatmap(events);
  const peak = Math.max(...cells.flat(), 1);

  // Rows: days of week starting today (newest at top), Cols: hours 0-23
  const dayLabels = [];
  const today = new Date();
  for (let i = 0; i < 7; i++) {
    const d = new Date(today.getTime() - i * 86_400_000);
    dayLabels.push(d.toLocaleDateString(undefined, { weekday: "short" }));
  }

  const card = el("div", { class: "card", style: "padding:12px;" }, [
    el(
      "div",
      { style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;" },
      [
        el("div", { class: "wizard-section-title", style: "margin:0;" }, "Activity heatmap"),
        el("span", { class: "muted small" }, "7d × 24h"),
      ],
    ),
  ]);

  // Hour-axis label strip
  const hourStrip = el(
    "div",
    {
      style:
        "display:grid;grid-template-columns:32px repeat(24, 1fr);gap:1px;font-size:8px;color:var(--text-4);margin-bottom:2px;",
    },
    [
      el("span", {}),
      ...Array.from({ length: 24 }, (_, h) =>
        el("span", { style: "text-align:center;" }, h % 6 === 0 ? String(h) : ""),
      ),
    ],
  );
  card.appendChild(hourStrip);

  // Grid
  for (let row = 0; row < 7; row++) {
    const rowEl = el(
      "div",
      {
        style: "display:grid;grid-template-columns:32px repeat(24, 1fr);gap:1px;margin-bottom:1px;",
      },
      [
        el(
          "span",
          { class: "muted small mono", style: "font-size:9px;text-align:right;padding-right:4px;" },
          dayLabels[row],
        ),
        ...Array.from({ length: 24 }, (_, h) => {
          const count = cells[row][h] || 0;
          const intensity = Math.min(1, count / peak);
          const opacity = count === 0 ? 0.06 : Math.max(0.18, intensity);
          return el("div", {
            style: `aspect-ratio:1;background:var(--accent);opacity:${opacity};border-radius:1px;cursor:default;`,
            title: `${dayLabels[row]} ${h}:00 — ${count} event${count === 1 ? "" : "s"}`,
          });
        }),
      ],
    );
    card.appendChild(rowEl);
  }

  // Now-indicator caption
  const currentHour = today.getHours();
  card.appendChild(
    el(
      "div",
      { class: "muted small", style: "margin-top:6px;font-size:10px;text-align:center;" },
      `Current hour: ${currentHour}:00 · peak cell: ${peak} events`,
    ),
  );

  return card;
}

// Bin events into a 7-row × 24-col grid (row 0 = today, row 6 = 6 days ago)
function computeActivityHeatmap(events) {
  const grid = Array.from({ length: 7 }, () => new Array(24).fill(0));
  const todayMidnight = new Date();
  todayMidnight.setHours(0, 0, 0, 0);
  for (const ev of events) {
    const ts = Date.parse(ev.ts);
    if (!ts) continue;
    const d = new Date(ts);
    const dayMidnight = new Date(d);
    dayMidnight.setHours(0, 0, 0, 0);
    const daysAgo = Math.round((todayMidnight.getTime() - dayMidnight.getTime()) / 86_400_000);
    if (daysAgo < 0 || daysAgo >= 7) continue;
    grid[daysAgo][d.getHours()] += 1;
  }
  return grid;
}

// ── 📌 Saved views — preset + custom view picker ──────────────
const BUILTIN_VIEWS = [
  { id: "preset_default", name: "Default", state: { timeRangeSec: 86_400, eventFilter: "all" } },
  {
    id: "preset_hot_replies",
    name: "Hot replies (1h)",
    state: { timeRangeSec: 3_600, eventFilter: "inbound" },
  },
  {
    id: "preset_signals",
    name: "Today's signals",
    state: { timeRangeSec: 86_400, eventFilter: "signal" },
  },
  {
    id: "preset_outreach",
    name: "Recent outreach",
    state: { timeRangeSec: 86_400, eventFilter: "outreach_sent" },
  },
  {
    id: "preset_deals",
    name: "Deal events (7d)",
    state: { timeRangeSec: 604_800, eventFilter: "deal" },
  },
  {
    id: "preset_enrich",
    name: "Enrichment activity",
    state: { timeRangeSec: 86_400, eventFilter: "enrichment" },
  },
  {
    id: "preset_failed",
    name: "Last 5 min (debug)",
    state: { timeRangeSec: 300, eventFilter: "all" },
  },
];

function applyViewState(state) {
  if (state.timeRangeSec !== undefined) liveMapState.timeRangeSec = state.timeRangeSec;
  if (state.eventFilter !== undefined) liveMapState.eventFilter = state.eventFilter;
  if (state.density !== undefined) liveMapState.density = state.density;
  persistLiveMapPrefs();
  renderLiveMap();
}

function renderSavedViewsButton() {
  const all = [...BUILTIN_VIEWS, ...(liveMapState.savedViews || [])];
  return el("div", { style: "position:relative;display:inline-block;" }, [
    el(
      "button",
      {
        type: "button",
        class: "btn btn-tiny btn-secondary",
        title: "Saved view presets",
        onclick: () => {
          liveMapState.showViewMenu = !liveMapState.showViewMenu;
          renderLiveMap();
        },
      },
      "Views",
    ),
    liveMapState.showViewMenu
      ? el(
          "div",
          {
            style:
              "position:absolute;top:30px;right:0;width:280px;max-height:60vh;overflow-y:auto;background:var(--bg-1);border:1px solid var(--bg-3);border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,0.4);z-index:200;padding:6px;",
          },
          [
            el(
              "div",
              {
                class: "muted",
                style:
                  "font-size:10px;text-transform:uppercase;letter-spacing:0.06em;padding:4px 8px;",
              },
              "Presets",
            ),
            ...BUILTIN_VIEWS.map((v) =>
              el(
                "div",
                {
                  style: "padding:8px;border-radius:4px;cursor:pointer;font-size:12px;",
                  onmouseenter: (e) => {
                    e.currentTarget.style.background = "var(--bg-2)";
                  },
                  onmouseleave: (e) => {
                    e.currentTarget.style.background = "";
                  },
                  onclick: () => {
                    liveMapState.showViewMenu = false;
                    applyViewState(v.state);
                  },
                },
                v.name,
              ),
            ),
            (liveMapState.savedViews || []).length > 0
              ? el(
                  "div",
                  {
                    class: "muted",
                    style:
                      "font-size:10px;text-transform:uppercase;letter-spacing:0.06em;padding:8px 8px 4px;border-top:1px solid var(--bg-3);margin-top:4px;",
                  },
                  "My saved",
                )
              : null,
            ...(liveMapState.savedViews || []).map((v, i) =>
              el(
                "div",
                {
                  style:
                    "padding:8px;border-radius:4px;font-size:12px;display:flex;align-items:center;gap:6px;",
                },
                [
                  el(
                    "span",
                    {
                      style: "flex:1;cursor:pointer;",
                      onclick: () => {
                        liveMapState.showViewMenu = false;
                        applyViewState(v.state);
                      },
                    },
                    v.name,
                  ),
                  el(
                    "button",
                    {
                      type: "button",
                      class: "btn btn-tiny btn-secondary",
                      style: "padding:1px 6px;font-size:10px;",
                      title: "Delete this saved view",
                      onclick: (e) => {
                        e.stopPropagation();
                        liveMapState.savedViews.splice(i, 1);
                        persistViews();
                        renderLiveMap();
                      },
                    },
                    "×",
                  ),
                ],
              ),
            ),
            el(
              "div",
              { style: "padding:6px 8px;border-top:1px solid var(--bg-3);margin-top:6px;" },
              [
                el(
                  "button",
                  {
                    type: "button",
                    class: "btn btn-tiny btn-primary",
                    style: "width:100%;",
                    title: "Save the current state (range + filter + density) as a new view",
                    onclick: () => {
                      const name = prompt("Save current view as:");
                      if (!name) return;
                      liveMapState.savedViews.push({
                        id: `custom_${Date.now()}`,
                        name: name.trim().slice(0, 40),
                        state: {
                          timeRangeSec: liveMapState.timeRangeSec,
                          eventFilter: liveMapState.eventFilter,
                          density: liveMapState.density,
                        },
                      });
                      persistViews();
                      liveMapState.showViewMenu = false;
                      renderLiveMap();
                    },
                  },
                  "+ Save current view",
                ),
              ],
            ),
          ],
        )
      : null,
  ]);
}

// ── 🔔 Notifications history bell + popover ────────────────────
// Persists across reloads via chrome.storage.local. Bell badge shows unseen count.
function renderNotifBellButton() {
  const log = liveMapState.notificationLog || [];
  const lastSeenAt = liveMapState._notifLastSeenAt || 0;
  const unseen = log.filter((n) => Date.parse(n.seenAt) > lastSeenAt).length;

  return el("div", { style: "position:relative;display:inline-block;" }, [
    el(
      "button",
      {
        type: "button",
        class: "btn btn-tiny btn-secondary",
        title: "Notifications history",
        onclick: () => {
          liveMapState.showNotifLog = !liveMapState.showNotifLog;
          if (liveMapState.showNotifLog) liveMapState._notifLastSeenAt = Date.now();
          renderLiveMap();
        },
      },
      [
        el("span", {}, liveMapState.showNotifLog ? "🔕" : "🔔"),
        unseen > 0
          ? el(
              "span",
              {
                style:
                  "position:absolute;top:-4px;right:-4px;background:var(--red);color:white;font-size:9px;font-weight:700;border-radius:8px;padding:1px 5px;line-height:1;",
              },
              String(unseen),
            )
          : null,
      ],
    ),
    liveMapState.showNotifLog ? renderNotifLogPopover(log) : null,
  ]);
}

function renderNotifLogPopover(log) {
  const platformIcons = {
    x: "🐦",
    twitter: "🐦",
    linkedin: "🔗",
    telegram: "✈",
    email: "✉",
    deals: "💰",
  };
  return el(
    "div",
    {
      style:
        "position:absolute;top:30px;right:0;width:380px;max-height:60vh;overflow-y:auto;background:var(--bg-1);border:1px solid var(--bg-3);border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,0.4);z-index:200;padding:8px;",
    },
    [
      el(
        "div",
        {
          style:
            "display:flex;justify-content:space-between;align-items:center;padding:6px 8px;border-bottom:1px solid var(--bg-3);margin-bottom:6px;",
        },
        [
          el("div", { style: "font-weight:600;font-size:12px;" }, `Notifications (${log.length})`),
          el(
            "button",
            {
              type: "button",
              class: "btn btn-tiny btn-secondary",
              onclick: () => {
                liveMapState.notificationLog = [];
                persistNotifLog();
                renderLiveMap();
              },
            },
            "Clear",
          ),
        ],
      ),
      log.length === 0
        ? el(
            "div",
            { class: "muted small", style: "padding:18px;text-align:center;" },
            "No notifications yet. Replies, deals, and signals will land here.",
          )
        : el(
            "div",
            {},
            [...log].reverse().map((n) =>
              el(
                "div",
                {
                  style:
                    "padding:8px;border-radius:4px;background:var(--bg-2);margin-bottom:4px;border-left:2px solid " +
                    (KIND_COLOR[n.kind] || "var(--text-4)") +
                    ";cursor:" +
                    (n.drillTo ? "pointer" : "default") +
                    ";font-size:11px;",
                  onclick: () => {
                    if (!n.drillTo) return;
                    liveMapState.showNotifLog = false;
                    if (n.drillTo.type === "contact") showContactDrawer(n.drillTo.id);
                    else if (n.drillTo.type === "deal") navigate("deals");
                    else if (n.drillTo.type === "signal") navigate("signals");
                  },
                },
                [
                  el("div", { style: "display:flex;gap:6px;align-items:center;" }, [
                    el(
                      "span",
                      { class: "muted mono small", style: "min-width:50px;" },
                      fmtRel(n.ts),
                    ),
                    el("span", {}, platformIcons[n.platform] || "•"),
                    el("span", { style: "font-weight:600;" }, n.label),
                    el(
                      "span",
                      {
                        style:
                          "flex:1;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
                      },
                      n.who || "—",
                    ),
                  ]),
                  n.what
                    ? el(
                        "div",
                        { class: "muted small", style: "margin-top:2px;" },
                        (n.what || "").slice(0, 140),
                      )
                    : null,
                ],
              ),
            ),
          ),
    ],
  );
}

// ── Per-account control bar (renders inside platform-lane drawer) ──
// Pause for 1h / 6h / 24h, Resume, set cap, Resync session — direct API calls.
function renderAccountControlBar(account, platform, currentCap) {
  const isCoolingDown = account.cooldown_until && Date.parse(account.cooldown_until) > Date.now();
  const status = (account.status || "").toLowerCase();
  const needsResync = status === "expired" || status === "logged_out";

  // Use the platform key the backend understands (twitter for X)
  const apiPlatform = platform === "x" ? "twitter" : platform;

  const callControl = async (body) => {
    const r = await api("account-control", {
      platform: apiPlatform,
      accountId: account.id,
      body,
    });
    if (r?.error) toast(`${r.error}`, "err");
    else {
      toast(`${body.action} on ${platform} #${account.id}`, "ok");
      liveMapState.data = null; // force fresh snapshot
      renderLiveMap();
    }
  };

  const pauseBtn = (label, hours) =>
    el(
      "button",
      {
        type: "button",
        class: "btn btn-tiny btn-secondary",
        style: "padding:2px 8px;font-size:10px;",
        title: `Pause this account for ${hours}h`,
        onclick: () => callControl({ action: "pause", hours }),
      },
      label,
    );

  const wrap = el("div", {
    style:
      "display:flex;flex-wrap:wrap;gap:4px;margin-top:8px;padding-top:8px;border-top:1px solid var(--bg-3);align-items:center;",
  });

  if (isCoolingDown || status === "paused" || status === "rate_limited") {
    wrap.appendChild(
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny btn-primary",
          style: "padding:2px 8px;font-size:10px;",
          title: "Resume — clear cooldown / restore active",
          onclick: () => callControl({ action: "resume" }),
        },
        "▶ Resume",
      ),
    );
  } else {
    wrap.appendChild(
      el("span", { class: "muted small", style: "font-size:10px;margin-right:4px;" }, "Pause"),
    );
    wrap.appendChild(pauseBtn("1h", 1));
    wrap.appendChild(pauseBtn("6h", 6));
    wrap.appendChild(pauseBtn("24h", 24));
  }

  // Daily cap editor (only platforms that have one — twitter / telegram)
  if (apiPlatform === "twitter" || apiPlatform === "telegram") {
    wrap.appendChild(
      el("span", { class: "muted small", style: "font-size:10px;margin-left:8px;" }, "Cap"),
    );
    const capInput = el("input", {
      type: "number",
      min: "0",
      max: "1000",
      step: "5",
      value: String(currentCap || 0),
      style:
        "width:60px;padding:2px 4px;font-size:11px;background:var(--bg-2);border:1px solid var(--border-strong);border-radius:var(--radius-sm);color:var(--text-1);",
    });
    wrap.appendChild(capInput);
    wrap.appendChild(
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny btn-secondary",
          style: "padding:2px 8px;font-size:10px;",
          title: "Save cap",
          onclick: () => {
            const cap = Math.max(0, Math.min(1000, parseInt(capInput.value, 10) || 0));
            callControl({ action: "set-cap", cap });
          },
        },
        "Set",
      ),
    );
  }

  // Resync — only show when account looks broken
  if (needsResync) {
    wrap.appendChild(
      el(
        "button",
        {
          type: "button",
          class: "btn btn-tiny",
          style:
            "padding:2px 8px;font-size:10px;background:var(--yellow-soft);color:var(--yellow);border:1px solid var(--yellow);margin-left:auto;",
          title: "Re-validate session",
          onclick: () => callControl({ action: "resync" }),
        },
        "↻ Resync session",
      ),
    );
  }

  return wrap;
}

// ═════════════════════════════════════════════════════════════════
// Phase 1 — new routes (March 2026 sidebar reorg)
//   * Home — Mission Control summary + Quick Actions + Live Map
//   * People / Competitor / Service Match — Phase-N "coming soon" stubs
//   * Strategies — fully wired (API already exists)
//   * Tutorial — interactive 5-page guide (client-side state only)
//   * Premium — read-only status panel; upgrade flow stays in Telegram
// ═════════════════════════════════════════════════════════════════

// ─── Home ────────────────────────────────────────────────────────
// Wraps the existing renderLiveMap with a Mission Control summary
// at the top. Mirrors the structure of /home in the Telegram bot:
//   1. Top-3 priority actions (from /api/mission-control)
//   2. Quick action row (New Campaign / People / CRM / Strategies)
//   3. Existing dashboard / live-map below
async function renderHome() {
  // Render the existing live-map first so all the topbar/insights/lanes are in place.
  await renderLiveMap();

  // First-run checklist — sits atop Home until the user completes onboarding.
  try {
    await _renderOnboardingChecklist();
  } catch {
    /* best-effort, never block home */
  }

  // Then prepend a mission-control summary card. We do it post-render so we
  // don't block the live-map fetch on the mission API call.
  const content = $("#content");
  if (!content) return;

  const summaryCard = el(
    "div",
    {
      class: "card",
      id: "home-mission-summary",
      style: "padding: 16px; margin-bottom: 12px;",
    },
    [
      el(
        "div",
        {
          style:
            "display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;",
        },
        [
          el("div", { class: "card-title", style: "margin: 0;" }, "Mission Control"),
          el(
            "button",
            {
              class: "btn btn-tiny btn-secondary",
              type: "button",
              onclick: () => navigate("mission"),
            },
            "Open →",
          ),
        ],
      ),
      el("div", { id: "home-mission-list", class: "muted small" }, "Loading top priorities…"),

      // Quick actions
      el(
        "div",
        {
          class: "row",
          style:
            "gap: 8px; flex-wrap: wrap; margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border);",
        },
        [
          el(
            "button",
            {
              class: "btn btn-tiny btn-primary",
              type: "button",
              onclick: () => navigate("operations"),
            },
            "New Campaign",
          ),
          el(
            "button",
            {
              class: "btn btn-tiny btn-secondary",
              type: "button",
              onclick: () => navigate("people"),
            },
            "Browse People",
          ),
          el(
            "button",
            { class: "btn btn-tiny btn-secondary", type: "button", onclick: () => navigate("crm") },
            "CRM",
          ),
          el(
            "button",
            {
              class: "btn btn-tiny btn-secondary",
              type: "button",
              onclick: () => navigate("strategies"),
            },
            "Strategies",
          ),
          el(
            "button",
            {
              class: "btn btn-tiny btn-secondary",
              type: "button",
              onclick: () => navigate("inbox"),
            },
            "Inbox",
          ),
        ],
      ),
    ],
  );

  // Insert the summary card as the very first element
  if (content.firstChild) {
    content.insertBefore(summaryCard, content.firstChild);
  } else {
    content.appendChild(summaryCard);
  }

  // Async-fetch mission control top-3 (do not block initial render).
  api("mission-control")
    .then((data) => {
      const list = $("#home-mission-list");
      if (!list) return;
      const actions = (data?.actions || []).slice(0, 3);
      if (!actions.length) {
        list.replaceChildren(
          el("div", { class: "muted", style: "padding: 8px 0;" }, "All clear — no urgent actions."),
        );
        return;
      }
      list.classList.remove("muted", "small");
      list.replaceChildren(
        ...actions.map((a, i) => {
          const urgencyColor =
            a.urgency === "high"
              ? "var(--red)"
              : a.urgency === "medium"
                ? "var(--yellow)"
                : "var(--text-4)";
          return el(
            "div",
            {
              style: `display: flex; align-items: center; gap: 12px; padding: 8px; border-left: 3px solid ${urgencyColor}; background: var(--bg-1); border-radius: var(--radius-sm); margin-bottom: 6px; cursor: pointer;`,
              onclick: () => navigate(a.route || "mission"),
            },
            [
              el("span", { style: "font-size: 16px;" }, a.icon || `${i + 1}`),
              el("div", { style: "flex: 1;" }, [
                el("div", { class: "primary-cell", style: "font-size: 13px;" }, a.title),
                a.hint
                  ? el(
                      "div",
                      { class: "muted", style: "font-size: 11px; margin-top: 2px;" },
                      a.hint,
                    )
                  : null,
              ]),
              a.count ? el("span", { class: "badge badge-red" }, String(a.count)) : null,
              el("span", { class: "muted small" }, "→"),
            ],
          );
        }),
      );
    })
    .catch(() => {
      const list = $("#home-mission-list");
      if (list) list.textContent = "Mission Control unavailable.";
    });
}

// ─── Strategies ──────────────────────────────────────────────────

async function renderStrategies() {
  showLoading("Loading strategies…");
  const data = await api("strategy-list").catch((err) => ({ error: err.message }));
  if (data?.error && !data?.strategies) {
    showError(data.error, renderStrategies);
    return;
  }
  const strategies = data.strategies || [];

  const grouped = { toggle: [], launch: [], action: [] };
  for (const s of strategies) {
    const kind = s.kind || s.type || "action";
    if (grouped[kind]) grouped[kind].push(s);
    else grouped.action.push(s);
  }

  function renderStrategyCard(s) {
    const card = el("div", { class: "card", style: "padding: 16px; margin-bottom: 8px;" }, [
      el(
        "div",
        {
          style:
            "display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;",
        },
        [
          el("div", { style: "flex: 1;" }, [
            el("div", { class: "primary-cell", style: "font-size: 14px;" }, [
              s.icon ? el("span", { style: "margin-right: 6px;" }, s.icon) : null,
              s.name || s.id,
            ]),
            s.description
              ? el(
                  "div",
                  { class: "muted", style: "font-size: 12px; margin-top: 2px;" },
                  s.description,
                )
              : null,
            s.statusLine
              ? el(
                  "div",
                  {
                    class: "muted",
                    style: "font-size: 11px; margin-top: 6px; font-family: var(--font-mono);",
                  },
                  s.statusLine,
                )
              : null,
          ]),
          el("div", { style: "flex-shrink: 0;" }, [renderStrategyControl(s)]),
        ],
      ),
    ]);
    return card;
  }

  function renderStrategyControl(s) {
    const kind = s.kind || s.type || "action";
    if (kind === "toggle") {
      const enabled = !!s.enabled;
      return el(
        "button",
        {
          class: `btn btn-tiny ${enabled ? "btn-primary" : "btn-secondary"}`,
          type: "button",
          onclick: async (ev) => {
            await withProgress(ev.currentTarget, async () => {
              const r = await api("strategy-toggle", { strategyId: s.id, enabled: !enabled });
              if (r?.error) toast(r.error, "err");
              else {
                toast(enabled ? "Disabled" : "Enabled", "ok");
                renderStrategies();
              }
            });
          },
        },
        enabled ? "● On" : "Off",
      );
    }
    if (kind === "launch") {
      return el(
        "button",
        {
          class: "btn btn-tiny btn-primary",
          type: "button",
          onclick: () => openTgStrategy(s.id),
        },
        "Launch ↗",
      );
    }
    return el(
      "button",
      {
        class: "btn btn-tiny btn-primary",
        type: "button",
        onclick: async (ev) => {
          await withProgress(ev.currentTarget, async () => {
            const r = await api("strategy-action", { strategyId: s.id, context: {} });
            if (r?.error) toast(r.error, "err");
            else toast(r.message || "Done", "ok");
          });
        },
      },
      "Run",
    );
  }

  function renderGroup(label, items, hint) {
    if (!items.length) return null;
    return el("div", { class: "card", style: "margin-bottom: 16px;" }, [
      el("div", { class: "card-title" }, label),
      hint
        ? el("div", { class: "muted", style: "font-size: 12px; margin-bottom: 8px;" }, hint)
        : null,
      ...items.map(renderStrategyCard),
    ]);
  }

  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "Strategies"),
        el("div", { class: "page-subtitle" }, "Background automations + one-shot actions"),
      ]),
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: renderStrategies },
        "↻ Refresh",
      ),
    ]),
    strategies.length === 0
      ? emptyState(
          "⚡",
          "No strategies registered yet",
          "Strategies come from the bot's strategy registry. Open the bot to register one.",
        )
      : el("div", {}, [
          renderGroup(
            "Toggle",
            grouped.toggle,
            "Persistent background jobs you can switch on/off.",
          ),
          renderGroup("Launch", grouped.launch, "One-tap deep-links into the Telegram bot."),
          renderGroup("Actions", grouped.action, "Run an immediate server-side action."),
        ]),
  ]);
  setContentNode(content);
}

// ─── Tutorial ────────────────────────────────────────────────────

const TUTORIAL_PAGES = [
  {
    title: "Welcome to Nexus",
    icon: "user",
    body: [
      "Nexus is a multi-channel CRM + outreach engine that lives in two places: the Telegram bot (mobile-first, conversational) and this Chrome extension (desktop power-user UI).",
      "This 5-step tour gets you oriented. Keyboard shortcut: <b>Esc</b> to close any drawer, <b>Ctrl+K</b> to search.",
    ],
  },
  {
    title: "People — your network graph",
    icon: "users",
    body: [
      "<b>People</b> shows registered users (other Nexus members, AI-matched to your profile) and incoming/outgoing connection requests.",
      "Open Telegram and type <code>/intro</code> to register your own profile so others can find you. Then come back here to discover matches.",
      "Tabs: Recommended (AI matches) — Requests (incoming) — Sent — Connections.",
    ],
  },
  {
    title: "CRM — contacts, companies, deals",
    icon: "folder",
    body: [
      "<b>CRM</b> is your B2B address book. Free tier: import CSVs, tag contacts, log notes. Premium: 15k+ vetted contacts + AI enrichment + Google Sheets sync.",
      "Add contacts manually, import a CSV, or auto-create from positive replies via the outreach engine.",
      "<b>Deals</b> is the pipeline view of contacts you're actively working.",
    ],
  },
  {
    title: "Inbox + Mission Control — daily focus",
    icon: "target",
    body: [
      "<b>Inbox</b> aggregates replies and notifications from every channel. <b>Mission Control</b> ranks the top 3 actions by urgency × revenue × freshness.",
      "Start every session at <b>Home</b>: it shows Mission Control + active campaign health + smart next action.",
    ],
  },
  {
    title: "Platforms — X / LinkedIn / Telegram / Email",
    icon: "launch",
    body: [
      "Each platform supports up to 5 accounts. Per-platform sub-tabs: <b>Overview</b> (accounts/pools/proxies/limits), <b>Operations</b> (campaigns/broadcast/scrape/etc), <b>Monitor</b> (analytics + health).",
      "<b>Telegram</b> exposes the most tools (DM campaigns, broadcast, mass invite, forward rules, auto-reply, scrape).",
      "<b>X</b> and <b>LinkedIn</b> live mostly inside this Chrome extension via direct browser automation — cookie-based, no VPS needed.",
      "You're done! Click <b>Finish</b> to close, or come back any time via System → Tutorial.",
    ],
  },
];

async function renderTutorial(params) {
  const stored = await chrome.storage.local.get(["tutorial_page", "tutorial_completed"]);
  let pageIndex = parseInt(params?.page, 10);
  if (!Number.isFinite(pageIndex)) pageIndex = stored.tutorial_page || 0;
  if (pageIndex < 0) pageIndex = 0;
  if (pageIndex >= TUTORIAL_PAGES.length) pageIndex = TUTORIAL_PAGES.length - 1;

  const page = TUTORIAL_PAGES[pageIndex];
  const isFirst = pageIndex === 0;
  const isLast = pageIndex === TUTORIAL_PAGES.length - 1;

  await chrome.storage.local.set({ tutorial_page: pageIndex });

  function dot(i) {
    return el("button", {
      class: `tutorial-dot ${i === pageIndex ? "active" : ""}${i < pageIndex ? " visited" : ""}`,
      type: "button",
      title: TUTORIAL_PAGES[i].title,
      onclick: async () => {
        await chrome.storage.local.set({ tutorial_page: i });
        renderTutorial({ page: i });
      },
    });
  }

  const content = el(
    "div",
    { class: "tutorial-wrap", style: "max-width: 720px; margin: 0 auto;" },
    [
      el("div", { class: "page-header" }, [
        el("div", {}, [
          el("div", { class: "page-title" }, "Tutorial"),
          el(
            "div",
            { class: "page-subtitle" },
            `Page ${pageIndex + 1} of ${TUTORIAL_PAGES.length}`,
          ),
        ]),
        stored.tutorial_completed ? el("span", { class: "badge badge-green" }, "Completed") : null,
      ]),

      // Progress dots
      el(
        "div",
        {
          class: "tutorial-progress",
          style: "display: flex; gap: 8px; justify-content: center; margin-bottom: 24px;",
        },
        TUTORIAL_PAGES.map((_, i) => dot(i)),
      ),

      // Page card
      el("div", { class: "card", style: "padding: 32px;" }, [
        el(
          "div",
          { style: "font-size: 48px; text-align: center; margin-bottom: 16px;" },
          page.icon,
        ),
        el(
          "div",
          {
            class: "card-title",
            style: "text-align: center; font-size: 20px; margin-bottom: 24px;",
          },
          page.title,
        ),
        ...page.body.map((p) =>
          el("div", {
            html: p,
            class: "muted",
            style: "font-size: 14px; line-height: 1.7; margin-bottom: 12px;",
          }),
        ),
      ]),

      // Navigation
      el(
        "div",
        {
          style:
            "display: flex; justify-content: space-between; align-items: center; margin-top: 24px;",
        },
        [
          el(
            "button",
            {
              class: "btn btn-secondary",
              type: "button",
              disabled: isFirst ? "disabled" : null,
              onclick: () => renderTutorial({ page: pageIndex - 1 }),
            },
            "← Back",
          ),
          el("div", { class: "muted small" }, `Step ${pageIndex + 1} / ${TUTORIAL_PAGES.length}`),
          isLast
            ? el(
                "button",
                {
                  class: "btn btn-primary",
                  type: "button",
                  onclick: async () => {
                    await chrome.storage.local.set({ tutorial_completed: true, tutorial_page: 0 });
                    toast("Tutorial complete — welcome aboard!", "ok");
                    navigate("home");
                  },
                },
                "Finish",
              )
            : el(
                "button",
                {
                  class: "btn btn-primary",
                  type: "button",
                  onclick: () => renderTutorial({ page: pageIndex + 1 }),
                },
                "Next →",
              ),
        ],
      ),
    ],
  );
  setContentNode(content);
}

// ─── Premium status (read-only) ──────────────────────────────────

async function renderPremium() {
  showLoading("Loading premium status…");
  const data = await api("premium-status").catch((err) => ({ error: err.message }));
  if (data?.error) {
    showError(data.error, renderPremium);
    return;
  }

  const tier = data.tier || "free";
  const isPremium = !!data.isPremium;
  const isTrial = !!data.isTrial;
  const addons = data.addons || {};
  const pricing = data.pricing || {};
  const trial = data.trial || {};

  const tierBadge = isPremium
    ? isTrial
      ? "badge-yellow"
      : tier === "lifetime"
        ? "badge-purple"
        : "badge-green"
    : "badge-gray";

  function addonRow(label, addon, priceSol) {
    const active = !!addon?.active;
    return el(
      "div",
      {
        class: "channel-row",
        style:
          "padding: 10px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px;",
      },
      [
        el("div", { class: "channel-icon" }, active ? "✅" : "⚫"),
        el("div", { style: "flex: 1;" }, [
          el("div", { class: "primary-cell" }, label),
          el(
            "div",
            { class: "muted", style: "font-size: 11px;" },
            active && addon.expiresAt
              ? `Active • expires ${fmtDate(addon.expiresAt)}`
              : `Inactive • ${priceSol} SOL / month`,
          ),
        ]),
        el(
          "button",
          {
            class: `btn btn-tiny ${active ? "btn-secondary" : "btn-primary"}`,
            type: "button",
            onclick: () => openTgCommand("/premium"),
          },
          active ? "Manage" : "Upgrade",
        ),
      ],
    );
  }

  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "Premium"),
        el("div", { class: "page-subtitle" }, "Subscription status + add-ons"),
      ]),
      el(
        "button",
        { class: "btn btn-secondary", type: "button", onclick: renderPremium },
        "↻ Refresh",
      ),
    ]),

    // Current tier
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Current plan"),
      el("div", { class: "row", style: "gap: 16px; flex-wrap: wrap; align-items: center;" }, [
        el("div", {}, [
          el("div", { class: "stat-label" }, "Tier"),
          el("div", { style: "margin-top: 4px;" }, [
            el("span", { class: `badge ${tierBadge}` }, tier.toUpperCase()),
            isTrial
              ? el("span", { class: "muted small", style: "margin-left: 8px;" }, "(trial)")
              : null,
          ]),
        ]),
        data.startedAt
          ? el("div", {}, [
              el("div", { class: "stat-label" }, "Started"),
              el("div", { class: "mono", style: "margin-top: 4px;" }, fmtDate(data.startedAt)),
            ])
          : null,
        data.expiresAt
          ? el("div", {}, [
              el("div", { class: "stat-label" }, "Expires"),
              el("div", { class: "mono", style: "margin-top: 4px;" }, fmtDate(data.expiresAt)),
            ])
          : null,
      ]),
      isPremium
        ? null
        : el("div", { style: "margin-top: 16px;" }, [
            el(
              "button",
              {
                class: "btn btn-primary",
                type: "button",
                onclick: () => openTgCommand("/premium"),
              },
              trial?.used ? "Upgrade in Telegram" : `Start free trial in Telegram`,
            ),
            el(
              "div",
              { class: "muted small", style: "margin-top: 8px;" },
              `SOL pricing: ${pricing.monthly_sol || "?"} / month • ${pricing.lifetime_sol || "?"} lifetime (yearly)`,
            ),
          ]),
    ]),

    // Add-ons
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Add-ons"),
      addonRow("X / Twitter Marketing", addons.x_marketing, pricing.x_addon_sol || "?"),
      addonRow("LinkedIn Marketing", addons.linkedin_marketing, pricing.li_addon_sol || "?"),
      addonRow("Telegram AI", addons.telegram_ai, pricing.li_addon_sol || "?"),
    ]),

    // Why upgrade is in Telegram
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "About payments"),
      el(
        "div",
        { class: "muted", style: "font-size: 13px; line-height: 1.7;" },
        "Subscriptions are paid in SOL on Solana mainnet. The full payment flow (TX hash submission, async polling) lives in the Telegram bot — we’ll port it to the extension once we’re sure the dual-execution paths stay in sync.",
      ),
    ]),
  ]);
  setContentNode(content);
}

// ─── Your Profile (Phase 6) ──────────────────────────────────────
// Edit your own intro/profile that other Nexus members discover via /matches.
// Initial creation must happen via /intro in Telegram (AI parses bio).

async function renderProfile() {
  showLoading("Loading your profile…");
  const data = await api("profile-get").catch((err) => ({ error: err.message }));

  if (data?.error === "no_profile") {
    setContentNode(renderProfileNoProfile(data));
    return;
  }
  if (data?.error) {
    showError(data.error, renderProfile);
    return;
  }

  const p = data.profile || {};
  const communities = data.communities || [];

  // Form state — editable subset
  const state = {
    displayName: p.displayName || "",
    company: p.company || "",
    role: p.role || "",
    sector: p.sector || "",
    skills: (p.skills || []).join(", "),
    offers: (p.offers || []).join(", "),
    needs: (p.needs || []).join(", "),
    bio: p.bio || "",
    linkedinUrl: p.linkedinUrl || "",
    twitterHandle: p.twitterHandle || "",
  };

  function field(label, key, opts = {}) {
    return el("div", { style: "margin-bottom: 12px;" }, [
      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;
              },
            },
            state[key] || "",
          )
        : el("input", {
            class: "wizard-input",
            type: opts.type || "text",
            placeholder: opts.placeholder || "",
            value: state[key] || "",
            oninput: (e) => {
              state[key] = e.target.value;
            },
          }),
      opts.hint ? el("div", { class: "muted small", style: "margin-top: 2px;" }, opts.hint) : null,
    ]);
  }

  const content = el("div", { style: "max-width: 720px; margin: 0 auto;" }, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "Your Profile"),
        el(
          "div",
          { class: "page-subtitle" },
          p.telegramUsername
            ? `@${p.telegramUsername} · how others find you`
            : "How others find you",
        ),
      ]),
      el(
        "button",
        { class: "btn btn-secondary btn-tiny", type: "button", onclick: renderProfile },
        "↻ Refresh",
      ),
    ]),

    // Identity card with metadata
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Profile status"),
      el("div", { class: "stats-grid" }, [
        statCard("Updated", p.updatedAt ? fmtDate(p.updatedAt) : "—", ""),
        statCard("Communities", communities.length, ""),
        statCard(
          "AI confidence",
          p.confidence != null ? `${Math.round((p.confidence || 0) * 100)}%` : "—",
          "from /intro",
        ),
      ]),
    ]),

    // Editable fields
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Identity"),
      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." }),
      field("Bio summary", "bio", {
        multiline: true,
        placeholder: "1–2 sentence summary that appears in matches",
        hint: "Replaces the AI-generated bio. Keep it short and concrete.",
      }),
    ]),

    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Skills · Offers · Needs"),
      el(
        "div",
        { class: "muted small", style: "margin-bottom: 12px;" },
        "Comma-separated. The matcher uses these to find people you should connect with.",
      ),
      field("Skills", "skills", {
        placeholder: "Solidity, growth marketing, fundraising",
        hint: "What you're good at",
      }),
      field("Offers", "offers", {
        placeholder: "Strategic advice, intro to investors, paid services",
        hint: "What you can give to others",
      }),
      field("Needs", "needs", {
        placeholder: "Co-founder, design help, intros to L2 teams",
        hint: "What you're looking for",
      }),
    ]),

    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Socials"),
      field("LinkedIn URL", "linkedinUrl", { placeholder: "https://linkedin.com/in/janedoe" }),
      field("Twitter / X handle", "twitterHandle", { placeholder: "janedoe (no @)" }),
    ]),

    communities.length
      ? el("div", { class: "card" }, [
          el("div", { class: "card-title" }, `Communities (${communities.length})`),
          el(
            "div",
            {},
            communities.map((c) =>
              el(
                "div",
                {
                  class: "muted",
                  style: "font-size: 12px; padding: 4px 0; border-bottom: 1px solid var(--border);",
                },
                c.name || c.title || `community ${c.id || c.community_id}`,
              ),
            ),
          ),
        ])
      : null,

    // Action bar
    el(
      "div",
      {
        style:
          "display: flex; gap: 8px; align-items: center; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border);",
      },
      [
        el(
          "button",
          {
            class: "btn btn-primary",
            type: "button",
            onclick: async (ev) => {
              if (!state.displayName.trim()) {
                toast("Display name is required", "err");
                return;
              }
              await withProgress(ev.currentTarget, async () => {
                const r = await api("profile-update", { body: state });
                if (r?.error) toast(r.error, "err");
                else {
                  toast("Profile saved", "ok");
                  renderProfile();
                }
              });
            },
          },
          "Save profile",
        ),
        el(
          "button",
          {
            class: "btn btn-secondary",
            type: "button",
            onclick: () => openTgCommand("/intro"),
          },
          "Re-run /intro in Telegram",
        ),
        el("div", { style: "flex: 1;" }),
        el(
          "button",
          {
            class: "btn btn-tiny btn-danger",
            type: "button",
            onclick: async () => {
              if (
                !confirm(
                  "Opt out and delete your profile? This is irreversible.\n\nAll your matches, conversations, and bookmarks will be wiped.",
                )
              )
                return;
              if (!confirm("Final confirmation — really delete?")) return;
              const r = await api("profile-delete");
              if (r?.error) toast(r.error, "err");
              else {
                toast("Profile deleted", "ok");
                navigate("home");
              }
            },
          },
          "Delete profile",
        ),
      ],
    ),
  ]);
  setContentNode(content);
}

function renderProfileNoProfile(err) {
  return el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [el("div", { class: "page-title" }, "Your Profile")]),
    ]),
    el("div", { class: "empty-state", style: "padding: 40px;" }, [
      el("div", { class: "empty-state-icon" }, icon("user")),
      el("div", { class: "empty-state-title" }, "Tell the network who you are"),
      el(
        "div",
        { class: "empty-state-hint", style: "max-width: 480px; margin: 12px auto;" },
        err?.message ||
          "Open Telegram and run /intro with a 1–2 paragraph description of yourself. The AI parses it into a structured profile that other Nexus members discover via /matches. After that, you can edit it from this page.",
      ),
      el("div", { style: "display: flex; gap: 8px; justify-content: center; margin-top: 16px;" }, [
        el(
          "button",
          {
            class: "btn btn-primary",
            type: "button",
            onclick: () => openTgCommand("/intro"),
          },
          "Open /intro in Telegram",
        ),
        el(
          "button",
          {
            class: "btn btn-secondary",
            type: "button",
            onclick: () => navigate("home"),
          },
          "← Home",
        ),
      ]),
    ]),
  ]);
}

// ─── Group Ops command center (Phase 6) ──────────────────────────
// Hub for Telegram group operations: scrape, announce, digest, group setup.
// Most live in the bot already; this page provides one-click access plus
// the Smart Scrape UI inline.

const GROUP_OPS = [
  {
    icon: "target",
    title: "Smart Scrape",
    desc: "Scrape members from a Telegram group by invite link.",
    in: "extension",
    route: "telegram/scrape",
  },
  {
    icon: "megaphone",
    title: "Announce",
    desc: "Send a one-time message to all groups your bot is in. 1/24h on free tier.",
    in: "telegram",
    command: "/announce",
  },
  {
    icon: "note",
    title: "Group Digest",
    desc: "Post a weekly summary of group activity (top members, topics, etc).",
    in: "telegram",
    command: "/groupdigest",
  },
  {
    icon: "settings",
    title: "Group Setup",
    desc: "Configure routing for matches, opportunities, and digests per community.",
    in: "telegram",
    command: "/groupsetup",
  },
  {
    icon: "flask",
    title: "Setup Scraper",
    desc: "Configure background scraping for a specific group with keyword/role filters.",
    in: "telegram",
    command: "/setupscraper",
  },
  {
    icon: "message",
    title: "Phone Check",
    desc: "Look up a Telegram phone number against your contacts pool.",
    in: "telegram",
    command: "/phonecheck",
  },
  {
    icon: "inbox",
    title: "Mass Invite",
    desc: "Bulk-invite users into your groups (rate-limited).",
    in: "extension",
    route: "telegram/invite",
  },
  {
    icon: "eye",
    title: "Passive Listener",
    desc: "Whitelist groups for background monitoring without joining.",
    in: "extension",
    route: "telegram/passive",
  },
];

// ─── Activity feed (live system event stream) ────────────────────
// Polls /api/activity/live with a `since` cursor so each tick only
// fetches new events. Pauses on tab blur or when the user scrolls up
// (so reading isn't disrupted). Cleans itself up on navigation.

const activityState = {
  events: [], // newest at top
  since: null, // ISO ts of last event we have
  pollHandle: null,
  paused: false,
  filter: { level: "", tag: "", q: "" },
  scrolledUp: false,
};
const ACTIVITY_POLL_MS = 8000;
const ACTIVITY_MAX_BUFFER = 500;

function activityLevelMeta(level) {
  switch (level) {
    case "ok":
      return { color: "var(--green)", badge: "OK" };
    case "warn":
      return { color: "var(--yellow)", badge: "WARN" };
    case "error":
      return { color: "var(--red)", badge: "ERR" };
    default:
      return { color: "var(--text-3)", badge: "INFO" };
  }
}

function activityShortTime(iso) {
  if (!iso) return "—";
  const d = new Date(iso);
  if (isNaN(d.getTime())) return "—";
  const h = String(d.getHours()).padStart(2, "0");
  const m = String(d.getMinutes()).padStart(2, "0");
  const s = String(d.getSeconds()).padStart(2, "0");
  return `${h}:${m}:${s}`;
}

function applyActivityFilter(events) {
  const { level, tag, q } = activityState.filter;
  const needle = (q || "").toLowerCase();
  return events.filter((ev) => {
    if (level && ev.level !== level) return false;
    if (tag && ev.tag !== tag) return false;
    if (needle && !`${ev.text} ${ev.tag}`.toLowerCase().includes(needle)) return false;
    return true;
  });
}

function activityEventNode(ev) {
  const { color, badge } = activityLevelMeta(ev.level);
  return el(
    "div",
    {
      class: "activity-row",
      style: `display: grid; grid-template-columns: 70px 50px 110px 1fr; gap: 8px; padding: 4px 8px; border-bottom: 1px solid var(--border); font-family: var(--font-mono); font-size: 11px; line-height: 1.5;`,
    },
    [
      el("span", { class: "muted", style: "white-space: nowrap;" }, activityShortTime(ev.ts)),
      el("span", { style: `color: ${color}; font-weight: 700;` }, badge),
      el(
        "span",
        {
          class: "muted",
          style: "white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",
        },
        ev.tag || "",
      ),
      el("span", { style: "white-space: pre-wrap; word-break: break-word;" }, ev.text || ""),
    ],
  );
}

function renderActivityList() {
  const list = $("#activity-list");
  if (!list) return;
  const filtered = applyActivityFilter(activityState.events);
  if (filtered.length === 0) {
    list.replaceChildren(
      el(
        "div",
        { class: "muted", style: "padding: 24px; text-align: center;" },
        activityState.events.length === 0 ? "Waiting for events…" : "No events match your filter",
      ),
    );
    return;
  }
  list.replaceChildren(...filtered.slice(0, 200).map(activityEventNode));
}

function updateActivityCounters() {
  const counter = $("#activity-counter");
  if (counter) {
    const n = activityState.events.length;
    counter.textContent = n === 0 ? "no events" : `${n} event${n === 1 ? "" : "s"}`;
  }
}

async function pollActivity() {
  if (activityState.paused) return;
  try {
    const qs = activityState.since ? `?since=${encodeURIComponent(activityState.since)}` : "";
    const r = await api("activity-live", { queryString: qs });
    if (r?.error) return;
    const fresh = (r.events || []).filter((e) => e?.ts);
    if (fresh.length > 0) {
      // server returns oldest-first; reverse so we prepend newest-first
      fresh.sort((a, b) => (a.ts > b.ts ? 1 : -1));
      activityState.since = fresh[fresh.length - 1].ts;
      activityState.events = [...fresh.reverse(), ...activityState.events].slice(
        0,
        ACTIVITY_MAX_BUFFER,
      );
      // Only repaint if user isn't actively scrolled up reading older events
      if (!activityState.scrolledUp) {
        renderActivityList();
      } else {
        // Show a "new events" pill at the top instead
        const pill = $("#activity-new-pill");
        if (pill) {
          pill.style.display = "inline-flex";
          pill.textContent = `↑ ${activityState.events.length} new — click to refresh`;
        }
      }
      updateActivityCounters();
    }
  } catch {
    /* swallow — next tick will retry */
  }
}

function stopActivityPoll() {
  if (activityState.pollHandle) {
    clearInterval(activityState.pollHandle);
    activityState.pollHandle = null;
  }
}

async function renderActivity() {
  // Reset state on each route entry so navigation away + back gives a fresh feed.
  stopActivityPoll();
  activityState.events = [];
  activityState.since = null;
  activityState.paused = false;
  activityState.scrolledUp = false;

  const filterChips = (label, levelKey) =>
    el(
      "button",
      {
        class: `btn btn-tiny ${activityState.filter.level === levelKey ? "btn-primary" : "btn-secondary"}`,
        type: "button",
        onclick: (ev) => {
          activityState.filter.level = activityState.filter.level === levelKey ? "" : levelKey;
          // Re-render the chips bar + list
          renderActivity();
        },
      },
      label,
    );

  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "Activity"),
        el(
          "div",
          { class: "page-subtitle" },
          "Live event stream from the bot — automations, errors, jobs",
        ),
      ]),
      el("div", { class: "row", style: "gap: 8px; align-items: center;" }, [
        el("span", { id: "activity-counter", class: "muted small mono" }, "no events"),
        el(
          "button",
          {
            id: "activity-pause-btn",
            class: "btn btn-tiny btn-secondary",
            type: "button",
            onclick: (ev) => {
              activityState.paused = !activityState.paused;
              ev.currentTarget.textContent = activityState.paused ? "▶ Resume" : "⏸ Pause";
            },
          },
          "⏸ Pause",
        ),
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            onclick: () => {
              activityState.events = [];
              activityState.since = null;
              renderActivityList();
              updateActivityCounters();
            },
          },
          "Clear",
        ),
      ]),
    ]),

    // Filter row
    el("div", { class: "filter-row" }, [
      el("input", {
        class: "filter-input",
        placeholder: "Search events / tags…",
        value: activityState.filter.q || "",
        oninput: (e) => {
          activityState.filter.q = e.target.value;
          renderActivityList();
        },
      }),
      filterChips("Info", "info"),
      filterChips("OK", "ok"),
      filterChips("Warn", "warn"),
      filterChips("Error", "error"),
    ]),

    // "New events" pill — shown if user scrolled up while events arrived
    el("div", { style: "padding: 4px 0;" }, [
      el(
        "button",
        {
          id: "activity-new-pill",
          class: "btn btn-tiny btn-primary",
          type: "button",
          style: "display: none;",
          onclick: () => {
            activityState.scrolledUp = false;
            const pill = $("#activity-new-pill");
            if (pill) pill.style.display = "none";
            renderActivityList();
            // Scroll back to top of list
            const list = $("#activity-list");
            if (list) list.scrollTop = 0;
          },
        },
        "↑ new events",
      ),
    ]),

    // Stream
    el(
      "div",
      {
        id: "activity-list",
        style:
          "max-height: calc(100vh - 260px); overflow-y: auto; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-1);",
        onscroll: (ev) => {
          const t = ev.currentTarget;
          // Treat "scrolled up" as "more than 80px from top". Resets on scroll-back.
          activityState.scrolledUp = t.scrollTop > 80;
          if (!activityState.scrolledUp) {
            const pill = $("#activity-new-pill");
            if (pill) pill.style.display = "none";
          }
        },
      },
      [
        el(
          "div",
          { class: "muted", style: "padding: 24px; text-align: center;" },
          "Waiting for events…",
        ),
      ],
    ),

    // Footer hint
    el(
      "div",
      { class: "muted small", style: "padding: 8px 0; text-align: center;" },
      `Polling every ${Math.round(ACTIVITY_POLL_MS / 1000)}s · scroll up to pin · last ${ACTIVITY_MAX_BUFFER} events buffered`,
    ),
  ]);
  setContentNode(content);

  // Initial fetch then start polling
  await pollActivity();
  renderActivityList();
  updateActivityCounters();
  activityState.pollHandle = setInterval(pollActivity, ACTIVITY_POLL_MS);
}

// ─── Outreach Queue (orchestrator visibility) ────────────────────
// Three buckets sourced from /api/outreach/queue?bucket=...
//   Sending now — about to fire (queued + scheduled <= now)
//   Queued      — waiting (scheduled in the future)
//   Failed      — needs attention (per-channel failure)
// Per-card actions: Retry the failed channel, Cancel the channel, or
// pause the whole campaign.

let outreachQueueBucket = "sending";

// Reuses CHANNEL_ICONS defined earlier in dashboard.js (line ~1153) — same
// {x, linkedin, telegram, email} keys, just slightly different glyphs.
const CHANNEL_STATUS_META = {
  queued: { color: "var(--yellow)", label: "queued" },
  sending: { color: "var(--accent)", label: "sending" },
  sent: { color: "var(--green)", label: "sent" },
  replied: { color: "var(--green)", label: "replied" },
  failed: { color: "var(--red)", label: "failed" },
  skipped: { color: "var(--text-4)", label: "skipped" },
};

async function renderOutreachQueue() {
  showLoading("Loading outreach queue…");
  const data = await api("outreach-queue", {
    queryString: `?bucket=${outreachQueueBucket}&limit=80`,
  }).catch((err) => ({ error: err.message }));
  if (data?.error) {
    showError(data.error, renderOutreachQueue);
    return;
  }

  const counts = data.counts || { sending: 0, queued: 0, failed: 0 };
  const allCampaigns = data.campaigns || [];

  // Apply contextAccount filter — keep only queue rows where the picker's
  // platform appears in the row's channels[]. Falls open when no context set.
  const ctx = dashboardState.contextAccount;
  const channelKey = ctx
    ? {
        twitter: ["twitter", "x"],
        linkedin: ["linkedin"],
        telegram: ["telegram"],
        smtp: ["email"],
      }[ctx.platform]
    : null;
  const campaigns =
    channelKey && Array.isArray(channelKey)
      ? allCampaigns.filter((c) => (c.channels || []).some((ch) => channelKey.includes(ch.channel)))
      : allCampaigns;
  const filteredOut = allCampaigns.length - campaigns.length;

  function tab(label, key, count) {
    return tabBtn(key, count != null ? `${label} (${count})` : label, () => {
      outreachQueueBucket = key;
      renderOutreachQueue();
    });
  }

  const header = el("div", { class: "page-header" }, [
    el("div", {}, [
      el("div", { class: "page-title" }, "Outreach Queue"),
      el(
        "div",
        { class: "page-subtitle" },
        "What the orchestrator is sending, queued, or failed on right now",
      ),
    ]),
    el(
      "button",
      { class: "btn btn-secondary btn-tiny", type: "button", onclick: renderOutreachQueue },
      "↻ Refresh",
    ),
  ]);

  const tabs = el("div", { class: "tabs" }, [
    tab("Sending now", "sending", counts.sending),
    tab("Queued", "queued", counts.queued),
    tab("Failed", "failed", counts.failed),
    tab("All recent", "all"),
  ]);
  setTimeout(() => {
    $$(".tab").forEach((t) =>
      t.classList.toggle("active", t.getAttribute("data-tab") === outreachQueueBucket),
    );
  }, 0);

  const body =
    campaigns.length === 0
      ? el("div", { class: "empty-state", style: "padding: 40px;" }, [
          el("div", { class: "empty-state-icon" }, outreachQueueBucket === "failed" ? "✓" : "⏸"),
          el(
            "div",
            { class: "empty-state-title" },
            {
              sending: "Nothing sending right now",
              queued: "Nothing queued for later",
              failed: "No failures",
              all: "Queue is empty",
            }[outreachQueueBucket] || "Nothing here",
          ),
          el(
            "div",
            { class: "empty-state-hint" },
            ctx && filteredOut > 0
              ? `${filteredOut} hidden by the account filter (${escapeHtml(ctx.label)}). Clear the filter from the top bar to see them.`
              : outreachQueueBucket === "failed"
                ? "Nice — all your channels are happy. Failures show up here when an account has a session expiry, rate-limit, or upstream API error."
                : outreachQueueBucket === "sending"
                  ? "The orchestrator runs on a schedule. When campaigns are due, they'll appear here briefly before sending."
                  : "Create an outreach campaign to start the engine.",
          ),
        ])
      : el("div", {}, campaigns.map(renderQueueCard));

  setContentNode(el("div", {}, [header, _runHubNav("outreach-queue"), tabs, body]));
}

function renderQueueCard(c) {
  const channelChips = (c.channels || []).map((ch) => {
    const meta = CHANNEL_STATUS_META[ch.status] || CHANNEL_STATUS_META.queued;
    return el(
      "div",
      {
        style: `display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--bg-1); margin-right: 4px; font-size: 11px;`,
        title: ch.lastError ? `Last error: ${ch.lastError}` : `${ch.channel} · ${ch.status}`,
      },
      [
        el("span", {}, CHANNEL_ICONS[ch.channel] || "•"),
        el("span", { style: `color: ${meta.color}; font-weight: 600;` }, meta.label),
        // Per-channel actions
        ch.status === "failed"
          ? el(
              "button",
              {
                class: "btn btn-tiny btn-primary",
                style: "padding: 0 6px; font-size: 10px; margin-left: 4px;",
                type: "button",
                title: "Retry this channel",
                onclick: async (ev) => {
                  ev.stopPropagation();
                  await withProgress(ev.currentTarget, async () => {
                    const r = await api("outreach-channel-retry", {
                      campaignId: c.campaignId,
                      channel: ch.channel,
                    });
                    if (r?.error) toast(r.error, "err");
                    else {
                      toast(`${ch.channel} re-queued`, "ok");
                      renderOutreachQueue();
                    }
                  });
                },
              },
              "↻",
            )
          : null,
        ch.status === "queued" || ch.status === "failed"
          ? el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                style: "padding: 0 6px; font-size: 10px; margin-left: 2px;",
                type: "button",
                title: "Skip this channel",
                onclick: async (ev) => {
                  ev.stopPropagation();
                  if (!confirm(`Skip the ${ch.channel} channel for ${c.contactName}?`)) return;
                  await withProgress(ev.currentTarget, async () => {
                    const r = await api("outreach-channel-cancel", {
                      campaignId: c.campaignId,
                      channel: ch.channel,
                    });
                    if (r?.error) toast(r.error, "err");
                    else {
                      toast(`${ch.channel} skipped`, "ok");
                      renderOutreachQueue();
                    }
                  });
                },
              },
              "✕",
            )
          : null,
      ],
    );
  });

  // Show first failure reason inline if any
  const firstFailure = (c.channels || []).find((ch) => ch.status === "failed" && ch.lastError);

  // Schedule indicator
  const sched = c.scheduledStartAt
    ? new Date(c.scheduledStartAt).getTime() > Date.now()
      ? `Scheduled ${fmtDate(c.scheduledStartAt)}`
      : `Due ${fmtDate(c.scheduledStartAt)}`
    : null;

  const isPaused = c.overallStatus === "paused";

  return el(
    "div",
    {
      class: "card",
      style: `padding: 12px; margin-bottom: 8px; ${isPaused ? "opacity: 0.6;" : ""}`,
    },
    [
      el("div", { style: "display: flex; align-items: flex-start; gap: 12px;" }, [
        // Avatar bubble
        el(
          "div",
          {
            style:
              "width: 32px; height: 32px; border-radius: 50%; background: var(--accent-soft); color: var(--accent); display: flex; align-items: center; justify-content: center; font-weight: 700; flex-shrink: 0; font-size: 12px;",
          },
          (c.contactName || "?").slice(0, 2).toUpperCase(),
        ),
        // Body
        el("div", { style: "flex: 1; min-width: 0;" }, [
          el("div", { style: "display: flex; align-items: center; gap: 8px; flex-wrap: wrap;" }, [
            el("span", { class: "primary-cell" }, c.contactName),
            c.companyName ? el("span", { class: "muted small" }, `@ ${c.companyName}`) : null,
            isPaused ? el("span", { class: "badge badge-gray" }, "paused") : null,
            sched ? el("span", { class: "muted small" }, sched) : null,
          ]),
          el("div", { style: "margin-top: 6px;" }, channelChips),
          c.customMessage
            ? el(
                "div",
                {
                  class: "muted",
                  style:
                    "font-size: 11px; margin-top: 6px; padding: 6px 8px; background: var(--bg-1); border-radius: var(--radius-sm); font-style: italic; line-height: 1.5;",
                },
                `"${c.customMessage}"`,
              )
            : null,
          firstFailure
            ? el(
                "div",
                {
                  class: "muted small",
                  style: "margin-top: 6px; color: var(--red);",
                },
                `${CHANNEL_ICONS[firstFailure.channel]} ${firstFailure.lastError}`,
              )
            : null,
        ]),
        // Right rail actions
        el("div", { style: "display: flex; flex-direction: column; gap: 4px; flex-shrink: 0;" }, [
          c.contactId
            ? el(
                "button",
                {
                  class: "btn btn-tiny btn-secondary",
                  type: "button",
                  onclick: () => showContactDrawer(c.contactId),
                  title: "View contact",
                },
                "👤",
              )
            : null,
          isPaused
            ? el(
                "button",
                {
                  class: "btn btn-tiny btn-primary",
                  type: "button",
                  onclick: async (ev) => {
                    await withProgress(ev.currentTarget, async () => {
                      const r = await api("outreach-resume", { campaignId: c.campaignId });
                      if (r?.error) toast(r.error, "err");
                      else {
                        toast("Resumed", "ok");
                        renderOutreachQueue();
                      }
                    });
                  },
                },
                "▶",
              )
            : el(
                "button",
                {
                  class: "btn btn-tiny btn-danger",
                  type: "button",
                  title: "Pause the whole campaign",
                  onclick: async (ev) => {
                    if (
                      !confirm(
                        `Pause the entire campaign for ${c.contactName}? You can resume later.`,
                      )
                    )
                      return;
                    await withProgress(ev.currentTarget, async () => {
                      const r = await api("outreach-cancel-all", { campaignId: c.campaignId });
                      if (r?.error) toast(r.error, "err");
                      else {
                        toast("Paused", "ok");
                        renderOutreachQueue();
                      }
                    });
                  },
                },
                "⏸",
              ),
        ]),
      ]),
    ],
  );
}

// ─── Bookmarks ───────────────────────────────────────────────────
// Single page for everything the user has starred — profiles, companies,
// opportunities. The People drawer's ★ Bookmark button writes to the
// same table; this is just the read-side view.

let bookmarksTab = "all";

async function renderBookmarks() {
  showLoading("Loading bookmarks…");

  const [listRes, countsRes] = await Promise.all([
    api("bookmarks-list", {
      queryString: bookmarksTab === "all" ? "?limit=200" : `?type=${bookmarksTab}&limit=200`,
    }).catch((err) => ({ error: err.message })),
    api("bookmarks-counts").catch(() => ({})),
  ]);

  if (listRes?.error) {
    showError(listRes.error, renderBookmarks);
    return;
  }

  const bookmarks = listRes.bookmarks || [];
  const stale = listRes.stale || 0;
  const counts = countsRes || {};
  const total = (counts.profile || 0) + (counts.company || 0) + (counts.opportunity || 0);

  function tab(label, key, count) {
    return tabBtn(key, count != null ? `${label} (${count})` : label, () => {
      bookmarksTab = key;
      renderBookmarks();
    });
  }

  function bookmarkCard(b) {
    const typeIcon =
      b.type === "profile"
        ? "👤"
        : b.type === "company"
          ? "🏢"
          : b.type === "opportunity"
            ? "🎯"
            : "•";
    const typeBadge = el(
      "span",
      {
        class: `badge ${b.type === "profile" ? "badge-blue" : b.type === "company" ? "badge-purple" : "badge-green"}`,
      },
      b.type,
    );

    return el(
      "div",
      {
        class: "card",
        style: "padding: 14px; margin-bottom: 8px; cursor: pointer;",
        onclick: (ev) => {
          if (ev.target.closest("button")) return;
          // Click-through to the underlying detail
          if (b.type === "profile") openPeopleDetail(b.itemId);
          else if (b.type === "company" && typeof showCompanyDrawer === "function")
            showCompanyDrawer(b.itemId);
          else if (b.type === "opportunity" && b.meta?.sourceUrl)
            chrome.tabs.create({ url: b.meta.sourceUrl });
        },
      },
      [
        el("div", { style: "display: flex; gap: 12px; align-items: flex-start;" }, [
          el("div", { style: "font-size: 28px; flex-shrink: 0;" }, typeIcon),
          el("div", { style: "flex: 1; min-width: 0;" }, [
            el("div", { class: "primary-cell", style: "font-size: 14px;" }, [
              b.title,
              el("span", { style: "margin-left: 8px;" }, [typeBadge]),
            ]),
            b.subtitle
              ? el(
                  "div",
                  { class: "muted", style: "font-size: 12px; margin-top: 2px;" },
                  b.subtitle,
                )
              : null,
            b.bio
              ? el(
                  "div",
                  { class: "muted", style: "font-size: 12px; margin-top: 6px; line-height: 1.5;" },
                  b.bio,
                )
              : null,
            el(
              "div",
              { class: "muted small", style: "margin-top: 6px;" },
              `Saved ${fmtDate(b.createdAt)}`,
            ),
          ]),
          el("div", { style: "flex-shrink: 0; display: flex; gap: 4px;" }, [
            // External links if available
            b.meta?.linkedinUrl
              ? el(
                  "a",
                  {
                    class: "btn btn-tiny btn-secondary",
                    href: b.meta.linkedinUrl,
                    target: "_blank",
                    onclick: (ev) => ev.stopPropagation(),
                  },
                  "LI",
                )
              : null,
            b.meta?.twitterHandle
              ? el(
                  "a",
                  {
                    class: "btn btn-tiny btn-secondary",
                    href: `https://x.com/${String(b.meta.twitterHandle).replace(/^@/, "")}`,
                    target: "_blank",
                    onclick: (ev) => ev.stopPropagation(),
                  },
                  "X",
                )
              : null,
            b.meta?.website
              ? el(
                  "a",
                  {
                    class: "btn btn-tiny btn-secondary",
                    href: b.meta.website,
                    target: "_blank",
                    onclick: (ev) => ev.stopPropagation(),
                  },
                  "Site",
                )
              : null,
            el(
              "button",
              {
                class: "btn btn-tiny btn-danger",
                type: "button",
                title: "Remove bookmark",
                onclick: async (ev) => {
                  ev.stopPropagation();
                  await withProgress(ev.currentTarget, async () => {
                    const r = await api("bookmark-remove", { type: b.type, id: b.itemId });
                    if (r?.error) toast(r.error, "err");
                    else {
                      toast("Removed", "ok");
                      renderBookmarks();
                    }
                  });
                },
              },
              "✕",
            ),
          ]),
        ]),
      ],
    );
  }

  const header = el("div", { class: "page-header" }, [
    el("div", {}, [
      el("div", { class: "page-title" }, "Bookmarks"),
      el("div", { class: "page-subtitle" }, total > 0 ? `${total} saved` : "Things you've starred"),
    ]),
    el(
      "button",
      { class: "btn btn-secondary btn-tiny", type: "button", onclick: renderBookmarks },
      "↻ Refresh",
    ),
  ]);

  const tabs = el("div", { class: "tabs" }, [
    tab("All", "all", total || null),
    tab("Profiles", "profile", counts.profile || 0),
    tab("Companies", "company", counts.company || 0),
    tab("Opportunities", "opportunity", counts.opportunity || 0),
  ]);

  setTimeout(() => {
    $$(".tab").forEach((t) =>
      t.classList.toggle("active", t.getAttribute("data-tab") === bookmarksTab),
    );
  }, 0);

  const body =
    bookmarks.length === 0
      ? el("div", { class: "empty-state", style: "padding: 40px;" }, [
          el("div", { class: "empty-state-icon" }, icon("star")),
          el(
            "div",
            { class: "empty-state-title" },
            bookmarksTab === "all" ? "No bookmarks yet" : `No ${bookmarksTab} bookmarks`,
          ),
          el(
            "div",
            { class: "empty-state-hint" },
            "Bookmark profiles from People, companies from CRM, and opportunities from the digest to save them here for later.",
          ),
        ])
      : el("div", {}, bookmarks.map(bookmarkCard));

  const staleNote =
    stale > 0
      ? el(
          "div",
          {
            class: "muted small",
            style:
              "padding: 8px 12px; background: var(--bg-1); border-radius: var(--radius-sm); margin-bottom: 8px;",
          },
          `ℹ ${stale} bookmark${stale === 1 ? "" : "s"} point to records that have been removed and aren't shown.`,
        )
      : null;

  setContentNode(el("div", {}, [header, tabs, staleNote, body]));
}

async function renderGroupOps() {
  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "Group Ops"),
        el(
          "div",
          { class: "page-subtitle" },
          "Telegram group operations — scrape, announce, configure, monitor",
        ),
      ]),
    ]),

    el("div", { class: "card" }, [
      el("div", { class: "muted", style: "font-size: 13px; line-height: 1.6;" }, [
        "Some operations live in the extension (full UI) and some in the Telegram bot ",
        el("span", { class: "badge badge-blue" }, "TG"),
        " (require interactive flows that don't translate well to a browser tab). Click any card below.",
      ]),
    ]),

    el(
      "div",
      { class: "action-card-grid", style: "margin-top: 8px;" },
      GROUP_OPS.map((op) =>
        el(
          "button",
          {
            class: "action-card",
            type: "button",
            onclick: () => {
              if (op.in === "extension") navigate(op.route);
              else openTgCommand(op.command);
            },
          },
          [
            el("div", { class: "action-card-icon" }, op.icon),
            el("div", { class: "action-card-body" }, [
              el("div", { class: "action-card-title" }, [
                el("span", null, op.title),
                op.in === "telegram"
                  ? el("span", { class: "badge badge-blue" }, "TG")
                  : el("span", { class: "badge badge-green" }, "UI"),
              ]),
              el("div", { class: "action-card-desc" }, op.desc),
              el(
                "div",
                { class: "action-card-meta" },
                op.in === "extension" ? `→ ${op.route}` : op.command,
              ),
            ]),
            el("div", { class: "action-card-cta" }, op.in === "extension" ? "→" : "↗"),
          ],
        ),
      ),
    ),

    // Quick links
    el("div", { class: "card", style: "margin-top: 16px;" }, [
      el("div", { class: "card-title" }, "Related"),
      el("div", { class: "row", style: "gap: 8px; flex-wrap: wrap;" }, [
        el(
          "button",
          {
            class: "btn btn-secondary btn-tiny",
            type: "button",
            onclick: () => navigate("telegram"),
          },
          "Telegram hub",
        ),
        el(
          "button",
          {
            class: "btn btn-secondary btn-tiny",
            type: "button",
            onclick: () => navigate("competitors"),
          },
          "Competitor Intel",
        ),
        el(
          "button",
          { class: "btn btn-secondary btn-tiny", type: "button", onclick: () => navigate("crm") },
          "CRM",
        ),
      ]),
    ]),
  ]);
  setContentNode(content);
}

// ─── Growth landing (Phase 6 polish) ─────────────────────────────
// Single entry point that mirrors the Telegram /growth menu — one card per
// platform with a quick-jump into its hub plus aggregate stats.

async function renderGrowth() {
  showLoading("Loading growth surface…");
  const accountsData = await api("accounts-all").catch(() => ({}));

  const platforms = [
    { id: "telegram", meta: PLATFORM_META.telegram, accounts: accountsData?.telegram || [] },
    { id: "x", meta: PLATFORM_META.x, accounts: accountsData?.twitter || [] },
    { id: "linkedin", meta: PLATFORM_META.linkedin, accounts: accountsData?.linkedin || [] },
    { id: "email", meta: PLATFORM_META.email, accounts: accountsData?.smtp || [] },
  ];

  const totalAccounts = platforms.reduce((sum, p) => sum + p.accounts.length, 0);
  const totalActive = platforms.reduce(
    (sum, p) => sum + p.accounts.filter((a) => a.status === "active").length,
    0,
  );
  const totalLimited = platforms.reduce(
    (sum, p) => sum + p.accounts.filter((a) => a.status === "rate_limited").length,
    0,
  );
  const opsCount = Object.values(PLATFORM_OPERATIONS).reduce((sum, ops) => sum + ops.length, 0);

  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, "Growth"),
        el(
          "div",
          { class: "page-subtitle" },
          "Multi-channel marketing — pick a platform to drill in",
        ),
      ]),
      el(
        "button",
        { class: "btn btn-secondary btn-tiny", type: "button", onclick: renderGrowth },
        "↻ Refresh",
      ),
    ]),

    // Aggregate stats across all platforms
    el("div", { class: "card" }, [
      el("div", { class: "stats-grid" }, [
        statCard(
          "Connected accounts",
          totalAccounts,
          `across ${platforms.filter((p) => p.accounts.length > 0).length} platforms`,
        ),
        statCard("Active", totalActive, `${totalAccounts - totalActive} idle/offline`),
        statCard("Rate-limited", totalLimited, "", totalLimited > 0 ? "warning" : null),
        statCard("Tools available", opsCount, "across all platforms"),
      ]),
    ]),

    // Platform cards — one per platform with quick-jump
    el(
      "div",
      {
        style:
          "display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; margin-top: 8px;",
      },
      platforms.map((p) => {
        const isLocked = p.accounts.length === 0;
        const opsList = PLATFORM_OPERATIONS[p.id] || [];
        const active = p.accounts.filter((a) => a.status === "active").length;

        return el(
          "div",
          {
            class: "card",
            style: `padding: 16px; border: 1px solid var(--border); ${isLocked ? "opacity: 0.7;" : ""}`,
          },
          [
            // Header row
            el(
              "div",
              { style: "display: flex; align-items: center; gap: 12px; margin-bottom: 12px;" },
              [
                el("div", { style: "font-size: 32px;" }, p.meta.emoji),
                el("div", { style: "flex: 1;" }, [
                  el("div", { class: "primary-cell", style: "font-size: 16px;" }, p.meta.name),
                  el(
                    "div",
                    { class: "muted small" },
                    isLocked
                      ? "Not connected"
                      : `${active}/${p.accounts.length} active · ${opsList.length} tools`,
                  ),
                ]),
              ],
            ),

            // Quick stats
            !isLocked
              ? el("div", { class: "stats-grid", style: "margin-bottom: 12px;" }, [
                  statCard("Accounts", `${p.accounts.length}/5`, ""),
                  statCard("Active", active, ""),
                ])
              : null,

            // Top tools preview (first 4)
            !isLocked && opsList.length > 0
              ? el("div", { style: "margin-bottom: 12px;" }, [
                  el("div", { class: "muted small", style: "margin-bottom: 6px;" }, "Tools"),
                  el(
                    "div",
                    { class: "filter-pills" },
                    opsList
                      .slice(0, 4)
                      .map((op) =>
                        el("div", { class: "filter-pill" }, [
                          icon.for(op.icon) ? icon(icon.for(op.icon)) : el("span", {}, op.icon),
                          " " + op.title,
                        ]),
                      ),
                  ),
                  opsList.length > 4
                    ? el(
                        "div",
                        { class: "muted small", style: "margin-top: 4px;" },
                        `+ ${opsList.length - 4} more`,
                      )
                    : null,
                ])
              : null,

            // Action buttons
            el(
              "div",
              { style: "display: flex; gap: 6px; flex-wrap: wrap;" },
              isLocked
                ? [
                    el(
                      "button",
                      {
                        class: "btn btn-primary btn-tiny",
                        type: "button",
                        onclick: () => navigate(`accounts/${p.id}`),
                      },
                      `+ Connect ${p.meta.name}`,
                    ),
                  ]
                : [
                    el(
                      "button",
                      {
                        class: "btn btn-primary btn-tiny",
                        type: "button",
                        onclick: () => navigate(`${p.id}/overview`),
                      },
                      "Overview",
                    ),
                    el(
                      "button",
                      {
                        class: "btn btn-secondary btn-tiny",
                        type: "button",
                        onclick: () => navigate(`${p.id}/operations`),
                      },
                      "Operations",
                    ),
                    el(
                      "button",
                      {
                        class: "btn btn-secondary btn-tiny",
                        type: "button",
                        onclick: () => navigate(`${p.id}/monitor`),
                      },
                      "Monitor",
                    ),
                  ],
            ),
          ],
        );
      }),
    ),

    // Cross-platform quick links
    el("div", { class: "card", style: "margin-top: 16px;" }, [
      el("div", { class: "card-title" }, "Cross-platform"),
      el("div", { class: "row", style: "gap: 8px; flex-wrap: wrap;" }, [
        el(
          "button",
          {
            class: "btn btn-secondary btn-tiny",
            type: "button",
            onclick: () => navigate("campaigns"),
          },
          "All campaigns",
        ),
        el(
          "button",
          {
            class: "btn btn-secondary btn-tiny",
            type: "button",
            onclick: () => navigate("competitors"),
          },
          "Competitor Intel",
        ),
        el(
          "button",
          {
            class: "btn btn-secondary btn-tiny",
            type: "button",
            onclick: () => navigate("service-match"),
          },
          "Service Match",
        ),
        el(
          "button",
          {
            class: "btn btn-secondary btn-tiny",
            type: "button",
            onclick: () => navigate("strategies"),
          },
          "Strategies",
        ),
        el(
          "button",
          {
            class: "btn btn-secondary btn-tiny",
            type: "button",
            onclick: () => navigate("group-ops"),
          },
          "Group Ops",
        ),
      ]),
    ]),
  ]);
  setContentNode(content);
}

// ── Phase 13: inline help tooltips ────────────────────────────────
//
// Tiny helper for wizard field hints. Uses the browser's native title
// attribute so no popup JS is needed. Hot-reloadable via the COMPOSER_HELP
// dictionary below.

const COMPOSER_HELP = {
  combine_mode:
    "How multiple audience sources merge. Union = any source includes them. Intersect = ALL sources must include them. Exclude = first source MINUS others.",
  recency_days:
    "Skip contacts already targeted by ANY composition in the last N days. Default 30 — prevents double-touching. Set to 0 to disable.",
  channel_use_when:
    "When to route a contact through this channel. has_<platform> = only when the contact has that handle. always = use unconditionally (e.g., for broadcast posts).",
  voice_picker:
    "AI voice profile injected at send time. 'Account default' uses the sender account's tuned voice. Presets are hand-tuned personalities.",
  canary_mode:
    "Run the first sample % of audience first. After wait minutes, the scheduler checks the canary's error rate. Above threshold → composition auto-pauses. Otherwise → main batch fires.",
  sandbox:
    "Hand-picked contact IDs (typically yourself on different platforms). 'Run on sandbox' executes plays for real, but only against this list — receive your own DM and judge it before unleashing on real audience.",
  trigger_event:
    "Composition fires once per matching event from inbound scanners. The triggering contact is the audience.",
  trigger_schedule:
    "Composition fires on a cron schedule. The audience is re-resolved at each fire time, so 'recently funded companies' picks up new ones each run.",
  trigger_manual: "Composition only fires when you click Run-now. Useful for one-off blasts.",
  share_weight:
    "Share weight (default 1.0). When multiple compositions use this account, the runner divides the daily cap proportionally to weights. Bump to 2 to take 2× share; drop to 0.5 to take half.",
};

function helpIcon(key) {
  const text = COMPOSER_HELP[key];
  if (!text) return null;
  return el(
    "span",
    {
      title: text,
      style:
        "display: inline-block; width: 14px; height: 14px; line-height: 14px; text-align: center; border-radius: 50%; background: #2a2a3a; color: var(--text-2); font-size: 9px; cursor: help; margin-left: 6px;",
    },
    "?",
  );
}

// Expose so any UI that builds wizard fields can use it
try {
  window.helpIcon = helpIcon;
  window.COMPOSER_HELP = COMPOSER_HELP;
} catch {}

// ── WHERE step (Phase 3 — full) ────────────────────────────────────

function _composerWhereForm(draft, accounts, onChange) {
  const root = el("div", {});

  // Platform-lock banner: derive the set of platforms allowed by the
  // chosen sources. If any source has no compatible_platforms entry, we
  // assume cross-platform; otherwise intersect across all sources.
  const allowedPlatforms = _composerAllowedPlatformsForSources(draft.audience.resolvers || []);
  const isLocked = allowedPlatforms !== null && allowedPlatforms.length < 4;
  if (isLocked) {
    root.appendChild(
      el(
        "div",
        {
          style:
            "padding: 8px 10px; background: #2a1a1a; border: 1px solid #5a3a3a; border-radius: 4px; font-size: 11px; margin-bottom: 10px; color: var(--red);",
        },
        `Selected source(s) only support: ${allowedPlatforms.join(", ")}. Other platforms hidden.`,
      ),
    );
  }

  const channelList = el("div", { style: "display: flex; flex-direction: column; gap: 6px;" });

  const renderChannels = () => {
    channelList.innerHTML = "";
    if (draft.channels.length === 0) {
      channelList.appendChild(
        el(
          "div",
          { class: "muted", style: "font-size: 12px; padding: 8px;" },
          "No channel yet. Pick a platform + account below. Multiple = fallback chain.",
        ),
      );
      return;
    }
    draft.channels.forEach((ch, i) => {
      const acc = accounts.find((a) => Number(a.id) === Number(ch.account_id));
      const isLocked = acc && acc.lockout_state && acc.lockout_state !== "ok";
      const label = `${i + 1}. ${ch.platform} · ${acc?.account_identifier || acc?.label || `account ${ch.account_id}`}${isLocked ? " 🔒" : ""}`;
      const useWhenInput = el(
        "select",
        { class: "wizard-input", style: "max-width: 180px; font-size: 11px;" },
        [
          el(
            "option",
            { value: `has_${ch.platform === "twitter" ? "x" : ch.platform}` },
            `Use only if has ${ch.platform}`,
          ),
          el("option", { value: "always" }, "Always (skip handle check)"),
        ],
      );
      useWhenInput.value = ch.use_when || `has_${ch.platform === "twitter" ? "x" : ch.platform}`;
      useWhenInput.addEventListener("change", () => {
        ch.use_when = useWhenInput.value;
        onChange?.();
      });

      // Phase 20: per-channel share weight (default 1.0). When multiple
      // active comps use the same account, the runner divides the daily
      // cap proportionally to weights.
      const shareInput = el("input", {
        type: "number",
        step: "0.5",
        min: "0.1",
        max: "10",
        class: "wizard-input",
        value: ch.account_share_weight ?? 1,
        title:
          "Share weight: when multiple compositions use this account, weight / sum-of-weights = your daily slice. Default 1.0.",
        style: "width: 60px; font-size: 11px;",
      });
      shareInput.addEventListener("change", () => {
        const v = parseFloat(shareInput.value);
        ch.account_share_weight = Number.isFinite(v) && v > 0 ? v : 1.0;
        onChange?.();
      });

      // Phase 25 (surfaced in 33): per-channel daily_cap_override. Lets a
      // composition cap itself tighter than the platform default (e.g.
      // 10/day on a probationary new LI account, even though platform
      // default is 30). Empty input = use platform default.
      const capOverrideInput = el("input", {
        type: "number",
        min: "1",
        class: "wizard-input",
        value: ch.daily_cap_override ?? "",
        placeholder: "default",
        title:
          "Daily cap override: tighter cap for THIS composition's use of this account. Leave blank to use the platform default (30 for tg/x/li, 100 for email).",
        style: "width: 70px; font-size: 11px;",
      });
      capOverrideInput.addEventListener("change", () => {
        const v = parseInt(capOverrideInput.value, 10);
        if (Number.isFinite(v) && v > 0) ch.daily_cap_override = v;
        else delete ch.daily_cap_override;
        onChange?.();
      });

      const upBtn = el(
        "button",
        {
          class: "btn btn-tiny btn-secondary",
          type: "button",
          disabled: i === 0 ? "disabled" : null,
          title: "Move up (higher priority)",
          onclick: () => {
            if (i === 0) return;
            [draft.channels[i - 1], draft.channels[i]] = [draft.channels[i], draft.channels[i - 1]];
            renderChannels();
            onChange?.();
          },
        },
        "↑",
      );
      const downBtn = el(
        "button",
        {
          class: "btn btn-tiny btn-secondary",
          type: "button",
          disabled: i === draft.channels.length - 1 ? "disabled" : null,
          title: "Move down (lower priority)",
          onclick: () => {
            if (i === draft.channels.length - 1) return;
            [draft.channels[i + 1], draft.channels[i]] = [draft.channels[i], draft.channels[i + 1]];
            renderChannels();
            onChange?.();
          },
        },
        "↓",
      );

      channelList.appendChild(
        el(
          "div",
          {
            style: `padding: 8px 10px; background: ${isLocked ? "#2a1a1a" : "#0c0c0c"}; border: 1px solid ${isLocked ? "#5a3a3a" : "#222"}; border-radius: 4px; gap: 8px; align-items: center; display: flex;`,
          },
          [
            el("div", { style: "font-size: 12px; flex: 1;" }, [
              el("div", {}, label),
              isLocked
                ? el(
                    "div",
                    { style: "font-size: 10px; color: var(--red); margin-top: 2px;" },
                    `Account locked: ${acc.lockout_reason || acc.lockout_state}. Recover before activating.`,
                  )
                : null,
            ]),
            useWhenInput,
            el("span", { class: "muted", style: "font-size: 10px;" }, "weight:"),
            shareInput,
            helpIcon ? helpIcon("share_weight") : null,
            el("span", { class: "muted", style: "font-size: 10px;" }, "cap:"),
            capOverrideInput,
            upBtn,
            downBtn,
            el(
              "button",
              {
                class: "btn btn-tiny btn-danger",
                type: "button",
                onclick: () => {
                  draft.channels.splice(i, 1);
                  renderChannels();
                  onChange?.();
                },
              },
              "✕",
            ),
          ].filter(Boolean),
        ),
      );
    });
  };

  // Add bar — pick platform → pick account → add
  const platformOptions = ["telegram", "linkedin", "twitter", "email"]
    .filter((p) => allowedPlatforms === null || allowedPlatforms.includes(p))
    .map((p) =>
      el("option", { value: p }, p === "twitter" ? "Twitter / X" : p[0].toUpperCase() + p.slice(1)),
    );
  const platformSelect = el(
    "select",
    { class: "wizard-input", style: "max-width: 130px;" },
    platformOptions,
  );

  const accountSelect = el("select", { class: "wizard-input", style: "max-width: 240px;" });
  const refreshAccountSelect = () => {
    const platform = platformSelect.value;
    accountSelect.innerHTML = "";
    const filtered = (accounts || []).filter((a) => String(a.platform).toLowerCase() === platform);
    if (filtered.length === 0) {
      accountSelect.appendChild(el("option", { value: "" }, "(no accounts)"));
    } else {
      filtered.forEach((a) => {
        const isLocked = a.lockout_state && a.lockout_state !== "ok";
        accountSelect.appendChild(
          el(
            "option",
            {
              value: String(a.id),
              disabled: isLocked ? "disabled" : null,
            },
            `${a.account_identifier || a.label || `account ${a.id}`}${isLocked ? " 🔒 locked" : ""}`,
          ),
        );
      });
    }
  };
  platformSelect.addEventListener("change", refreshAccountSelect);
  refreshAccountSelect();

  const addBtn = el(
    "button",
    {
      class: "btn btn-tiny btn-secondary",
      type: "button",
      onclick: () => {
        const platform = platformSelect.value;
        const accId = parseInt(accountSelect.value, 10);
        if (!Number.isFinite(accId)) {
          toast("Pick an account first", "err");
          return;
        }
        draft.channels.push({
          platform,
          account_id: accId,
          use_when: `has_${platform === "twitter" ? "x" : platform}`,
        });
        renderChannels();
        onChange?.();
      },
    },
    "+ Add channel",
  );

  renderChannels();
  root.appendChild(channelList);

  if (draft.channels.length > 1) {
    root.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 10px; margin-top: 4px;" },
        "Order = fallback priority. Per contact, runner uses the first reachable channel.",
      ),
    );
  }

  root.appendChild(
    el("div", { class: "row", style: "gap: 8px; margin-top: 12px; align-items: center;" }, [
      platformSelect,
      accountSelect,
      addBtn,
    ]),
  );

  if ((accounts || []).length === 0) {
    root.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 11px; margin-top: 6px;" },
        "No social accounts yet. Connect one in Settings → Connected accounts.",
      ),
    );
  }
  return root;
}

function _composerAllowedPlatformsForSources(resolvers) {
  const catalog = _composerSourceCatalog || [];
  if (resolvers.length === 0) return null; // no constraint yet
  let intersection = null;
  for (const r of resolvers) {
    const meta = catalog.find((c) => c.id === r.type);
    const allowed = meta?.compatible_platforms || ["telegram", "twitter", "linkedin", "email"];
    if (intersection === null) intersection = new Set(allowed);
    else intersection = new Set([...intersection].filter((p) => allowed.includes(p)));
  }
  return intersection ? [...intersection] : null;
}

// ── Smart prefill for crm_selected source ───────────────────────────

async function _composerSmartPrefillFromSelection(draft, onChange) {
  const ids = draft.audience.resolvers.find((r) => r.type === "crm_selected")?.contact_ids || [];
  if (ids.length === 0) {
    toast("crm_selected has no contact IDs", "err");
    return;
  }
  // Reuse existing bulk-campaign helpers if present (computeSharedTags + buildSmartPrefill)
  // — they live in dashboard.js elsewhere from the bulk-campaign modal.
  if (typeof computeSharedTags !== "function" || typeof buildSmartPrefill !== "function") {
    toast("Smart-prefill helpers not loaded — falling back to a default greeting.", "err");
    _ensureFirstSendDmPlay(draft, "Hey {{first_name}},\n\n", null);
    onChange?.();
    return;
  }
  // Pull contact rows we have cached on dashboardState (CRM grid)
  const rows = (window._lastCrmContactRows || []).filter((r) => ids.includes(r.id));
  if (rows.length === 0) {
    toast("Contact rows not in cache — open CRM first.", "err");
    return;
  }
  const sharedTags = computeSharedTags(rows);
  const prefill = buildSmartPrefill(sharedTags);
  _ensureFirstSendDmPlay(
    draft,
    prefill?.body || "Hey {{first_name}},\n\n",
    prefill?.subject || null,
  );
  toast(
    `Pre-filled from ${sharedTags.length} shared tag${sharedTags.length === 1 ? "" : "s"}`,
    "ok",
  );
  onChange?.();
}

function _ensureFirstSendDmPlay(draft, body, subject) {
  let target = (draft.plays || []).find((p) => p.type === "send_dm" || p.type === "email_send");
  if (!target) {
    target = { type: "send_dm", template: "", subject: "" };
    draft.plays.unshift(target);
  }
  target.template = body;
  if (subject != null) target.subject = subject;
}

// ── Save draft helper ───────────────────────────────────────────────

async function _composerSaveDraft(draft) {
  // Phase 9: auto-name if user left it as default.
  draft.name = _composerEnsureName(draft);
  // Phase 16: auto-description. Filled in only when empty so a user-
  // written description is never overwritten (auto-stamp guarded with
  // a "Migrated from " sentinel for legacy rows).
  draft.description = _composerEnsureDescription(draft);
  const body = {
    name: draft.name,
    description: draft.description,
    trigger: draft.trigger,
    audience: draft.audience,
    channels: draft.channels,
    plays: draft.plays,
  };
  let resp;
  if (draft.id) {
    resp = await api("composition-update", { id: draft.id, body }).catch((e) => ({
      error: e?.message,
    }));
  } else {
    resp = await api("composition-create", { body }).catch((e) => ({ error: e?.message }));
  }
  // Phase 9: edit-lock — surface 409 distinctly so the user knows to wait
  if (resp?.error && resp?.running) {
    toast("Composition has a running run — edits blocked until it finishes.", "err", 5000);
    return null;
  }
  if (resp?.error) {
    toast(`Save failed: ${resp.error}`, "err");
    return null;
  }
  const c = resp?.composition;
  if (!c) {
    toast("Save returned no composition", "err");
    return null;
  }
  draft.id = c.id;
  return c;
}

// ── Bulk-campaign hand-off (Phase 6) ───────────────────────────────
//
// External entry point used by the existing CRM bulk-select flow.
// Call from the bulk-campaign modal's "Build as workflow" button:
//   composerHandoffFromCrmSelection(contactIds)
// Pre-fills `bulk_express` with the selected contact IDs and opens
// the wizard at the REVIEW step so the user can tweak + activate.

async function composerHandoffFromCrmSelection(contactIds) {
  if (!Array.isArray(contactIds) || contactIds.length === 0) {
    toast("No contacts selected", "err");
    return;
  }
  const r = await api("composition-from-template", {
    templateId: "bulk_express",
    body: { contactIds },
  }).catch((e) => ({ error: e?.message }));
  if (r?.error) {
    toast(`Hand-off failed: ${r.error}`, "err");
    return;
  }
  const c = r?.composition;
  if (!c) {
    toast("Hand-off returned no composition", "err");
    return;
  }
  toast(`${contactIds.length} contacts staged in composer`, "ok", 3500);
  dashboardState.composerDraft = { ...c };
  navigate("workflows/edit");
}

// Expose globally so the existing bulk-campaign modal can call it.
try {
  window.composerHandoffFromCrmSelection = composerHandoffFromCrmSelection;
} catch {}

// ── Daily-cap simulator (Phase 7) ───────────────────────────────────

const COMPOSER_DEFAULT_DAILY_CAPS = {
  telegram: 30, // DMs/day per account
  linkedin: 30, // connections/day (the binding cap; messages/day is higher)
  twitter: 30, // follows or DMs/day per account
  email: 100, // emails/day per account (warmup-progressive in real life)
};

function _composerEstimateCapacity(channels) {
  // Sum the per-channel cap. If multiple accounts on the same platform,
  // total = sum (multi-account compounding). Conservative if user picks
  // an aggressive primary play (e.g., connect_with_note hits the LI cap fast).
  let total = 0;
  const breakdown = [];
  for (const ch of channels || []) {
    const cap = COMPOSER_DEFAULT_DAILY_CAPS[ch.platform] || 0;
    total += cap;
    breakdown.push(`${ch.platform}@${ch.account_id || "?"}: ${cap}/day`);
  }
  return { total, breakdown };
}

function _composerBuildCapSimulator(audienceTotal, reachable, channels) {
  if (!channels || channels.length === 0) return null;
  const { total, breakdown } = _composerEstimateCapacity(channels);
  if (total === 0) return null;

  // Approximate "reachable via these channels" — sum the per-platform
  // reachable counts for channels we have, capped at audience total.
  const reachableViaChannels = (channels || []).reduce(
    (acc, ch) => acc + (reachable?.[ch.platform === "twitter" ? "twitter" : ch.platform] || 0),
    0,
  );
  const effectiveAudience = Math.min(audienceTotal || 0, reachableViaChannels);

  const days = total > 0 ? Math.ceil(effectiveAudience / total) : Infinity;

  return el(
    "div",
    {
      style:
        "padding: 10px 12px; background: #0a1a0a; border: 1px solid #1a3a1a; border-radius: 4px; margin-top: 10px; font-size: 11px;",
    },
    [
      el(
        "div",
        { style: "font-weight: 500; margin-bottom: 4px;" },
        `Capacity: ~${total}/day across ${channels.length} channel${channels.length === 1 ? "" : "s"}`,
      ),
      el("div", { class: "muted", style: "font-size: 10px;" }, breakdown.join(" · ")),
      el(
        "div",
        { style: "margin-top: 4px;" },
        `Effective audience: ${effectiveAudience} of ${audienceTotal} ` +
          `→ ETA ~${days === Infinity ? "—" : days} day${days === 1 ? "" : "s"} to clear`,
      ),
      el(
        "div",
        { class: "muted", style: "font-size: 10px; margin-top: 2px;" },
        "(estimate; doesn't account for today's existing usage or per-account lockouts)",
      ),
    ],
  );
}

// ── Audience-count debouncing + cache (Phase 7) ────────────────────

const _composerCountCache = new Map(); // hash → { total, reachable, usableViaChannels, ts }
const COMPOSER_COUNT_CACHE_TTL_MS = 30_000;
let _composerCountDebounce = null;

function _composerHashAudience(audience, channels) {
  // Cheap stable hash (no real cryptography needed). JSON.stringify with
  // sorted keys → fnv-1a-style numeric digest.
  const payload = JSON.stringify({ a: audience, c: channels });
  let h = 0x811c9dc5;
  for (let i = 0; i < payload.length; i++) {
    h ^= payload.charCodeAt(i);
    h = (h * 0x01000193) >>> 0;
  }
  return h.toString(16);
}

function _composerDebouncedAudienceCount(audience, channels, cb) {
  const key = _composerHashAudience(audience, channels);
  // Cache hit?
  const cached = _composerCountCache.get(key);
  if (cached && Date.now() - cached.ts < COMPOSER_COUNT_CACHE_TTL_MS) {
    cb(cached);
    return;
  }
  // Debounce
  if (_composerCountDebounce) clearTimeout(_composerCountDebounce);
  _composerCountDebounce = setTimeout(async () => {
    const r = await api("composition-audience-count", { body: { audience, channels } }).catch(
      () => null,
    );
    if (!r) return cb(null);
    const stamped = { ...r, ts: Date.now() };
    _composerCountCache.set(key, stamped);
    // GC old entries
    if (_composerCountCache.size > 100) {
      const cutoff = Date.now() - COMPOSER_COUNT_CACHE_TTL_MS;
      for (const [k, v] of _composerCountCache) if (v.ts < cutoff) _composerCountCache.delete(k);
    }
    cb(stamped);
  }, 400);
}

// Expose for the wizard's WHO step refresh handler.
try {
  window._composerDebouncedAudienceCount = _composerDebouncedAudienceCount;
} catch {}

// ── Phase 8: pre-flight warnings ───────────────────────────────────
//
// Six red-flag checks at activate-time per roadmap §18.4. Each warning
// is a yellow banner; user can override with "Activate anyway" or fix.

function _composerPreflightWarnings(draft) {
  const warnings = [];
  const audienceTotal = draft._lastCount?.total || 0;
  const recencyDays = Number(draft.audience?.exclude_already_contacted_within_days) || 0;
  const plays = draft.plays || [];
  const triggerType = draft.trigger?.type || "manual";

  // 1. Large audience without recency exclusion
  if (audienceTotal > 500 && recencyDays === 0) {
    warnings.push({
      severity: "high",
      title: `Large audience (${audienceTotal}) with no recency exclusion`,
      body: "You'll likely re-contact people you've already messaged. Consider setting 'Skip already-contacted within' to 30 days in the WHO step.",
    });
  }

  // 2. Send-class plays missing {{first_name}} interpolation
  const dmPlays = plays.filter((p) =>
    ["send_dm", "email_send", "comment", "connect_with_note", "quote_tweet"].includes(p?.type),
  );
  for (const p of dmPlays) {
    const tpl = String(p.template || p.body || "");
    if (tpl && !/\{\{\s*first_name\s*\}\}|\{\{\s*name\s*\}\}/.test(tpl)) {
      warnings.push({
        severity: "medium",
        title: `${p.type} template doesn't use {{first_name}}`,
        body: `Personalization matters for reply rates. The body reads: "${tpl.slice(0, 80)}…"`,
      });
      break; // one warning per composition is enough
    }
  }

  // 3. Send-class plays with very short body
  for (const p of dmPlays) {
    const tpl = String(p.template || p.body || "").trim();
    if (tpl && tpl.length > 0 && tpl.length <= 30) {
      warnings.push({
        severity: "medium",
        title: `${p.type} template is suspiciously short (${tpl.length} chars)`,
        body: `Body: "${tpl}". Likely incomplete — finish writing before activating.`,
      });
      break;
    }
  }

  // 4. Inbound trigger AND no rate cap
  if (triggerType === "event" && !draft.audience?.exclude_already_contacted_within_days) {
    warnings.push({
      severity: "low",
      title: "Inbound trigger with no recency cap",
      body: "An inbound event-trigger composition can fire many times per day. Consider a recency cap so the same person doesn't get spammed.",
    });
  }

  // 5. Email play but no unsubscribe link in template (CAN-SPAM)
  const emailPlay = plays.find((p) => p.type === "email_send");
  if (emailPlay) {
    const tpl = String(emailPlay.template || emailPlay.body || "").toLowerCase();
    if (tpl && !/unsubscribe|opt[\s-]?out|reply\s+stop/i.test(tpl)) {
      warnings.push({
        severity: "high",
        title: "Email play has no unsubscribe link",
        body: "CAN-SPAM Act requires a one-click unsubscribe. Add 'Reply STOP to opt out' or a footer link before activating.",
      });
    }
  }

  // 6. No plays at all
  if (plays.length === 0) {
    warnings.push({
      severity: "high",
      title: "No plays defined",
      body: "Add at least one action in the WHAT step before activating.",
    });
  }

  return warnings;
}

function _composerShowPreflightModal(warnings) {
  return new Promise((resolve) => {
    const overlay = el("div", {
      style:
        "position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center;",
    });
    const modal = el("div", {
      style:
        "background: var(--bg-2); border: 1px solid #5a4a1a; border-radius: 8px; width: 600px; max-width: 95vw; max-height: 80vh; overflow-y: auto;",
    });
    const decide = (proceed) => {
      overlay.remove();
      resolve(proceed);
    };

    const sevColor = (s) => (s === "high" ? "#ff6666" : s === "medium" ? "#daaa5a" : "#5a8aff");

    modal.appendChild(
      el("div", { style: "padding: 16px 20px; border-bottom: 1px solid #2a2a2a;" }, [
        el(
          "div",
          { style: "font-weight: 600; font-size: 16px; color: #faaa5a;" },
          `Pre-flight: ${warnings.length} warning${warnings.length === 1 ? "" : "s"}`,
        ),
        el(
          "div",
          { class: "muted", style: "font-size: 11px; margin-top: 4px;" },
          "Worth fixing before you activate. You can proceed anyway, but the composer will note these in the run log.",
        ),
      ]),
    );
    const body = el("div", { style: "padding: 12px 20px;" });
    for (const w of warnings) {
      body.appendChild(
        el(
          "div",
          {
            style: `padding: 10px 12px; margin-bottom: 8px; background: #1a1a0a; border-left: 3px solid ${sevColor(w.severity)}; border-radius: 3px;`,
          },
          [
            el("div", { style: "font-weight: 500; font-size: 13px;" }, w.title),
            el(
              "div",
              { class: "muted", style: "font-size: 11px; margin-top: 4px; line-height: 1.4;" },
              w.body,
            ),
          ],
        ),
      );
    }
    modal.appendChild(body);
    modal.appendChild(
      el(
        "div",
        {
          style:
            "padding: 12px 20px; border-top: 1px solid #2a2a2a; display: flex; justify-content: flex-end; gap: 8px;",
        },
        [
          el(
            "button",
            { class: "btn btn-secondary", type: "button", onclick: () => decide(false) },
            "← Fix it",
          ),
          el(
            "button",
            { class: "btn btn-primary", type: "button", onclick: () => decide(true) },
            "I understand, activate anyway",
          ),
        ],
      ),
    );
    overlay.appendChild(modal);
    document.body.appendChild(overlay);
  });
}

// ── Phase 8: audit drill-down ("Why these contacts?") ──────────────

async function _composerOpenRunAudit(runId) {
  const r = await api("composition-run-audit", { runId }).catch((e) => ({ error: e?.message }));
  if (r?.error) {
    toast(`Audit fetch failed: ${r.error}`, "err");
    return;
  }
  const audit = r?.audit || [];
  if (audit.length === 0) {
    toast("No audit log for this run (older than v80, or run skipped)", "ok");
    return;
  }

  const overlay = el("div", {
    style:
      "position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center;",
    onclick: (ev) => {
      if (ev.target === ev.currentTarget) overlay.remove();
    },
  });
  const modal = el("div", {
    style:
      "background: var(--bg-1); border: 1px solid var(--border-strong); border-radius: 8px; width: 800px; max-width: 95vw; max-height: 80vh; overflow-y: auto;",
  });

  const included = audit.filter((a) => a.decision === "included");
  const excluded = audit.filter((a) => a.decision === "excluded");

  modal.appendChild(
    el(
      "div",
      {
        style:
          "padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;",
      },
      [
        el("div", {}, [
          el(
            "div",
            { style: "font-weight: 600; font-size: 16px;" },
            `Why these contacts? (run #${runId})`,
          ),
          el(
            "div",
            { class: "muted", style: "font-size: 11px; margin-top: 2px;" },
            `${included.length} included · ${excluded.length} excluded`,
          ),
        ]),
        el(
          "button",
          { class: "btn btn-tiny btn-secondary", type: "button", onclick: () => overlay.remove() },
          "✕",
        ),
      ],
    ),
  );

  const renderRow = (a) => {
    const reason = a.reason || {};
    const matched = (reason.matched_resolvers || []).map((m) => m.type).join(" + ") || "—";
    const channel = reason.channel_chosen || "—";
    const skipped = (reason.channels_skipped || []).join(", ") || null;
    const excludeReasons = (reason.exclusion_reasons || []).join(", ") || null;
    return el(
      "div",
      {
        style: `padding: 8px 10px; border-bottom: 1px solid var(--border); font-size: 11px; border-left: 3px solid ${a.decision === "included" ? "#5ada5a" : "#ff6666"};`,
      },
      [
        el(
          "div",
          { style: "font-weight: 500;" },
          `${a.display_name}${a.company ? ` · ${a.company}` : ""} (#${a.contact_id})`,
        ),
        el("div", { class: "muted", style: "margin-top: 2px;" }, `Matched by: ${matched}`),
        a.decision === "included"
          ? el(
              "div",
              { class: "muted", style: "margin-top: 2px;" },
              `Channel: ${channel}${skipped ? ` · skipped: ${skipped}` : ""}`,
            )
          : el(
              "div",
              { style: "color: var(--red); margin-top: 2px;" },
              `Excluded: ${excludeReasons || skipped || "no reachable channel"}`,
            ),
      ],
    );
  };

  const body = el("div", { style: "padding: 12px 20px;" });
  if (included.length > 0) {
    body.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 11px; margin: 8px 0 4px;" },
        `INCLUDED (${included.length})`,
      ),
    );
    for (const a of included) body.appendChild(renderRow(a));
  }
  if (excluded.length > 0) {
    body.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 11px; margin: 16px 0 4px;" },
        `EXCLUDED (${excluded.length})`,
      ),
    );
    for (const a of excluded) body.appendChild(renderRow(a));
  }
  modal.appendChild(body);

  overlay.appendChild(modal);
  document.body.appendChild(overlay);
}

// ── Phase 8: funnel viz ───────────────────────────────────────────

function _composerRenderFunnelBar(funnel) {
  // Returns a horizontal funnel: matched → eligible → attempted → sent → replied
  const stages = [
    { id: "matched", label: "Matched", color: "#5a8aff" },
    { id: "eligible", label: "Eligible", color: "#5aaaff" },
    { id: "attempted", label: "Attempted", color: "#5acaaa" },
    { id: "sent", label: "Sent", color: "#5ada5a" },
    { id: "replied", label: "Replied", color: "#daaa5a" },
    { id: "converted", label: "Converted", color: "#fa8a5a" },
  ];
  const max = Math.max(...stages.map((s) => funnel[s.id] || 0), 1);
  return el(
    "div",
    { style: "display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px;" },
    stages.map((s) => {
      const n = funnel[s.id] || 0;
      const pct = Math.round((100 * n) / max);
      return el(
        "div",
        {
          style:
            "background: var(--bg-0); border: 1px solid var(--border); border-radius: 4px; padding: 8px; position: relative; overflow: hidden;",
        },
        [
          el("div", {
            style: `position: absolute; bottom: 0; left: 0; right: 0; height: ${pct}%; background: ${s.color}22; transition: height 200ms;`,
          }),
          el(
            "div",
            {
              class: "muted",
              style: "font-size: 10px; text-transform: uppercase; position: relative;",
            },
            s.label,
          ),
          el(
            "div",
            {
              style: `font-size: 18px; font-weight: 600; margin-top: 2px; color: ${s.color}; position: relative;`,
            },
            String(n),
          ),
        ],
      );
    }),
  );
}

function _composerFunnelMiniSparkline(funnel) {
  // Compact 5-cell horizontal bar for the list page
  const stages = ["matched", "eligible", "attempted", "sent", "replied"];
  const max = Math.max(...stages.map((s) => funnel[s] || 0), 1);
  return el(
    "div",
    { class: "row", style: "gap: 2px; align-items: flex-end; height: 18px;" },
    stages.map((s) => {
      const n = funnel[s] || 0;
      const h = Math.max(2, Math.round((16 * n) / max));
      return el("div", {
        style: `width: 6px; height: ${h}px; background: ${n > 0 ? "#5a8aff" : "#222"}; border-radius: 1px;`,
        title: `${s}: ${n}`,
      });
    }),
  );
}

// Expose helpers for renderWorkflowDetail + renderWorkflowsHome
try {
  window._composerOpenRunAudit = _composerOpenRunAudit;
  window._composerRenderFunnelBar = _composerRenderFunnelBar;
  window._composerFunnelMiniSparkline = _composerFunnelMiniSparkline;
} catch {}

// ── Phase 9: killswitch ────────────────────────────────────────────

/**
 * Killswitch handler. Confirms with the user (type-to-confirm if the
 * composition has reached >50 contacts in any prior run), then pauses
 * the composition and cancels in-flight run rows.
 */
async function _composerKillswitch(composition) {
  const audienceReached = composition.total_audience_reached || 0;
  const isHighVolume = audienceReached > 50;
  let confirmed = false;
  if (isHighVolume) {
    const typed = prompt(
      `KILL "${composition.name}"?\n\n` +
        `This composition has reached ${audienceReached} contacts. ` +
        `Type the composition name to confirm:`,
    );
    confirmed = typed && typed.trim() === composition.name.trim();
    if (typed && !confirmed) toast("Name didn't match — cancelled", "err");
  } else {
    confirmed = confirm(`Pause "${composition.name}" and cancel any in-flight run?`);
  }
  if (!confirmed) return false;
  const r = await api("composition-killswitch", { id: composition.id }).catch((e) => ({
    error: e?.message,
  }));
  if (r?.error) {
    toast(`Killswitch failed: ${r.error}`, "err");
    return false;
  }
  toast(
    `Paused "${composition.name}"${r.cancelledRuns ? ` · ${r.cancelledRuns} run${r.cancelledRuns === 1 ? "" : "s"} marked killed` : ""}`,
    "ok",
    4000,
  );
  return true;
}

// ── Phase 9: sandbox list ─────────────────────────────────────────

const COMPOSER_SANDBOX_KEY = "composer:sandbox_contact_ids";

function _composerGetSandboxIds() {
  try {
    const raw = localStorage.getItem(COMPOSER_SANDBOX_KEY);
    return Array.isArray(JSON.parse(raw)) ? JSON.parse(raw) : [];
  } catch {
    return [];
  }
}

function _composerSetSandboxIds(ids) {
  try {
    const cleaned = (Array.isArray(ids) ? ids : [])
      .map((n) => parseInt(n, 10))
      .filter(Number.isFinite);
    localStorage.setItem(COMPOSER_SANDBOX_KEY, JSON.stringify([...new Set(cleaned)]));
  } catch {}
}

function _composerRenderSandboxPanel() {
  const wrap = el("div", {
    style:
      "padding: 10px 12px; background: #0a0a14; border: 1px solid #1a2a4a; border-radius: 4px; font-size: 11px; margin-top: 10px;",
  });
  wrap.appendChild(
    el("div", { style: "font-weight: 500; margin-bottom: 4px;" }, [
      "Sandbox contacts",
      helpIcon("sandbox"),
    ]),
  );
  const desc = el(
    "div",
    { class: "muted", style: "font-size: 10px; margin-bottom: 6px;" },
    "Comma-separated contact IDs (typically yourself on different platforms). 'Run on sandbox' executes plays for real, but only against this list.",
  );
  wrap.appendChild(desc);

  const cur = _composerGetSandboxIds();
  const input = el("input", {
    type: "text",
    class: "wizard-input",
    value: cur.join(", "),
    placeholder: "e.g. 12, 47, 89",
    style: "width: 100%; max-width: 400px; font-size: 11px;",
  });
  input.addEventListener("change", () => {
    const ids = String(input.value || "")
      .split(",")
      .map((s) => parseInt(s.trim(), 10))
      .filter(Number.isFinite);
    _composerSetSandboxIds(ids);
    toast(`Sandbox set to ${ids.length} contact${ids.length === 1 ? "" : "s"}`, "ok", 1500);
  });
  wrap.appendChild(input);
  return wrap;
}

// ── Phase 9: auto-name ──────────────────────────────────────────────

function _composerDeriveAutoName(draft) {
  // Heuristic: "<verb> → <audience hint> → <platform>"
  const firstPlay = (draft.plays || [])[0];
  const verb = firstPlay
    ? firstPlay.type.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase())
    : "Workflow";
  const audienceHint = (() => {
    const r = (draft.audience?.resolvers || [])[0];
    if (!r) return "";
    if (r.type === "crm_query") {
      const tags = Array.isArray(r.filters?.tags) ? r.filters.tags.join(", ") : "";
      return tags || "CRM";
    }
    if (r.type === "scraper_luma_event") return `Luma ${r.event_slug || ""}`.trim();
    if (r.type === "scraper_x_followers_of")
      return `X followers of @${r.target_handle || ""}`.trim();
    if (r.type === "feed_li_pending_invites") return "LI invites";
    if (r.type === "companies_recently_funded") return "Recently funded";
    if (r.type === "companies_by_marketcap") return `Top tokens`;
    if (r.type === "crm_by_icp_score") return `ICP ≥${r.min_score || 7}`;
    if (r.type === "manual_list" || r.type === "crm_selected")
      return `${(r.contact_ids || []).length} contacts`;
    if (r.type === "inbound_event_subject") return "event subject";
    return r.type.replace(/_/g, " ");
  })();
  const platforms = [...new Set((draft.channels || []).map((c) => c.platform))].join(" · ");
  const parts = [verb, audienceHint, platforms].filter(Boolean);
  return parts.join(" → ").slice(0, 100);
}

function _composerEnsureName(draft) {
  const cur = String(draft.name || "").trim();
  if (cur && cur !== "Untitled workflow") return cur;
  return _composerDeriveAutoName(draft) || "Untitled workflow";
}

// Phase 16: auto-description — derives a single human-readable line
// from the composition shape. Used by save flow when description is
// empty (so future-you / a teammate inheriting this row can read what
// it does without expanding the wizard).
function _composerDeriveAutoDescription(draft) {
  const audienceCount = draft._lastCount?.total;
  const trig = draft.trigger?.type || "manual";
  const trigSuffix =
    trig === "schedule"
      ? ` on schedule (${draft.trigger.cron || "?"})`
      : trig === "schedule_simple"
        ? ` on simple schedule`
        : trig === "event"
          ? ` when ${draft.trigger.source || "?"} fires`
          : ` manually`;
  const audSummary = (() => {
    const r = (draft.audience?.resolvers || [])[0];
    if (!r) return "no audience";
    if (r.type === "crm_query") {
      const tags = Array.isArray(r.filters?.tags) ? r.filters.tags.join(", ") : "";
      return tags ? `CRM tag${r.filters.tags.length === 1 ? "" : "s"} '${tags}'` : "CRM segment";
    }
    if (r.type === "scraper_luma_event") return `Luma event '${r.event_slug || "?"}'`;
    if (r.type === "scraper_x_followers_of") return `followers of @${r.target_handle || "?"}`;
    if (r.type === "feed_li_pending_invites") return "incoming LinkedIn invites";
    if (r.type === "companies_recently_funded")
      return `companies funded in last ${r.window_days || 30} days`;
    if (r.type === "companies_by_marketcap") return `top ${r.top_n || 50} tokens by market cap`;
    if (r.type === "crm_by_icp_score") return `contacts with ICP score ≥${r.min_score || 7}`;
    if (r.type === "crm_lead_score") return `contacts with lead score ≥${r.min_score || 60}`;
    if (r.type === "crm_open_loop")
      return `open-loop contacts (≥${r.days_since_send || 5}d since send)`;
    if (r.type === "crm_unenriched") return "unenriched (TG-only) contacts";
    if (r.type === "manual_list" || r.type === "crm_selected")
      return `${(r.contact_ids || []).length} hand-picked contacts`;
    if (r.type === "inbound_event_subject") return "the contact who triggered the event";
    return r.type.replace(/_/g, " ");
  })();
  const verb = (() => {
    const p = (draft.plays || []).find((x) =>
      ["send_dm", "email_send", "comment", "quote_tweet", "connect_with_note"].includes(x?.type),
    );
    if (!p) return (draft.plays || [])[0]?.type?.replace(/_/g, " ") || "Run plays";
    if (p.type === "send_dm") return "DMs";
    if (p.type === "email_send") return "emails";
    if (p.type === "comment") return "comments on";
    if (p.type === "quote_tweet") return "quote-tweets";
    if (p.type === "connect_with_note") return "connects with";
    return p.type.replace(/_/g, " ");
  })();
  const platforms = [...new Set((draft.channels || []).map((c) => c.platform))];
  const platformPart =
    platforms.length === 0
      ? ""
      : platforms.length === 1
        ? ` via ${platforms[0]}`
        : ` via ${platforms.slice(0, -1).join(", ")} → ${platforms[platforms.length - 1]} fallback`;
  const sizeHint =
    Number.isFinite(audienceCount) && audienceCount > 0 ? ` (~${audienceCount} contacts)` : "";
  return `${verb} ${audSummary}${platformPart}${trigSuffix}${sizeHint}.`;
}

function _composerEnsureDescription(draft) {
  const cur = String(draft.description || "").trim();
  if (cur && !cur.startsWith("Migrated from ")) return cur; // user-written or migration-stamped
  return _composerDeriveAutoDescription(draft);
}

try {
  window._composerKillswitch = _composerKillswitch;
  window._composerRenderSandboxPanel = _composerRenderSandboxPanel;
  window._composerGetSandboxIds = _composerGetSandboxIds;
  window._composerEnsureName = _composerEnsureName;
} catch {}

// ── Phase 15: composition clone + A/B compare ─────────────────────

async function _composerOpenComparePicker(currentComp) {
  const resp = await api("compositions-list").catch(() => null);
  const items = (resp?.compositions || []).filter((w) => w.id !== currentComp.id);
  if (items.length === 0) {
    toast("No other compositions to compare against.", "err");
    return;
  }
  const overlay = el("div", {
    style:
      "position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center;",
    onclick: (ev) => {
      if (ev.target === ev.currentTarget) overlay.remove();
    },
  });
  const modal = el("div", {
    style:
      "background: var(--bg-1); border: 1px solid var(--border-strong); border-radius: 8px; width: 600px; max-width: 95vw; max-height: 80vh; display: flex; flex-direction: column;",
  });
  modal.appendChild(
    el("div", { style: "padding: 14px 18px; border-bottom: 1px solid var(--border);" }, [
      el("div", { style: "font-weight: 600; font-size: 16px;" }, "Compare with…"),
      el(
        "div",
        { class: "muted", style: "font-size: 11px; margin-top: 2px;" },
        `Pick another composition to compare against "${currentComp.name}". Side-by-side funnel + run stats.`,
      ),
    ]),
  );
  const list = el("div", { style: "padding: 8px; overflow-y: auto; flex: 1;" });
  for (const w of items) {
    const replyRate =
      w.total_audience_reached > 0
        ? Math.round((100 * (w.total_replies || 0)) / w.total_audience_reached)
        : null;
    list.appendChild(
      el(
        "div",
        {
          style:
            "padding: 10px 12px; margin-bottom: 4px; background: var(--bg-0); border: 1px solid var(--border); border-radius: 4px; cursor: pointer;",
          onclick: () => {
            overlay.remove();
            dashboardState.composerCompareIds = [currentComp.id, w.id];
            navigate("workflows/compare");
          },
        },
        [
          el("div", { style: "font-weight: 500; font-size: 13px;" }, [
            w.name,
            " ",
            el(
              "span",
              {
                class: "pill",
                style: "background: var(--bg-3); font-size: 9px; padding: 2px 6px;",
              },
              w.status,
            ),
          ]),
          el(
            "div",
            { class: "muted", style: "font-size: 11px; margin-top: 2px;" },
            `${w.total_runs || 0} runs · ${w.total_audience_reached || 0} reached · ${w.total_replies || 0} replied${replyRate != null ? ` · ${replyRate}% reply rate` : ""}`,
          ),
        ],
      ),
    );
  }
  modal.appendChild(list);
  overlay.appendChild(modal);
  document.body.appendChild(overlay);
}

async function renderWorkflowCompare() {
  const ids = dashboardState.composerCompareIds || [];
  if (ids.length < 2) {
    setContentNode(
      el("div", { class: "empty-state" }, [
        el("div", {}, "Pick 2+ compositions to compare."),
        el(
          "button",
          {
            class: "btn btn-secondary",
            type: "button",
            onclick: () => navigate("workflows"),
          },
          "← Back",
        ),
      ]),
    );
    return;
  }
  const r = await api("composition-compare", { ids }).catch((e) => ({ error: e?.message }));
  if (r?.error) {
    setContentNode(
      el("div", { class: "empty-state" }, [
        el("div", {}, "Compare failed"),
        el("div", { class: "muted small" }, esc(r.error || "")),
      ]),
    );
    return;
  }
  const comps = r?.compositions || [];

  const root = el("div", { style: "max-width: 1300px; margin: 0 auto; padding: 24px;" });
  root.appendChild(
    el(
      "div",
      {
        class: "row",
        style: "justify-content: space-between; align-items: center; margin-bottom: 18px;",
      },
      [
        el("h1", { style: "margin: 0; font-size: 22px;" }, "A/B compare"),
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            onclick: () => navigate("workflows"),
          },
          "← Back to workflows",
        ),
      ],
    ),
  );

  // Side-by-side grid
  const grid = el("div", {
    style: `display: grid; grid-template-columns: repeat(${comps.length}, 1fr); gap: 16px;`,
  });
  for (const c of comps) {
    if (c.error) {
      grid.appendChild(
        el("div", { class: "card" }, [el("div", { class: "muted" }, `#${c.id}: ${c.error}`)]),
      );
      continue;
    }
    const card = el("div", { class: "card", style: "padding: 14px;" });
    card.appendChild(
      el(
        "div",
        {
          class: "row",
          style: "justify-content: space-between; align-items: flex-start; margin-bottom: 10px;",
        },
        [
          el("div", { style: "flex: 1;" }, [
            el("div", { style: "font-weight: 600; font-size: 14px;" }, c.name),
            el(
              "div",
              { class: "muted", style: "font-size: 10px; margin-top: 2px;" },
              `${c.status} · ${c.total_runs} runs · last run: ${c.last_run_at ? new Date(c.last_run_at).toLocaleDateString() : "never"}`,
            ),
          ]),
          el(
            "button",
            {
              class: "btn btn-tiny btn-secondary",
              type: "button",
              onclick: () => {
                dashboardState.composerDraft = { id: c.id, name: c.name };
                navigate("workflows/detail");
              },
            },
            "Detail →",
          ),
        ],
      ),
    );

    // Headline reply rate
    const replyRate =
      c.total_audience_reached > 0
        ? Math.round((100 * (c.total_replies || 0)) / c.total_audience_reached)
        : 0;
    card.appendChild(
      el(
        "div",
        {
          style:
            "padding: 10px; background: #0a1a0a; border: 1px solid #1a3a1a; border-radius: 4px; margin-bottom: 10px;",
        },
        [
          el(
            "div",
            { class: "muted", style: "font-size: 10px; text-transform: uppercase;" },
            "Reply rate",
          ),
          el(
            "div",
            {
              style: `font-size: 28px; font-weight: 700; color: ${replyRate > 10 ? "#5ada5a" : "#aaa"};`,
            },
            `${replyRate}%`,
          ),
          el(
            "div",
            { class: "muted", style: "font-size: 11px;" },
            `${c.total_replies || 0} of ${c.total_audience_reached || 0} reached`,
          ),
        ],
      ),
    );

    // Funnel
    if (c.funnel && Object.values(c.funnel).some((n) => n > 0)) {
      card.appendChild(_composerRenderFunnelBar(c.funnel));
    } else {
      card.appendChild(
        el("div", { class: "muted", style: "font-size: 11px; padding: 8px;" }, "No funnel data."),
      );
    }

    // Shape summary
    card.appendChild(
      el("div", { class: "muted", style: "font-size: 11px; margin-top: 12px; line-height: 1.6;" }, [
        el(
          "div",
          {},
          `Trigger: ${c.trigger?.type || "manual"}${c.trigger?.cron ? ` (${c.trigger.cron})` : ""}${c.trigger?.source ? ` (${c.trigger.source})` : ""}`,
        ),
        el(
          "div",
          {},
          `Audience: ${(c.audience?.resolvers || []).map((r) => r.type).join(" + ") || "—"}`,
        ),
        el(
          "div",
          {},
          `Channels: ${(c.channels || []).map((ch) => `${ch.platform}@${ch.account_id || "?"}`).join(" → ") || "—"}`,
        ),
        el("div", {}, `Plays: ${(c.plays || []).map((p) => p.type).join(" · ") || "—"}`),
      ]),
    );
    grid.appendChild(card);
  }
  root.appendChild(grid);

  // Verdict line — which one wins on reply rate?
  const valid = comps.filter((c) => !c.error && c.total_audience_reached > 0);
  if (valid.length >= 2) {
    const ranked = [...valid].sort(
      (a, b) =>
        b.total_replies / Math.max(b.total_audience_reached, 1) -
        a.total_replies / Math.max(a.total_audience_reached, 1),
    );
    const winner = ranked[0];
    const winRate = Math.round(
      (100 * winner.total_replies) / Math.max(winner.total_audience_reached, 1),
    );
    root.appendChild(
      el(
        "div",
        {
          class: "card",
          style:
            "margin-top: 16px; padding: 12px 14px; background: #0a1a0a; border: 1px solid #1a3a1a;",
        },
        [
          el(
            "div",
            { style: "font-weight: 500; font-size: 13px;" },
            `Best reply rate: "${winner.name}" at ${winRate}% (${winner.total_replies}/${winner.total_audience_reached})`,
          ),
          el(
            "div",
            { class: "muted", style: "font-size: 11px; margin-top: 4px;" },
            "Compare the audience + plays + voice on each side to see what drove the difference.",
          ),
        ],
      ),
    );
  }

  setContentNode(root);
}

// ── Phase 14: operator overview card ──────────────────────────────

function _composerBuildOperatorOverview(summary) {
  if (!summary || summary.error) return null;
  const c = summary.compositions || {};
  const r = summary.runs_today || {};
  const activeCount = c.active || 0;
  const draftCount = c.draft || 0;
  const pausedCount = c.paused || 0;

  const needsAttention =
    (summary.recent_errors_24h || 0) +
    (summary.locked_accounts || 0) +
    (summary.pending_canaries || 0);

  const card = el("div", { class: "card", style: "margin-bottom: 18px; padding: 14px 16px;" });
  card.appendChild(
    el(
      "div",
      {
        class: "row",
        style: "justify-content: space-between; align-items: center; margin-bottom: 12px;",
      },
      [
        el("div", { style: "font-weight: 500; font-size: 13px;" }, "Operator overview"),
        el(
          "div",
          { class: "muted", style: "font-size: 10px;" },
          `Updated ${new Date(summary.generated_at || Date.now()).toLocaleTimeString()}`,
        ),
      ],
    ),
  );

  // 5 stat cards in a row (Phase 7 _statCard helper).
  card.appendChild(
    el("div", { style: "display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px;" }, [
      _statCard("Active", `${activeCount}${draftCount > 0 ? ` · ${draftCount} draft` : ""}`),
      _statCard(
        "Runs today",
        `${summary.runs_today_total || 0}${r.error > 0 ? ` · ${r.error} err` : ""}`,
      ),
      _statCard("Replies (7d)", String(summary.replies_this_week || 0)),
      _statCard("Pending canaries", String(summary.pending_canaries || 0)),
      _statCard("Needs attention", needsAttention > 0 ? `${needsAttention} 🚨` : "0"),
    ]),
  );

  // Inline alerts row — only shown when there's something to surface.
  if (needsAttention > 0) {
    const alerts = [];
    if (summary.recent_errors_24h > 0) {
      alerts.push(
        el(
          "span",
          {
            style: "color: #ff8888; font-size: 11px;",
          },
          `${summary.recent_errors_24h} errored run${summary.recent_errors_24h === 1 ? "" : "s"} in last 24h`,
        ),
      );
    }
    if (summary.locked_accounts > 0) {
      alerts.push(
        el(
          "span",
          {
            style: "color: #faaa5a; font-size: 11px; cursor: pointer;",
            onclick: () => navigate("workflows/budgets"),
          },
          `${summary.locked_accounts} locked account${summary.locked_accounts === 1 ? "" : "s"} → fix in budgets`,
        ),
      );
    }
    if (summary.pending_canaries > 0) {
      alerts.push(
        el(
          "span",
          {
            style: "color: #daaa5a; font-size: 11px;",
          },
          `${summary.pending_canaries} canary${summary.pending_canaries === 1 ? "" : "s"} awaiting decision`,
        ),
      );
    }
    card.appendChild(
      el(
        "div",
        {
          style:
            "margin-top: 10px; padding: 8px 10px; background: #1a1a0a; border: 1px solid #2a2a1a; border-radius: 4px; display: flex; gap: 16px; flex-wrap: wrap;",
        },
        alerts,
      ),
    );
  }

  if (pausedCount > 0) {
    card.appendChild(
      el(
        "div",
        { class: "muted", style: "margin-top: 8px; font-size: 11px;" },
        `${pausedCount} paused composition${pausedCount === 1 ? "" : "s"} (auto-archive after 30 days idle).`,
      ),
    );
  }

  return card;
}

// ── Phase 33 (over Phase 32): performance / KPI dashboard ─────────
//
// Renders three stacked sections from the new performance endpoints:
//   1. Cost-per-reply summary (single-glance KPI)
//   2. Per-template funnel + ratios (which template actually converts?)
//   3. Per-account funnel (which account pulls its weight?)

// ── Phase 12: budget allocator dashboard ──────────────────────────

// "Budgets" body — per-account daily-cap utilization. Shared between
// the legacy standalone page (above) and the #performance/budgets tab
// inside renderPerformanceHub.
async function _buildWorkflowAccountBudgetsBody(root) {
  const resp = await api("composition-account-budgets").catch((e) => ({ error: e?.message }));
  if (resp?.error) {
    root.appendChild(emptyState("⚠️", "Could not load account budgets", esc(resp.error || "")));
    return;
  }
  const accounts = resp?.accounts || [];

  root.appendChild(
    el("div", { style: "margin-bottom: 18px;" }, [
      el("h2", { style: "margin: 0; font-size: 16px; color: var(--text-1);" }, "Budgets"),
      el(
        "div",
        { class: "muted", style: "font-size: 12px; margin-top: 4px;" },
        "Per-account daily-cap utilization across active compositions. Helps diagnose 'why isn't this composition sending more?' " +
          "(answer: the account is at its daily limit). Caps are conservative defaults — actual rate-limit gates run at send-time.",
      ),
    ]),
  );

  if (accounts.length === 0) {
    root.appendChild(
      el("div", { class: "empty-state", style: "padding: 40px 20px;" }, [
        el("div", { class: "empty-state-icon" }, icon("money")),
        el("div", { class: "empty-state-title" }, "No accounts connected"),
        el(
          "div",
          { class: "empty-state-hint" },
          "Connect a Telegram, X, LinkedIn, or Email account to see daily budgets here.",
        ),
      ]),
    );
    return;
  }

  // Aggregate stats at top
  const totalAcc = accounts.length;
  const lockedAcc = accounts.filter((a) => a.lockout_state && a.lockout_state !== "ok").length;
  const saturatedAcc = accounts.filter((a) => a.actions_today >= a.daily_cap).length;

  root.appendChild(
    el("div", { class: "row", style: "gap: 12px; margin-bottom: 20px;" }, [
      _statCard("Accounts", String(totalAcc)),
      _statCard("Locked", String(lockedAcc)),
      _statCard("Saturated", String(saturatedAcc)),
    ]),
  );

  // Group by platform
  const byPlatform = {};
  for (const a of accounts) {
    if (!byPlatform[a.platform]) byPlatform[a.platform] = [];
    byPlatform[a.platform].push(a);
  }
  const platformOrder = ["telegram", "linkedin", "twitter", "email"];
  const platformLabel = {
    telegram: "Telegram",
    linkedin: "LinkedIn",
    twitter: "Twitter / X",
    email: "Email",
  };

  for (const platform of platformOrder) {
    const list = byPlatform[platform];
    if (!list || list.length === 0) continue;

    const card = el("div", { class: "card", style: "margin-bottom: 16px;" });
    card.appendChild(el("div", { class: "card-title" }, platformLabel[platform] || platform));

    for (const a of list) {
      card.appendChild(_renderAccountBudgetRow(a));
    }
    root.appendChild(card);
  }
}

function _renderAccountBudgetRow(a) {
  const used = a.actions_today || 0;
  const cap = a.daily_cap || 1;
  const pct = Math.min(100, Math.round((100 * used) / cap));
  const remaining = a.remaining ?? Math.max(0, cap - used);
  const isLocked = a.lockout_state && a.lockout_state !== "ok";
  const isSaturated = remaining <= 0;
  const barColor = isLocked
    ? "#ff6666"
    : isSaturated
      ? "#daaa5a"
      : pct >= 80
        ? "#daaa5a"
        : "#5ada5a";

  const row = el("div", { style: "padding: 10px 0; border-bottom: 1px solid var(--border);" });

  row.appendChild(
    el(
      "div",
      {
        class: "row",
        style: "justify-content: space-between; align-items: center; margin-bottom: 6px;",
      },
      [
        el(
          "div",
          {},
          [
            el("span", { style: "font-weight: 500;" }, a.account_identifier),
            isLocked
              ? el(
                  "span",
                  {
                    class: "pill",
                    style:
                      "background: #2a1a1a; color: var(--red); font-size: 10px; padding: 2px 6px; margin-left: 8px;",
                  },
                  "locked",
                )
              : null,
            isSaturated && !isLocked
              ? el(
                  "span",
                  {
                    class: "pill",
                    style:
                      "background: #2a2a0a; color: #daaa5a; font-size: 10px; padding: 2px 6px; margin-left: 8px;",
                  },
                  "limit reached",
                )
              : null,
          ].filter(Boolean),
        ),
        el(
          "div",
          { class: "muted", style: "font-size: 11px;" },
          `${used}/${cap} ${platformVerbLabel(a.platform)} today · ${remaining} remaining`,
        ),
      ],
    ),
  );

  // Progress bar
  const barWrap = el("div", {
    style: "height: 6px; background: var(--bg-2); border-radius: 3px; overflow: hidden;",
  });
  barWrap.appendChild(
    el("div", {
      style: `height: 100%; width: ${pct}%; background: ${barColor}; transition: width 200ms;`,
    }),
  );
  row.appendChild(barWrap);

  if (isLocked && a.lockout_reason) {
    row.appendChild(
      el(
        "div",
        { style: "font-size: 11px; color: var(--red); margin-top: 4px;" },
        `${a.lockout_reason}`,
      ),
    );
  }

  // Active compositions using this account — Phase 14: with mini-funnel
  const acs = a.active_compositions || [];
  if (acs.length > 0) {
    row.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 11px; margin-top: 6px;" },
        `${acs.length} active composition${acs.length === 1 ? "" : "s"}:`,
      ),
    );
    for (const c of acs) {
      const replyRate =
        c.total_audience_reached > 0
          ? Math.round((100 * (c.total_replies || 0)) / c.total_audience_reached)
          : null;
      const compRow = el(
        "div",
        {
          style:
            "display: grid; grid-template-columns: 16px 1fr auto auto; gap: 8px; align-items: center; padding: 4px 0 4px 8px; border-bottom: 1px dashed var(--border); cursor: pointer;",
          onclick: () => {
            dashboardState.composerDraft = { id: c.id, name: c.name, status: c.status };
            navigate("workflows/detail");
          },
        },
        [
          el("span", { class: "muted", style: "font-size: 10px;" }, "├─"),
          el(
            "div",
            { style: "color: #5a8aff; font-size: 12px;" },
            [
              c.name,
              c.status === "draft"
                ? el(
                    "span",
                    { class: "muted", style: "margin-left: 6px; font-size: 10px;" },
                    "(draft)",
                  )
                : null,
              // Phase 17: per-comp daily cap badge
              c.pacing?.max_per_day
                ? el(
                    "span",
                    { class: "muted", style: "margin-left: 6px; font-size: 10px; color: #daaa5a;" },
                    `· ⏱${c.pacing.max_per_day}/day`,
                  )
                : null,
            ].filter(Boolean),
          ),
          el(
            "div",
            { style: "min-width: 50px;" },
            c.funnel ? _composerFunnelMiniSparkline(c.funnel) : null,
          ),
          el(
            "div",
            { class: "muted", style: "font-size: 10px; min-width: 90px; text-align: right;" },
            replyRate != null
              ? `${c.total_replies || 0}/${c.total_audience_reached || 0} · ${replyRate}%`
              : `${c.total_runs || 0} runs`,
          ),
        ],
      );
      row.appendChild(compRow);
    }
  } else {
    row.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 11px; margin-top: 6px;" },
        "No active composition uses this account.",
      ),
    );
  }

  return row;
}

function platformVerbLabel(platform) {
  if (platform === "telegram") return "DMs";
  if (platform === "linkedin") return "actions";
  if (platform === "twitter") return "actions";
  if (platform === "email") return "emails";
  return "actions";
}

// ── Phase 17: pacing panel ────────────────────────────────────────

function _composerRenderPacingPanel(draft) {
  // Pacing config lives on draft.audience.pacing so it travels with the
  // composition body. Optional — null/0 means "no per-comp cap, use the
  // account's full daily budget."
  draft.audience = draft.audience || { resolvers: [], combine: "union" };
  const cur = draft.audience.pacing || null;
  const enabled = cur && cur.max_per_day > 0;

  const wrap = el("div", {
    style:
      "padding: 10px 12px; background: #14140a; border: 1px solid #4a3a1a; border-radius: 4px; font-size: 11px; margin-top: 10px;",
  });
  const headerRow = el("div", {
    class: "row",
    style: "justify-content: space-between; align-items: center; margin-bottom: 6px;",
  });
  headerRow.appendChild(el("div", { style: "font-weight: 500;" }, ["⏱ Per-composition daily cap"]));

  const toggle = el("input", { type: "checkbox", checked: enabled ? "checked" : null });
  toggle.addEventListener("change", () => {
    if (toggle.checked) draft.audience.pacing = { max_per_day: 50 };
    else delete draft.audience.pacing;
    const newPanel = _composerRenderPacingPanel(draft);
    wrap.replaceWith(newPanel);
  });
  headerRow.appendChild(
    el("label", { style: "cursor: pointer; font-size: 11px;" }, [toggle, " enable"]),
  );
  wrap.appendChild(headerRow);

  wrap.appendChild(
    el(
      "div",
      { class: "muted", style: "font-size: 10px; margin-bottom: 6px; line-height: 1.4;" },
      "When enabled, this composition processes at most N contacts per day. " +
        "Spillover lands on tomorrow's schedule. Useful when multiple compositions share an account: prevents one from blowing through the daily budget.",
    ),
  );

  if (enabled) {
    const capInput = el("input", {
      type: "number",
      class: "wizard-input",
      value: cur.max_per_day,
      min: "1",
      style: "width: 90px;",
    });
    capInput.addEventListener("change", () => {
      draft.audience.pacing.max_per_day = Math.max(1, parseInt(capInput.value, 10) || 50);
    });
    wrap.appendChild(
      el("div", { class: "row", style: "gap: 8px; align-items: center;" }, [
        el("span", { class: "muted" }, "Max contacts per day:"),
        capInput,
      ]),
    );
  }
  return wrap;
}

// ── Phase 11: canary mode panel ────────────────────────────────────

function _composerRenderCanaryPanel(draft) {
  draft.audience = draft.audience || { resolvers: [], combine: "union" };
  const cur = draft.audience.canary || null;
  const enabled = !!cur;

  const wrap = el("div", {
    style:
      "padding: 10px 12px; background: #14140a; border: 1px solid #4a3a1a; border-radius: 4px; font-size: 11px; margin-top: 10px;",
  });

  const headerRow = el("div", {
    class: "row",
    style: "justify-content: space-between; align-items: center; margin-bottom: 6px;",
  });
  const title = el("div", { style: "font-weight: 500;" }, ["Canary mode", helpIcon("canary_mode")]);
  const toggle = el("input", { type: "checkbox", checked: enabled ? "checked" : null });
  toggle.addEventListener("change", () => {
    if (toggle.checked) {
      draft.audience.canary = { sample_pct: 5, wait_minutes: 60, max_error_rate: 0.1 };
    } else {
      delete draft.audience.canary;
    }
    // Re-render the panel to show/hide knobs
    const newPanel = _composerRenderCanaryPanel(draft);
    wrap.replaceWith(newPanel);
  });
  headerRow.appendChild(title);
  headerRow.appendChild(
    el("label", { style: "cursor: pointer; font-size: 11px;" }, [toggle, " enable"]),
  );
  wrap.appendChild(headerRow);

  wrap.appendChild(
    el(
      "div",
      { class: "muted", style: "font-size: 10px; margin-bottom: 6px; line-height: 1.4;" },
      "When enabled, runs only the first sample % of audience first. After wait minutes, the scheduler checks the canary's error rate. " +
        "If above threshold → composition auto-pauses. Otherwise → main batch fires automatically. Skipped on audiences <20.",
    ),
  );

  if (enabled) {
    const sampleInput = el("input", {
      type: "number",
      class: "wizard-input",
      value: cur.sample_pct ?? 5,
      min: "1",
      max: "50",
      style: "width: 70px;",
    });
    sampleInput.addEventListener("change", () => {
      draft.audience.canary.sample_pct = Math.max(
        1,
        Math.min(50, parseInt(sampleInput.value, 10) || 5),
      );
    });
    const waitInput = el("input", {
      type: "number",
      class: "wizard-input",
      value: cur.wait_minutes ?? 60,
      min: "5",
      style: "width: 70px;",
    });
    waitInput.addEventListener("change", () => {
      draft.audience.canary.wait_minutes = Math.max(5, parseInt(waitInput.value, 10) || 60);
    });
    const errInput = el("input", {
      type: "number",
      class: "wizard-input",
      value: Math.round((cur.max_error_rate ?? 0.1) * 100),
      min: "0",
      max: "100",
      step: "1",
      style: "width: 70px;",
    });
    errInput.addEventListener("change", () => {
      const v = Math.max(0, Math.min(100, parseInt(errInput.value, 10) || 10));
      draft.audience.canary.max_error_rate = v / 100;
    });
    wrap.appendChild(
      el(
        "div",
        { class: "row", style: "gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 4px;" },
        [
          el("span", { class: "muted" }, "Sample:"),
          sampleInput,
          el("span", { class: "muted" }, "%"),
          el("span", { class: "muted", style: "margin-left: 12px;" }, "Wait:"),
          waitInput,
          el("span", { class: "muted" }, "min"),
          el("span", { class: "muted", style: "margin-left: 12px;" }, "Max error rate:"),
          errInput,
          el("span", { class: "muted" }, "%"),
        ],
      ),
    );
  }
  return wrap;
}

// ─────────────────────────────────────────────────────────────────
// Hub renderers (2026-05-30 from-scratch reorg)
// ─────────────────────────────────────────────────────────────────
//
// Two new hubs replace the 9 LinkedIn/Email/Telegram entries that
// used to live in the sidebar:
//
//   #automations  → renderAutomationsHub   (platform tabs LI/X/Gmail/TG)
//   #action-log   → renderActivityHub      (platform tabs All/LI/X/Gmail/TG)
//
// Each hub renders a tab strip + a content area, then delegates to a
// body builder for the active tab. LinkedIn tabs reuse the existing
// body helpers (_buildLiAutomationsBody, _buildLiActivityBody). Other
// platform tabs are Phase A stubs that surface the existing legacy
// pages as cards-as-links — full inline rendering lands in Phase B.

// Shared tab strip used by both hubs. `spec` = [{ id, label, icon, route, badge? }, ...].
// `activeId` highlights the current tab. Clicking a non-active tab
// navigates to its route.
function _renderHubTabStrip(spec, activeId) {
  return el(
    "div",
    {
      class: "hub-tab-strip",
      style:
        "display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap;padding-bottom:10px;border-bottom:1px solid var(--border-soft);",
    },
    spec.map((t) => {
      const isActive = t.id === activeId;
      const btn = el(
        "button",
        {
          type: "button",
          class: `btn btn-tiny ${isActive ? "btn-primary" : "btn-secondary"}`,
          style: t.disabled && !isActive ? "opacity:0.55;" : "",
          title: t.disabled ? `${t.label} — not yet wired` : t.label,
          onclick: () => {
            if (!isActive) navigate(t.route);
          },
        },
        [
          // A drawn mark, not the field printed as text. _renderHubTabStrip is
          // SHARED by four hubs, and their tab tables are migrating to mark names
          // at different times, so icon.for() takes either and this one fix covers
          // all of them. Printing the field was about to render "users Automations".
          el("span", {}, [
            icon.for(t.icon) ? icon(icon.for(t.icon)) : el("span", {}, t.icon),
            " " + t.label,
          ]),
          t.badge
            ? el(
                "span",
                {
                  style:
                    "margin-left:6px;font-size:9px;background:var(--border-soft);color:var(--text-2);padding:1px 5px;border-radius:8px;",
                },
                t.badge,
              )
            : null,
        ],
      );
      return btn;
    }),
  );
}

// ── #automations hub ─────────────────────────────────────────────
//
// Platform tab strip [LinkedIn] [X] [Gmail] [Telegram] + content area.
// LinkedIn tab is fully wired (calls _buildLiAutomationsBody). X is an
// empty state. Gmail + Telegram surface the existing legacy pages as
// cards-as-links — Phase B inlines them as proper saved-config cards.
// Shared capability banner — rendered identically at the top of EVERY
// Automations tab so the four platforms read as one section with one mental
// model, differing only in HOW each runs. The three body structures below the
// banner are an in-progress convergence (LinkedIn runs from here; X runs on
// x.com; Gmail/Telegram are server-cron configs) — the banner names that
// difference up front instead of surprising the user after they click.
const _AUTO_CAPABILITY = {
  linkedin: {
    icon: "link",
    mode: "Runs from here",
    tone: "#4ade80",
    bg: "#0f1a13",
    bd: "#1f3d2a",
    blurb:
      "LinkedIn actions execute in a background tab — click a card's Run button and watch progress live.",
  },
  x: {
    icon: "bird",
    mode: "Runs on x.com",
    tone: "#93c5fd",
    bg: "#0f1620",
    bd: "#1f3048",
    blurb:
      "Some X actions now run from here — configure and click Run, and they execute in a hidden x.com window (no need to be on X). The rest still open X for the in-page launcher while we migrate them.",
  },
  gmail: {
    icon: "mail",
    mode: "Configure & schedule",
    tone: "#c4b5fd",
    bg: "#16121f",
    bd: "#2e2448",
    blurb:
      "Email automations run server-side on a cron — save a config and it runs itself. No browser tab needed.",
  },
  telegram: {
    icon: "mail",
    mode: "Configure & schedule",
    tone: "#c4b5fd",
    bg: "#16121f",
    bd: "#2e2448",
    blurb:
      "Telegram automations run through the bot — save a config and it runs itself. No browser tab needed.",
  },
};
function _renderAutomationCapabilityBanner(platform) {
  const c = _AUTO_CAPABILITY[platform];
  if (!c) return null;
  return el(
    "div",
    {
      style: `display:flex;align-items:center;gap:12px;padding:10px 14px;margin-bottom:16px;border:1px solid ${c.bd};background:${c.bg};border-radius:8px;`,
    },
    [
      el("span", { style: "font-size:18px;line-height:1;" }, c.icon),
      el("div", { style: "flex:1;font-size:12px;line-height:1.5;color:var(--text-2);" }, [
        el("span", { style: `font-weight:600;color:${c.tone};` }, c.mode + " — "),
        el("span", {}, c.blurb),
      ]),
    ],
  );
}

// ═══════════════════════════════════════════════════════════════════
// Unified automations layer (2026-05-31)
//
// The hub used to show ONLY the 4 platform tabs driven by client-side
// catalogs (LI_ACTION_REGISTRY + PLATFORM_ACTIONS) and saved-config
// endpoints. The 26 server-side presets (signal_*, outreach_orchestrator,
// enrichment_pipeline, …), their cloned customs, and the entire
// Multi-platform + System groups never appeared anywhere on this page.
//
// This layer fixes BOTH user-reported problems:
//   1) Completeness — fetch the full server catalog (automations-list)
//      once, classify every preset/custom by platform, and surface them
//      under their platform tab + two new tabs (Multi / System). Nothing
//      is hidden: anything unmapped falls into System.
//   2) Consistency — EVERY card (LinkedIn action, X action, preset,
//      custom) opens the SAME detail shell via openAutomationDetail().
//      The type-specific action (configure-and-run / open-on-x / clone /
//      run-now) becomes a button inside that one shell instead of four
//      different modals and navigate-aways.
// ═══════════════════════════════════════════════════════════════════

// Presets carry no `platform`/`category` field on disk, so classification
// lives here (derived from AUTOMATIONS.md). Keyed by preset id (also the
// source_preset_id of any clone). Unmapped ids fall through to "system"
// so a newly-added preset is always visible somewhere, never dropped.
const _PRESET_PLATFORM = {
  // LinkedIn
  signal_linkedin: "linkedin",
  crypto_hiring_outreach: "linkedin", // jobs→AI→find contact→draft LinkedIn comments (review queue)
  // X / Twitter
  signal_twitter: "x",
  cross_account_react: "x",
  coordinated_post: "x", // Boost suite — also surfaced in #boost
  raid_army: "x", // Boost suite — also surfaced in #boost
  // Telegram
  tg_campaign: "telegram",
  account_warmer: "telegram",
  group_join: "telegram",
  feed_monitor: "telegram",
  active_member_listener: "telegram",
  daily_briefing: "telegram",
  // Email (rendered under the Gmail tab)
  email_campaign: "email",
  email_sequence: "email",
  // Multi-platform
  outreach_orchestrator: "multi",
  comment_campaign: "multi",
  event_promotion: "multi",
  new_token_outreach: "multi",
  coin_mm_outreach: "multi", // TG-DM + group post + X + LinkedIn + email
  coin_mm_group_activation: "telegram", // multi-account TG group play (opt-in)
  group_amplification: "multi",
  engagement_boost: "multi", // Boost suite — also surfaced in #boost
  // System & data (no outbound platform)
  enrichment_pipeline: "system",
  top_tokens_ingest: "system",
  opportunity_defillama: "system",
  opportunity_rootdata: "system",
  opportunity_jobs: "system",
  signal_reddit: "system",
  signal_hn: "system",
};

function _presetPlatform(presetId) {
  return _PRESET_PLATFORM[presetId] || "system";
}

// Per-tab presentation. `bucket` is the buckets[] key the tab reads.
// "email" presets render under the "gmail" tab.
const _AUTO_TABS = [
  { id: "linkedin", icon: "link", label: "LinkedIn", bucket: "linkedin" },
  { id: "x", icon: "bird", label: "X", bucket: "x" },
  { id: "gmail", icon: "mail", label: "Gmail", bucket: "gmail" },
  { id: "telegram", icon: "mail", label: "Telegram", bucket: "telegram" },
  { id: "multi", icon: "shuffle", label: "Multi-platform", bucket: "multi" },
  { id: "system", icon: "settings", label: "System", bucket: "system" },
];

// Fetch the server catalog once and refresh the wired-runners cache so
// run-now buttons know which presets can actually execute.
async function _fetchAutomationsCatalog() {
  // Carry an error sentinel (not a bare {}) so the hub can tell "the catalog
  // failed to load" apart from "the user genuinely has no server automations"
  // — the latter must never be shown when the former is true (silent failure).
  let data;
  try {
    data = await api("automations-list");
  } catch (e) {
    return { __error: e?.message || "Couldn't reach the bot" };
  }
  if (data?.error) return { __error: data.error };
  if (Array.isArray(data?.wired_runners)) _wiredRunnersCache = new Set(data.wired_runners);
  return data || {};
}

// Group the flat workflows list into per-tab buckets. Each workflow is a
// preset (kind !== "custom") or a custom (kind === "custom", carries
// source_preset_id). Email maps onto the gmail bucket.
function _bucketAutomations(workflows) {
  const buckets = { linkedin: [], x: [], gmail: [], telegram: [], multi: [], system: [] };
  for (const w of workflows || []) {
    const pid = w.kind === "custom" ? w.source_preset_id || w.id : w.id;
    let plat = _presetPlatform(pid);
    if (plat === "email") plat = "gmail";
    (buckets[plat] || buckets.system).push(w);
  }
  return buckets;
}

// Is this preset's engine actually wired to run from a button?
function _autoIsWired(presetId) {
  try {
    return _wiredRunnersCache instanceof Set && _wiredRunnersCache.has(presetId);
  } catch {
    return false;
  }
}

// ── Shared card ────────────────────────────────────────────────────
// One card renderer for presets AND customs. Click → openAutomationDetail.
// Visual parity with the LinkedIn/X action cards (same padding, border,
// hover treatment) so all six tabs read as one surface.
function _renderUnifiedAutoCard(wf) {
  const isCustom = wf.kind === "custom";
  const status = wf.status || (isCustom ? "draft" : "preset");
  const open = () =>
    openAutomationDetail(isCustom ? { type: "custom", wf } : { type: "preset", wf });
  // Real button semantics for keyboard + screen-reader users (role/tabindex/
  // Enter/Space), not a bare clickable <div>.
  const card = el(
    "div",
    {
      class: "card unified-auto-card",
      role: "button",
      tabindex: "0",
      "aria-label": `${wf.name || wf.id} — view details`,
      style:
        "background:var(--bg-card);border:1px solid var(--border-soft);border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:8px;cursor:pointer;transition:border-color 120ms,background 120ms;",
      onmouseenter: (e) => {
        e.currentTarget.style.borderColor = "var(--border-hover)";
        e.currentTarget.style.background = "#1f1f28";
      },
      onmouseleave: (e) => {
        e.currentTarget.style.borderColor = "var(--border-soft)";
        e.currentTarget.style.background = "var(--bg-card)";
      },
      onclick: open,
      onkeydown: (e) => {
        if (e.key === "Enter" || e.key === " ") {
          e.preventDefault();
          open();
        }
      },
    },
    [
      el("div", { style: "display:flex;align-items:center;gap:8px;" }, [
        el(
          "h4",
          { style: "margin:0;font-size:14px;color:var(--text-1);flex:1;" },
          wf.name || wf.id,
        ),
        isCustom
          ? _renderConfigStatusPill(status)
          : el(
              "span",
              {
                style:
                  "font-size:10px;background:var(--bg-1);color:var(--text-3);padding:2px 7px;border-radius:6px;text-transform:uppercase;letter-spacing:0.4px;",
              },
              "preset",
            ),
      ]),
      el(
        "div",
        { class: "muted", style: "font-size:12px;line-height:1.5;color:#999;flex:1;" },
        wf.description || "—",
      ),
      el("div", { style: "display:flex;gap:6px;margin-top:auto;align-items:center;" }, [
        _autoIsWired(isCustom ? wf.source_preset_id || wf.id : wf.id)
          ? el(
              "span",
              {
                style:
                  "font-size:10px;color:var(--green);background:#0f1a13;padding:2px 7px;border-radius:6px;",
              },
              "● runnable",
            )
          : el(
              "span",
              {
                style:
                  "font-size:10px;color:var(--text-3);background:var(--bg-1);padding:2px 7px;border-radius:6px;",
              },
              "scheduled",
            ),
        el(
          "span",
          { style: "font-size:10px;color:var(--text-4);margin-left:auto;" },
          "view details →",
        ),
      ]),
    ],
  );
  return card;
}

// A preset/custom section for a given tab bucket. Splits "My workflows"
// (customs) from "Available automations" (presets) with a shared heading
// style. Returns null when the bucket is empty so callers can skip it.
function _renderAutoPresetSection(items, { heading } = {}) {
  if (!items || items.length === 0) return null;
  const customs = items.filter((w) => w.kind === "custom");
  const presets = items.filter((w) => w.kind !== "custom");

  const wrap = el("div", { style: "margin-top:6px;" });
  const sub = (title, blurb, list) => {
    if (!list.length) return;
    wrap.appendChild(
      el("div", { style: "display:flex;align-items:baseline;gap:10px;margin:18px 0 8px;" }, [
        el("h3", { style: "margin:0;font-size:13px;color:var(--text-1);" }, title),
        el(
          "span",
          {
            style:
              "font-size:10px;background:var(--border-soft);color:var(--text-2);padding:1px 6px;border-radius:8px;font-family:monospace;",
          },
          String(list.length),
        ),
        blurb ? el("span", { class: "muted", style: "font-size:11px;" }, blurb) : null,
      ]),
    );
    const grid = el("div", {
      style: "display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px;",
    });
    for (const w of list) grid.appendChild(_renderUnifiedAutoCard(w));
    wrap.appendChild(grid);
  };
  if (heading)
    wrap.appendChild(
      el("h2", { style: "margin:22px 0 2px;font-size:15px;color:var(--text-1);" }, heading),
    );
  sub("My workflows", "Cloned from a preset — editable + runnable.", customs);
  sub(
    "Available automations",
    "Server-side engines. Clone one to customize, or it runs on its schedule.",
    presets,
  );
  return wrap;
}

// ── #action-log hub ──────────────────────────────────────────────
//
// Platform tab strip [All] [LinkedIn] [X] [Gmail] [Telegram] + content.
// LinkedIn tab calls _buildLiActivityBody (full feed + filters + 5s
// poll). Other tabs are Phase A stubs until the unified
// /api/activity/unified endpoint lands.
async function renderActivityHub({ platform } = {}) {
  // Default to "all" — the unified feed is the most useful landing once
  // multiple platforms feed it. Users can still drill into a single
  // platform via the tab strip.
  const activePlatform = platform || "all";
  _stopAllActivityPoll(); // tab switch / re-entry — drop any prior unified poll
  const wrapper = el("div", { class: "activity-hub", style: "padding:16px;" });
  setContentNode(wrapper);

  wrapper.appendChild(
    el("div", { class: "page-header", style: "margin-bottom:10px;" }, [
      el("h1", { style: "margin:0;font-size:18px;" }, "Action log"),
      el(
        "p",
        { class: "muted", style: "margin:4px 0 0;font-size:12px;" },
        "Every action recorded — what fired, when, to whom, with what result. Use the tabs to scope by platform.",
      ),
    ]),
  );

  const tabs = [
    { id: "all", icon: "globe", label: "All", route: "action-log/all" },
    { id: "linkedin", icon: "link", label: "LinkedIn", route: "action-log/linkedin" },
    { id: "x", icon: "bird", label: "X", route: "action-log/x" },
    {
      id: "gmail",
      icon: "mail",
      label: "Gmail",
      route: "action-log/gmail",
      disabled: true,
      badge: "soon",
    },
    { id: "telegram", icon: "mail", label: "Telegram", route: "action-log/telegram" },
  ];
  wrapper.appendChild(_renderHubTabStrip(tabs, activePlatform));

  const content = el("div", { class: "hub-content" });
  wrapper.appendChild(content);

  if (activePlatform === "linkedin") {
    await _buildLiActivityBody(content, { routeName: "action-log" });
  } else if (activePlatform === "all") {
    await _buildAllActivityBody(content);
  } else if (activePlatform === "telegram") {
    await _buildTgActivityBody(content);
  } else if (activePlatform === "x") {
    await _buildXActivityBody(content);
  } else if (activePlatform === "gmail") {
    content.appendChild(_renderActivityPlatformStub(activePlatform));
  } else {
    content.appendChild(_renderActivityAllStub());
  }
}

// ── X (Twitter) Activity view ─────────────────────────────────────────
// Mirror of the LinkedIn Activity view, backed by twitter_action_log via
// /api/twitter/log/recent. Rows have the same shape as LinkedIn rows, so we
// reuse _renderLinkedInActivityRow / _openLinkedInActivityDrawer. Polls every
// 5s. This is the live, in-dashboard counterpart to the x-action-trace.jsonl
// file an operator (or Claude) reads on disk.
const _X_ACTIVITY_MAX_ROWS = 200;
let _xActivityRows = [];
let _xActivityLastId = 0;
let _xActivityPollTimer = null;
// JSON of the rows currently mounted in #x-activity-feed. The row renderer is a
// pure function of the row object, so an unchanged signature means a rebuild
// would produce byte-identical DOM — and the 5s full poll can skip it.
let _xActivityFeedSig = "";

function _stopXActivityPoll() {
  if (_xActivityPollTimer) {
    clearInterval(_xActivityPollTimer);
    _xActivityPollTimer = null;
  }
}

function _renderXActivityHeader(stats) {
  const s = stats || { ok: 0, fail: 0, skip: 0, byErrorCode: {} };
  const topErrors = Object.entries(s.byErrorCode || {})
    .sort((a, b) => b[1] - a[1])
    .slice(0, 4);
  return el(
    "div",
    {
      class: "li-activity-header",
      style: "display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:10px;",
    },
    [
      el("span", { class: "chip", style: "background:#10311f;color:#5fdc8f;" }, `${s.ok || 0} ok`),
      el(
        "span",
        { class: "chip", style: "background:#3a1414;color:#ff8b8b;" },
        `${s.fail || 0} fail`,
      ),
      el(
        "span",
        { class: "chip", style: "background:var(--bg-3);color:var(--text-2);" },
        `${s.skip || 0} skip`,
      ),
      ...topErrors.map(([code, n]) =>
        el("span", { class: "li-activity-error-chip", title: "last 24h" }, `${code} ×${n}`),
      ),
    ],
  );
}

async function _refreshXActivity({ full = false } = {}) {
  const params = { limit: _X_ACTIVITY_MAX_ROWS, stats: 1 };
  if (!full && _xActivityLastId > 0) params.since = _xActivityLastId;
  const logRes = await api("tw-server-log-recent", { params }).catch((e) => ({ error: e.message }));

  const feedEl = document.getElementById("x-activity-feed");
  if (logRes?.error) {
    if (feedEl && full) {
      feedEl.innerHTML = "";
      feedEl.appendChild(
        el("div", { class: "empty-state", style: "padding:24px;" }, [
          el("div", { class: "empty-state-icon" }, icon("alert")),
          el("div", { class: "empty-state-title" }, "Couldn't load X activity"),
          el("div", { class: "empty-state-hint" }, logRes.error),
          el(
            "button",
            {
              class: "btn btn-secondary btn-tiny",
              style: "margin-top:12px;",
              onclick: () => _refreshXActivity({ full: true }),
            },
            "Retry",
          ),
        ]),
      );
      _xActivityFeedSig = "";
    }
    return;
  }

  if (logRes?.rows) {
    if (full) _xActivityRows = logRes.rows;
    else _xActivityRows = [...logRes.rows, ..._xActivityRows].slice(0, _X_ACTIVITY_MAX_ROWS);
    if (logRes.rows[0]) _xActivityLastId = Math.max(_xActivityLastId, logRes.rows[0].id);
  }

  if (feedEl) {
    // Skip the destroy+rebuild when the poll came back with the very same rows
    // that are already mounted (the steady state at 5s intervals). The count
    // check keeps us honest if anything else emptied the feed.
    const sig = JSON.stringify(_xActivityRows);
    const mountedCount = _xActivityRows.length || 1; // rows, or the single empty-state block
    if (sig !== _xActivityFeedSig || feedEl.childElementCount !== mountedCount) {
      feedEl.innerHTML = "";
      if (_xActivityRows.length === 0) {
        feedEl.appendChild(
          el("div", { class: "empty-state", style: "padding:32px;" }, [
            el("div", { class: "empty-state-icon" }, icon("inbox")),
            el("div", { class: "empty-state-title" }, "No X actions yet"),
            el(
              "div",
              { class: "empty-state-hint" },
              "Run an X action (e.g. Engage Feed) from the automations tab. Steps appear here live, and the full cross-process trace is written to nexus-bot/data/logs/x-action-trace.jsonl (run `npm run x-trace` for a merged view).",
            ),
            el("div", { style: "margin-top:16px;" }, [
              el(
                "button",
                { class: "btn btn-secondary btn-tiny", onclick: () => navigate("automations/x") },
                "X automations",
              ),
            ]),
          ]),
        );
      } else {
        // Reuse the LinkedIn row renderer — identical row shape.
        for (const row of _xActivityRows) feedEl.appendChild(_renderLinkedInActivityRow(row));
      }
      _xActivityFeedSig = sig;
    }
  }

  if (full && logRes?.stats) {
    const headerEl = document.getElementById("x-activity-header-mount");
    if (headerEl) {
      headerEl.innerHTML = "";
      headerEl.appendChild(_renderXActivityHeader(logRes.stats));
    }
  }
}

async function _buildXActivityBody(parent) {
  _stopXActivityPoll();
  _xActivityRows = [];
  _xActivityLastId = 0;
  _xActivityFeedSig = "";

  parent.appendChild(el("div", { id: "x-activity-header-mount" }, [_renderXActivityHeader(null)]));
  parent.appendChild(
    el("div", {
      id: "x-activity-feed",
      class: "li-activity-feed",
      style: "display:flex;flex-direction:column;gap:2px;",
    }),
  );

  await _refreshXActivity({ full: true });
  // Full refresh each tick keeps the stats header live and avoids dedup logic;
  // 200 rows every 5s is cheap for a dashboard view.
  _xActivityPollTimer = setInterval(() => _refreshXActivity({ full: true }), 5000);
}

// ─────────────────────────────────────────────────────────────────
// Goals / Objectives hub (objective-based guidance)
// ─────────────────────────────────────────────────────────────────
//
// An objective is a goal-oriented playbook (defined server-side in
// nexus-bot/src/workflow/objectives/) that maps "what are you trying to do?"
// onto the existing presets / services / extension actions. The hub lists
// objectives; the detail view walks the phased steps with 1-click setup
// (clone the right presets as drafts), deep-links to services/actions, and a
// pinned "how to monitor" section.

// ─────────────────────────────────────────────────────────────────
// Connections hub — one place to see what's connected + how to add more.
// Replaces the scattered, technical connect flows with a single status page
// (onboarding = activation; a clear "what's missing" list lifts completion).
// ─────────────────────────────────────────────────────────────────
// Playbooks — "mission sections" that bundle customized automations
// ─────────────────────────────────────────────────────────────────
// Boost — coordinated multi-account project-boosting campaigns
// ─────────────────────────────────────────────────────────────────
// Cross-platform action log — unified row renderer + body builders
// ─────────────────────────────────────────────────────────────────
// r50 — LinkedIn Automations view
// ── Bootstrap ────────────────────────────────────────────────────
init();

// ═══════════════════════════════════════════════════════════════════
// Company Console + Conversation Plays + Group-reply review — re-homed
// onto main's split dashboard.js during the origin/main merge (2026-07).
// main lacked these; they are classic-script globals like the rest.
// ═══════════════════════════════════════════════════════════════════
async function renderGroupReplyReview() {
  const wrap = el("div", { style: "padding:16px;max-width:720px;" });
  setContentNode(wrap);
  wrap.appendChild(
    el("div", { class: "page-header", style: "margin-bottom:10px;" }, [
      el("h1", { style: "margin:0;font-size:18px;" }, "Group replies — review"),
      el(
        "p",
        { class: "muted", style: "margin:4px 0 0;font-size:12px;line-height:1.5;" },
        "When someone genuinely asks for a service you offer in a group or feed, the authentic conversation-play tier drafts a transparent reply (with a disclosure line). Review and approve each one before it posts — approved replies are delivered into the thread automatically.",
      ),
    ]),
  );
  wrap.appendChild(
    el(
      "button",
      {
        class: "btn btn-primary",
        style: "margin-top:8px;",
        onclick: () => showGroupReplyDrawer(),
      },
      "Open review queue →",
    ),
  );
  const strip = el("div", { class: "muted small", style: "margin-top:14px;" }, "Loading counts…");
  wrap.appendChild(strip);
  try {
    const data = await api("grd-list", { queryString: "?status=drafted&limit=1" });
    const c = data?.counts || {};
    strip.textContent = `${c.drafted || 0} awaiting review · ${c.approved || 0} approved · ${c.sent || 0} sent`;
  } catch {
    strip.textContent = "";
  }
  showGroupReplyDrawer();
}

async function showGroupReplyDrawer() {
  let activeStatus = "drafted";

  async function refresh() {
    const data = await api("grd-list", {
      queryString: `?status=${encodeURIComponent(activeStatus)}&limit=100`,
    });
    if (data?.error) {
      toast(`Error: ${data.error}`, "err");
      return;
    }
    renderBody(data);
  }

  const PLATFORM_ICON = { telegram: "✈️", x: "𝕏" };

  function renderBody(data) {
    const body = $("#drawer-body");
    if (!body) return;
    body.innerHTML = "";
    const counts = data.counts || {};
    const drafts = data.drafts || [];

    body.appendChild(
      el(
        "div",
        { class: "muted small", style: "line-height:1.6;margin-bottom:12px;" },
        "Transparent replies to REAL questions people asked in groups/feeds. Each reply carries a disclosure line and posts only after you approve it.",
      ),
    );

    const countsRow = el("div", {
      class: "row",
      style: "gap:6px;flex-wrap:wrap;margin-bottom:12px;font-size:11px;",
    });
    const PILL = (label, n, color) =>
      el(
        "span",
        {
          style: `padding:3px 8px;background:var(--bg-2);border:1px solid ${color}33;color:${color};border-radius:var(--radius-full);font-weight:600;`,
        },
        `${label} ${n || 0}`,
      );
    countsRow.appendChild(PILL("drafted", counts.drafted, "var(--accent)"));
    countsRow.appendChild(PILL("approved", counts.approved, "var(--blue)"));
    countsRow.appendChild(PILL("sent", counts.sent, "var(--green)"));
    if (counts.skipped) countsRow.appendChild(PILL("skipped", counts.skipped, "var(--text-4)"));
    if (counts.failed) countsRow.appendChild(PILL("failed", counts.failed, "var(--red)"));
    body.appendChild(countsRow);

    const subtabs = el("div", { class: "tabs", style: "margin-bottom:8px;font-size:11px;" });
    for (const s of ["drafted", "approved", "sent", "skipped", "failed"]) {
      subtabs.appendChild(
        el(
          "button",
          {
            class: `tab${activeStatus === s ? " active" : ""}`,
            type: "button",
            onclick: () => {
              activeStatus = s;
              refresh();
            },
          },
          `${s} ${counts[s] || 0}`,
        ),
      );
    }
    body.appendChild(subtabs);

    if (drafts.length === 0) {
      body.appendChild(
        el(
          "div",
          { class: "muted small", style: "padding:12px;text-align:center;" },
          activeStatus === "drafted"
            ? "No drafts yet. When the listener catches a real buying-intent question in a group you target, an AI-drafted reply appears here for review."
            : `No ${activeStatus} drafts.`,
        ),
      );
      return;
    }

    if (activeStatus === "drafted") {
      body.appendChild(
        el(
          "button",
          {
            class: "btn btn-tiny btn-primary",
            type: "button",
            style: "margin-bottom:10px;",
            onclick: async () => {
              const ids = drafts.map((d) => d.id);
              const r = await api("grd-approve", { body: { ids } });
              if (r?.ok) {
                toast(`Approved ${r.updated} replies`, "ok");
                refresh();
              }
            },
          },
          `Approve all ${drafts.length} visible`,
        ),
      );
    }

    for (const row of drafts) {
      const card = el("div", {
        style:
          "border:1px solid var(--border);background:var(--bg-2);border-radius:var(--radius-md);padding:10px 12px;margin-bottom:8px;",
      });
      card.appendChild(
        el(
          "div",
          { class: "row", style: "gap:6px;align-items:center;margin-bottom:4px;font-size:12px;" },
          [
            el("span", { style: "font-size:14px;" }, PLATFORM_ICON[row.platform] || "💬"),
            el(
              "span",
              { style: "font-weight:700;" },
              row.source_handle ? `@${row.source_handle}` : "someone",
            ),
            el("span", { class: "muted small" }, `· in ${row.chat_id}`),
            el(
              "span",
              { class: "muted small", style: "margin-left:auto;" },
              `disclosure: ${row.disclosure_mode || "soft"}`,
            ),
          ],
        ),
      );
      // The real question we're answering (context).
      if (row.source_text) {
        card.appendChild(
          el(
            "div",
            {
              style:
                "background:var(--bg-1);border-left:2px solid var(--border);padding:6px 8px;margin-bottom:6px;font-size:11px;color:var(--text-2);line-height:1.5;max-height:64px;overflow:hidden;",
            },
            `"${String(row.source_text).slice(0, 220)}${row.source_text.length > 220 ? "…" : ""}"`,
          ),
        );
      }
      // Editable drafted reply (disclosure already appended).
      const ta = el(
        "textarea",
        {
          class: "wizard-textarea",
          rows: "3",
          style: "width:100%;font-size:12px;line-height:1.5;",
          onblur: async (e) => {
            const t = e.target.value.trim();
            if (t === row.ai_drafted_reply) return;
            const r = await api("grd-edit", { body: { id: row.id, reply: t } });
            if (r?.ok) {
              toast("Draft saved", "ok", 1200);
              row.ai_drafted_reply = t;
            } else toast(r?.error || "save failed", "err");
          },
        },
        row.ai_drafted_reply || "",
      );
      card.appendChild(ta);

      if (row.last_error) {
        card.appendChild(
          el(
            "div",
            {
              class: "muted small",
              style: "color:var(--red);font-family:var(--font-mono);font-size:10px;margin-top:4px;",
            },
            row.last_error,
          ),
        );
      }

      if (row.status === "drafted") {
        card.appendChild(
          el("div", { class: "row", style: "gap:6px;margin-top:6px;" }, [
            el(
              "button",
              {
                class: "btn btn-tiny btn-primary",
                type: "button",
                onclick: async () => {
                  const r = await api("grd-approve", { body: { ids: [row.id] } });
                  if (r?.ok) {
                    toast("Approved", "ok", 1500);
                    refresh();
                  }
                },
              },
              "Approve",
            ),
            el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                onclick: async () => {
                  const r = await api("grd-skip", { body: { ids: [row.id] } });
                  if (r?.ok) {
                    toast("Skipped", "ok", 1500);
                    refresh();
                  }
                },
              },
              "Skip",
            ),
          ]),
        );
      }

      const meta = [];
      if (row.approved_at) meta.push(`approved ${fmtDate(row.approved_at)}`);
      if (row.sent_at) meta.push(`sent ${fmtDate(row.sent_at)}`);
      if (meta.length)
        card.appendChild(
          el(
            "div",
            { class: "muted small mono", style: "font-size:10px;margin-top:4px;" },
            meta.join(" · "),
          ),
        );

      body.appendChild(card);
    }
  }

  openDrawer(
    "Group replies — approve before posting",
    el("div", { class: "muted small" }, "Loading…"),
  );
  refresh();
}

// ── Platform Health → event detail modal ─────────────────────────
//
// Click a row in "Recent inbound" or "Recent outbound" inside the platform
// lane drawer → opens a modal that shows the FULL message text, who-from
// → who-to with both sides' identifiers, and platform-aware deep links so
// the operator can jump straight to the exact tweet / DM thread / email
// thread they're looking at. Designed to be opinionated about one thing:
// "where do I see this on the platform itself?".

// Visual identity per platform — used to tint the modal hero so the
// operator instantly recognises which surface they're looking at.

const CONSOLE_BOOKS = [
  { svc: "crypto_native_mm", label: "Market making (crypto-native)" },
  { svc: "rwa_liquidity", label: "RWA liquidity (institutional)" },
  { svc: "volume_hft_miniapp", label: "Trading volume (mini-app)" },
  { svc: "platform_marketing", label: "Marketing / growth" },
];
const CONSOLE_STATE_BG = { live: "#13211a", draft: "#1b1b24", paused: "#241d10", done: "#181818" };
const CONSOLE_STATE_FG = { live: "#5fd08a", draft: "#9aa0aa", paused: "#f0b34e", done: "#777" };
const CONSOLE_CH_LABEL = {
  x: "X",
  linkedin: "LinkedIn",
  telegram: "Telegram",
  email: "Email",
  multi: "Multi",
};
const FUNNEL_ORDER = ["matched", "eligible", "attempted", "sent", "replied", "converted"];

async function renderCompanyConsole(params) {
  const companyId = params && params.id;
  const root = el("div", { style: "max-width:1000px;margin:0 auto;padding:20px 16px;" });
  setContentNode(root);
  if (!companyId) {
    root.appendChild(
      el("div", { style: "color:var(--red);font-size:13px;" }, "No company selected."),
    );
    return;
  }

  let data = null;
  let activeTab = "campaigns";

  const load = async () => {
    try {
      data = await api("company-console", { companyId });
    } catch {
      data = null;
    }
  };

  const numericId = (c) => parseInt(String(c.id).split(":")[1] || c.id, 10);
  const CONSOLE_CHANNELS = [
    ["twitter", "𝕏 X"],
    ["telegram", "Telegram"],
    ["linkedin", "LinkedIn"],
    ["email", "Email"],
  ];

  // Start a paused campaign, but show a faithful dry-run preview + confirm first.
  const startWithPreview = (c) => {
    (async () => {
      let pr = null;
      try {
        pr = await api("run-preview", { body: { kind: "composition", id: numericId(c) } });
      } catch {
        pr = null;
      }
      const manifest = pr && pr.manifest;
      const body = el("div", {});
      if (manifest) {
        body.appendChild(
          el(
            "div",
            { style: "font-size:13px;color:#cdd;margin-bottom:8px;" },
            manifest.summary || "This campaign will run as configured.",
          ),
        );
        for (const w of manifest.warnings || [])
          body.appendChild(
            el("div", { style: "color:#f0b34e;font-size:12px;margin-bottom:4px;" }, `${w}`),
          );
        const acts = manifest.actions || [];
        if (acts.length) {
          const list = el("div", {
            style:
              "max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:8px;padding:6px 10px;margin-top:6px;",
          });
          for (const a of acts.slice(0, 80)) {
            list.appendChild(
              el(
                "div",
                {
                  style:
                    "font-size:12px;color:#bcc;padding:3px 0;border-top:1px solid var(--bg-card);",
                },
                a.message ? `↳ ${a.message}` : `• ${a.action}${a.target ? ": " + a.target : ""}`,
              ),
            );
          }
          body.appendChild(list);
        }
      } else {
        body.appendChild(
          el(
            "div",
            { style: "color:#f0b34e;font-size:13px;" },
            "Couldn't build a preview — you can still start it; it will run as configured.",
          ),
        );
      }
      const doStart = async () => {
        modal.close();
        let r = null;
        try {
          r = await api("campaigns-action", { body: { id: c.id, action: "resume" } });
        } catch {
          r = null;
        }
        if (r && r.ok) {
          c.state = r.state;
          toast("Campaign started", "ok");
          draw();
        } else toast((r && r.error) || "Couldn't start", "err");
      };
      const modal = openModal({
        title: "Start campaign — preview",
        subtitle: c.name,
        body,
        actions: [
          uiButton("Cancel", { size: "tiny", onClick: () => modal.close() }),
          uiButton("Confirm & Start", { variant: "primary", size: "tiny", onClick: doStart }),
        ],
      });
    })();
  };

  // Inline per-campaign settings editor → POST company-update-campaign (owner-scoped).
  const buildParamsEditor = (c, comp) => {
    const wrap = el("div", {
      style:
        "border:1px solid var(--border);border-radius:8px;background:var(--bg-2);padding:12px 14px;",
    });
    wrap.appendChild(
      el(
        "div",
        {
          style:
            "font-size:12px;color:#9aa0aa;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;",
        },
        "Settings",
      ),
    );

    const initialChannels = new Set(((comp && comp.channels) || []).map((ch) => ch.platform));
    const chBoxes = {};
    const chRow = el("div", { style: "display:flex;gap:12px;flex-wrap:wrap;margin-bottom:10px;" });
    for (const [key, label] of CONSOLE_CHANNELS) {
      const cb = el("input", { type: "checkbox" });
      if (initialChannels.has(key)) cb.checked = true;
      chBoxes[key] = cb;
      chRow.appendChild(
        el(
          "label",
          { style: "display:flex;align-items:center;gap:5px;font-size:12px;color:#cdd;" },
          [cb, label],
        ),
      );
    }
    wrap.appendChild(
      el("div", {}, [
        el("div", { style: "font-size:11px;color:var(--text-4);margin-bottom:4px;" }, "Channels"),
        chRow,
      ]),
    );

    const schedSel = el("select", { class: "wizard-input", style: "width:150px;" }, [
      el("option", { value: "schedule" }, "Scheduled (auto)"),
      el("option", { value: "manual" }, "Manual only"),
    ]);
    schedSel.value = comp && comp.trigger && comp.trigger.type === "manual" ? "manual" : "schedule";

    const curAud = (comp && comp.audience) || {};
    const curResolver =
      (curAud.resolvers || []).find(
        (r) => String(r.type || "").toLowerCase() === "crm_by_company_ids",
      ) || {};
    const limitInp = el("input", {
      class: "wizard-input",
      type: "number",
      min: "1",
      style: "width:90px;",
      value: curResolver.limit != null ? String(curResolver.limit) : "",
    });
    const exclInp = el("input", {
      class: "wizard-input",
      type: "number",
      min: "0",
      style: "width:90px;",
      value:
        curAud.exclude_already_contacted_within_days != null
          ? String(curAud.exclude_already_contacted_within_days)
          : "",
    });

    wrap.appendChild(
      el(
        "div",
        { style: "display:flex;gap:16px;flex-wrap:wrap;align-items:flex-end;margin-bottom:10px;" },
        [
          el("label", { style: "font-size:11px;color:var(--text-4);" }, [
            "Schedule",
            el("div", { style: "margin-top:3px;" }, [schedSel]),
          ]),
          el("label", { style: "font-size:11px;color:var(--text-4);" }, [
            "Max contacts",
            el("div", { style: "margin-top:3px;" }, [limitInp]),
          ]),
          el("label", { style: "font-size:11px;color:var(--text-4);" }, [
            "Skip if contacted (days)",
            el("div", { style: "margin-top:3px;" }, [exclInp]),
          ]),
        ],
      ),
    );

    const plays = (comp && comp.plays) || [];
    const draft = plays.find((p) => p.type === "ai_draft") || {};
    const dm = plays.find((p) => p.type === "send_dm") || {};
    const offerInp = el(
      "textarea",
      { class: "wizard-input", style: "width:100%;min-height:52px;" },
      draft.offer || "",
    );
    const voiceInp = el("input", {
      class: "wizard-input",
      style: "width:100%;",
      value: draft.voice || "",
    });
    const subjInp = el("input", {
      class: "wizard-input",
      style: "width:100%;",
      value: dm.subject || "",
    });
    const fallbackInp = el(
      "textarea",
      { class: "wizard-input", style: "width:100%;min-height:44px;" },
      draft.fallback || "",
    );
    const maxWInp = el("input", {
      class: "wizard-input",
      type: "number",
      min: "1",
      style: "width:90px;",
      value: draft.max_words != null ? String(draft.max_words) : "",
    });

    wrap.appendChild(
      el("div", { style: "display:flex;flex-direction:column;gap:8px;margin-bottom:10px;" }, [
        el("label", { style: "font-size:11px;color:var(--text-4);" }, [
          "Offer (what we pitch)",
          offerInp,
        ]),
        el("label", { style: "font-size:11px;color:var(--text-4);" }, ["Voice / tone", voiceInp]),
        el("label", { style: "font-size:11px;color:var(--text-4);" }, ["DM subject", subjInp]),
        el("label", { style: "font-size:11px;color:var(--text-4);" }, [
          "Fallback message (if AI draft fails)",
          fallbackInp,
        ]),
        el("label", { style: "font-size:11px;color:var(--text-4);" }, [
          "Max words",
          el("div", { style: "margin-top:3px;" }, [maxWInp]),
        ]),
      ]),
    );

    const saveBtn = uiButton("Save settings", { variant: "primary", size: "tiny" });
    const statusEl = el(
      "span",
      { style: "font-size:12px;color:var(--text-3);margin-left:10px;" },
      "",
    );
    saveBtn.onclick = async () => {
      const selected = CONSOLE_CHANNELS.map(([k]) => k).filter((k) => chBoxes[k].checked);
      const channelsChanged =
        selected.length !== initialChannels.size || selected.some((k) => !initialChannels.has(k));
      const patch = { schedule: { type: schedSel.value }, targeting: {}, message: {} };
      if (channelsChanged) patch.channels = selected;
      const lim = Number(limitInp.value);
      if (Number.isFinite(lim) && lim > 0) patch.targeting.limit = lim;
      if (exclInp.value !== "") {
        const ed = Number(exclInp.value);
        if (Number.isFinite(ed) && ed >= 0) patch.targeting.exclude_days = ed;
      }
      if (offerInp.value.trim()) patch.message.offer = offerInp.value.trim();
      if (voiceInp.value.trim()) patch.message.voice = voiceInp.value.trim();
      if (fallbackInp.value.trim()) patch.message.fallback = fallbackInp.value.trim();
      if (subjInp.value.trim()) patch.message.subject = subjInp.value.trim();
      const mw = Number(maxWInp.value);
      if (Number.isFinite(mw) && mw > 0) patch.message.max_words = mw;

      saveBtn.disabled = true;
      statusEl.textContent = "Saving…";
      let r = null;
      try {
        r = await api("company-update-campaign", {
          companyId,
          campaignId: numericId(c),
          body: { patch },
        });
      } catch {
        r = null;
      }
      saveBtn.disabled = false;
      if (r && r.ok) {
        statusEl.textContent = "Saved ✓";
        toast("Settings saved", "ok");
      } else {
        statusEl.textContent = "";
        toast(
          (r && r.error) === "run_in_flight"
            ? "Can't edit while a run is in progress"
            : (r && r.error) || "Couldn't save",
          "err",
        );
      }
    };
    wrap.appendChild(
      el("div", { style: "display:flex;align-items:center;margin-top:4px;" }, [saveBtn, statusEl]),
    );
    return wrap;
  };

  // Expanded row: per-campaign analytics (runs) + the inline settings editor.
  const campaignDetailPanel = (c) => {
    const mount = el("div", {
      style:
        "padding:12px 14px;border:1px solid var(--border);border-top:none;border-radius:0 0 10px 10px;background:#101017;",
    });
    mount.appendChild(el("div", { style: "color:var(--text-3);font-size:12px;" }, "Loading…"));
    (async () => {
      let comp = null,
        detail = null;
      try {
        comp = (await api("composition-get", { id: numericId(c) })).composition;
      } catch {
        comp = null;
      }
      try {
        detail = await api("campaigns-detail", { queryString: `?id=${encodeURIComponent(c.id)}` });
      } catch {
        detail = null;
      }
      mount.innerHTML = "";
      const sum = (detail && detail.summary) || {};
      mount.appendChild(
        el(
          "div",
          {
            style:
              "display:flex;gap:16px;flex-wrap:wrap;font-size:12px;color:#9aa0aa;margin-bottom:10px;",
          },
          [
            el("span", {}, `Runs: ${sum.totalRuns ?? c.runCount ?? 0}`),
            el("span", { style: "color:#5fd08a;" }, `OK: ${sum.ok ?? 0}`),
            el("span", { style: "color:#f0876a;" }, `Failed: ${sum.failed ?? 0}`),
            el("span", {}, `Last run: ${sum.lastRunAt ? String(sum.lastRunAt).slice(5, 16) : "—"}`),
          ],
        ),
      );
      const runs = (detail && detail.runs) || [];
      if (runs.length) {
        const rl = el("div", {
          style:
            "border:1px solid var(--border);border-radius:8px;background:var(--bg-2);padding:4px 10px;margin-bottom:12px;",
        });
        for (const rn of runs.slice(0, 6)) {
          const okState = rn.status === "ok" || rn.status === "worked";
          rl.appendChild(
            el(
              "div",
              {
                style:
                  "display:flex;gap:8px;font-size:11px;padding:4px 0;border-top:1px solid var(--bg-card);",
              },
              [
                el(
                  "span",
                  { style: "min-width:96px;color:var(--text-3);" },
                  rn.at ? String(rn.at).slice(5, 16) : "—",
                ),
                el(
                  "span",
                  {
                    style: `flex:1;color:${okState ? "#5fd08a" : rn.status === "failed" ? "#f0876a" : "#bcc"};`,
                  },
                  `${rn.status || ""}${rn.label ? " — " + rn.label : ""}`,
                ),
              ],
            ),
          );
        }
        mount.appendChild(rl);
      }
      mount.appendChild(buildParamsEditor(c, comp));
    })();
    return mount;
  };

  const actionBtn = (c) => {
    if (c.state === "done") return el("span", { style: "min-width:72px;" }, "");
    const isLive = c.state === "live";
    const b = el(
      "button",
      {
        class: "btn btn-tiny",
        type: "button",
        style:
          "min-width:72px;background:#1b1b24;border:1px solid var(--border-soft);border-radius:7px;padding:4px 10px;font-size:12px;color:#cdd;",
        onclick: async (e) => {
          e.stopPropagation();
          if (!isLive) {
            startWithPreview(c);
            return;
          } // Start → preview-gated
          b.disabled = true;
          b.textContent = "…";
          let r;
          try {
            r = await api("campaigns-action", { body: { id: c.id, action: "pause" } });
          } catch {
            r = null;
          }
          if (r && r.ok) {
            c.state = r.state;
            draw();
          } else {
            b.disabled = false;
            b.textContent = "Pause";
            toast((r && r.error) || "Couldn't update", "err");
          }
        },
      },
      isLive ? "Pause" : "Start",
    );
    return b;
  };

  const campaignRow = (c) => {
    const viaBadge = el(
      "span",
      {
        style: `font-size:10px;padding:1px 7px;border-radius:12px;border:1px solid var(--border-soft);color:${c.via === "targets" ? "#5fd08a" : "#8a8a9a"};`,
        title:
          c.via === "targets"
            ? "Audience explicitly targets this company"
            : "Has already contacted this company's people",
      },
      c.via === "targets" ? "targets" : "touched",
    );
    const caret = el(
      "span",
      {
        style: "font-size:12px;color:var(--text-3);min-width:14px;",
        title: "Show settings & analytics",
      },
      "▸",
    );
    const header = el(
      "div",
      {
        style:
          "display:flex;align-items:center;gap:12px;padding:11px 14px;border:1px solid var(--border);border-radius:10px;background:var(--bg-2);cursor:pointer;",
      },
      [
        caret,
        el(
          "span",
          {
            style: `font-size:11px;padding:2px 9px;border-radius:20px;background:${CONSOLE_STATE_BG[c.state] || "#222"};color:${CONSOLE_STATE_FG[c.state] || "#999"};border:1px solid ${CONSOLE_STATE_FG[c.state] || "#333"}33;text-transform:uppercase;letter-spacing:.4px;`,
          },
          c.state,
        ),
        el("div", { style: "flex:1;min-width:0;" }, [
          el(
            "div",
            {
              style:
                "color:#e8e8ef;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
            },
            c.name,
          ),
          c.bundleName
            ? el(
                "div",
                { style: "color:var(--text-4);font-size:11px;margin-top:1px;" },
                `${c.bundleName}`,
              )
            : null,
        ]),
        viaBadge,
        el(
          "span",
          { style: "font-size:12px;color:var(--text-3);min-width:64px;text-align:right;" },
          CONSOLE_CH_LABEL[c.channel] || "—",
        ),
        actionBtn(c),
      ],
    );
    const detailWrap = el("div", { style: "display:none;" });
    let built = false,
      open = false;
    header.onclick = () => {
      open = !open;
      caret.textContent = open ? "▾" : "▸";
      if (open && !built) {
        detailWrap.appendChild(campaignDetailPanel(c));
        built = true;
      }
      detailWrap.style.display = open ? "" : "none";
    };
    return el("div", { style: "margin-bottom:8px;" }, [header, detailWrap]);
  };

  const funnelView = () => {
    const f = (data && data.funnel) || {};
    const max = Math.max(1, ...FUNNEL_ORDER.map((s) => Number(f[s] || 0)));
    const wrap = el(
      "div",
      {
        style:
          "border:1px solid var(--border);border-radius:10px;background:var(--bg-2);padding:14px;",
      },
      [
        el(
          "div",
          {
            style:
              "font-size:12px;color:#9aa0aa;margin-bottom:10px;text-transform:uppercase;letter-spacing:.5px;",
          },
          "Funnel (this company)",
        ),
      ],
    );
    for (const s of FUNNEL_ORDER) {
      const n = Number(f[s] || 0);
      wrap.appendChild(
        el("div", { style: "display:flex;align-items:center;gap:10px;margin:4px 0;" }, [
          el(
            "span",
            {
              style: "min-width:78px;font-size:12px;color:var(--text-3);text-transform:capitalize;",
            },
            s,
          ),
          el(
            "div",
            { style: "flex:1;background:#0e0e14;border-radius:6px;overflow:hidden;height:16px;" },
            [
              el(
                "div",
                {
                  style: `height:16px;width:${Math.round((n / max) * 100)}%;background:#2f6f43;min-width:${n ? 2 : 0}px;`,
                },
                "",
              ),
            ],
          ),
          el(
            "span",
            { style: "min-width:34px;text-align:right;font-size:12px;color:#cdd;" },
            String(n),
          ),
        ]),
      );
    }
    return wrap;
  };

  const activityView = () => {
    const rows = (data && data.activity) || [];
    const icons = {
      x: "𝕏",
      linkedin: "🔗",
      telegram: "✈️",
      email: "✉️",
      quest: "🎯",
      system: "⚙️",
    };
    if (!rows.length)
      return el(
        "div",
        { style: "color:var(--text-3);font-size:13px;padding:8px 2px;" },
        "No recorded activity for this company yet.",
      );
    const list = el("div", {
      style:
        "border:1px solid var(--border);border-radius:10px;background:var(--bg-2);padding:6px 12px;margin-top:12px;",
    });
    for (const a of rows.slice(0, 30)) {
      list.appendChild(
        el(
          "div",
          {
            style:
              "display:flex;gap:10px;font-size:12px;padding:6px 0;border-top:1px solid var(--bg-card);",
          },
          [
            el("span", { style: "min-width:20px;" }, icons[a.channel] || "•"),
            el(
              "span",
              {
                style:
                  "flex:1;color:#cdd;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
              },
              `${a.action}${a.summary ? " — " + a.summary : ""}`,
            ),
            el(
              "span",
              { style: "min-width:104px;color:var(--text-3);text-align:right;" },
              a.occurred_at ? String(a.occurred_at).slice(5, 16) : "—",
            ),
          ],
        ),
      );
    }
    return list;
  };

  const signalsView = () => {
    const rows = (data && data.signals) || [];
    if (!rows.length)
      return el(
        "div",
        { style: "color:var(--text-3);font-size:13px;padding:8px 2px;" },
        "No buying signals captured for this company yet.",
      );
    const URG = { high: "#f0876a", medium: "#f0b34e", low: "#8a8a9a" };
    const list = el("div", {});
    for (const s of rows) {
      list.appendChild(
        el(
          "div",
          {
            style:
              "border:1px solid var(--border);border-radius:10px;background:var(--bg-2);padding:10px 14px;margin-bottom:8px;",
          },
          [
            el("div", { style: "display:flex;gap:10px;align-items:center;margin-bottom:4px;" }, [
              el(
                "span",
                { style: "font-size:11px;color:var(--text-3);text-transform:uppercase;" },
                s.platform || "—",
              ),
              el(
                "span",
                { style: `font-size:11px;color:${URG[s.urgency] || "#8a8a9a"};` },
                s.urgency || "",
              ),
              s.matched_keyword
                ? el("span", { style: "font-size:11px;color:#6a6a7a;" }, `“${s.matched_keyword}”`)
                : null,
            ]),
            el(
              "div",
              { style: "font-size:13px;color:#cdd;line-height:1.5;" },
              (s.source_text || "").slice(0, 240),
            ),
          ],
        ),
      );
    }
    return list;
  };

  const overviewView = () => {
    const co = (data && data.company) || {};
    const wrap = el("div", {});
    wrap.appendChild(
      el(
        "div",
        {
          style:
            "border:1px solid var(--border);border-radius:10px;background:var(--bg-2);padding:14px;margin-bottom:12px;",
        },
        [
          el(
            "div",
            {
              style:
                "font-size:12px;color:#9aa0aa;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px;",
            },
            "Company",
          ),
          el("div", { style: "font-size:13px;color:#cdd;line-height:1.8;" }, [
            co.sector ? el("div", {}, `Sector: ${co.sector}`) : null,
            co.stage ? el("div", {}, `Stage: ${co.stage}`) : null,
            el("div", {}, `Linked contacts: ${(data && data.contactCount) || 0}`),
            data && data.clientId ? el("div", {}, `Client: #${data.clientId}`) : null,
          ]),
        ],
      ),
    );
    // + New campaign (book launcher)
    const sel = el(
      "select",
      { class: "wizard-input", style: "flex:1;min-width:180px;" },
      CONSOLE_BOOKS.map((b) => el("option", { value: b.svc }, b.label)),
    );
    const launch = el(
      "button",
      {
        class: "btn btn-primary",
        type: "button",
        onclick: async () => {
          launch.disabled = true;
          launch.textContent = "Creating…";
          let r;
          try {
            r = await api("company-create-campaign", {
              companyId,
              body: { svc: sel.value, companyName: co.name || null },
            });
          } catch {
            r = null;
          }
          launch.disabled = false;
          launch.textContent = "+ New campaign";
          if (r && r.ok) {
            toast(
              `Created “${r.campaign.name}” (paused) — press Start on the Campaigns tab`,
              "ok",
              4000,
            );
            await load();
            activeTab = "campaigns";
            draw();
          } else {
            toast((r && r.error) || "Couldn't create campaign", "err");
          }
        },
      },
      "+ New campaign",
    );
    wrap.appendChild(
      el(
        "div",
        {
          style:
            "border:1px solid var(--border);border-radius:10px;background:var(--bg-2);padding:14px;",
        },
        [
          el(
            "div",
            {
              style:
                "font-size:12px;color:#9aa0aa;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px;",
            },
            "Launch a campaign for this company",
          ),
          el(
            "div",
            { style: "font-size:12px;color:var(--text-4);margin-bottom:10px;" },
            "Creates a PAUSED book campaign scoped to this company. Nothing sends until you press Start.",
          ),
          el("div", { style: "display:flex;gap:8px;flex-wrap:wrap;align-items:center;" }, [
            sel,
            launch,
          ]),
          el(
            "div",
            { style: "margin-top:10px;padding-top:10px;border-top:1px solid var(--border);" },
            [
              el(
                "button",
                {
                  class: "btn btn-secondary btn-tiny",
                  type: "button",
                  onclick: () => navigate("plays"),
                },
                "+ New Conversation Play (seeded social proof) →",
              ),
            ],
          ),
        ],
      ),
    );

    // Publish for community amplification (the operator-driven publish bridge):
    // turn this company into a subscribable campaign so the community can amplify it.
    const amp = (data && data.amplification) || {
      published: false,
      handle: "",
      subscriberCount: 0,
    };
    const ampStatus = el(
      "div",
      { style: "font-size:12px;color:var(--text-4);margin-bottom:10px;line-height:1.5;" },
      amp.published
        ? `Published — ${amp.subscriberCount || 0} subscriber${(amp.subscriberCount || 0) === 1 ? "" : "s"} amplifying @${amp.handle}. Community members can subscribe their own accounts to like + repost this company's posts.`
        : amp.handle
          ? `Publish @${amp.handle} so your community can subscribe their own X accounts and amplify (like + repost) this company's posts — turning the community into the project's promoters. Nothing runs on your accounts.`
          : "This company has no X handle on file — add one to publish it for community amplification.",
    );
    const ampBtn = el(
      "button",
      {
        class: `btn ${amp.published ? "btn-secondary" : "btn-primary"} btn-tiny`,
        type: "button",
      },
      amp.published ? "Unpublish" : "Publish for community amplification",
    );
    ampBtn.disabled = !amp.handle && !amp.published;
    // Optional XP reward (funds it → reach_mode 'funded'). Only shown pre-publish.
    const xpInput = el("input", {
      type: "number",
      min: "0",
      placeholder: "0",
      class: "wizard-input",
      style: "width:64px;",
    });
    const rewardRow =
      !amp.published && amp.handle
        ? el(
            "div",
            {
              style:
                "display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-4);margin-bottom:10px;flex-wrap:wrap;",
            },
            [
              "Optional reward:",
              xpInput,
              "XP per verified action",
              el("span", { style: "color:#6a6a7a;" }, "· paid via the Circuit once connected"),
            ],
          )
        : null;
    ampBtn.onclick = async () => {
      ampBtn.disabled = true;
      ampBtn.textContent = amp.published ? "Unpublishing…" : "Publishing…";
      let body;
      if (amp.published) body = { unpublish: true };
      else {
        body = {};
        const xp = parseInt(xpInput.value, 10);
        if (Number.isFinite(xp) && xp > 0) body.reward = { type: "xp", xpPerAction: xp };
      }
      let r;
      try {
        r = await api("company-publish", { companyId, body });
      } catch {
        r = null;
      }
      if (r && r.ok) {
        toast(
          r.reachMode === "funded"
            ? "Published + funded (rewards go live once the Circuit is connected)"
            : r.reachMode === "published"
              ? "Published for community amplification"
              : "Unpublished",
          "ok",
          4000,
        );
        await load();
        draw();
      } else {
        ampBtn.disabled = false;
        ampBtn.textContent = amp.published ? "Unpublish" : "Publish for community amplification";
        toast(
          (r && r.error) === "no_x_handle"
            ? "Add an X handle to this company first"
            : (r && r.error) === "circuit_mint_failed"
              ? "Couldn't mint the Circuit reward — try again"
              : (r && r.error) || "Couldn't publish",
          "err",
        );
      }
    };
    wrap.appendChild(
      el(
        "div",
        {
          style:
            "border:1px solid var(--border);border-radius:10px;background:var(--bg-2);padding:14px;margin-top:12px;",
        },
        [
          el(
            "div",
            {
              style:
                "font-size:12px;color:#9aa0aa;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px;",
            },
            "Community amplification",
          ),
          ampStatus,
          rewardRow,
          ampBtn,
        ],
      ),
    );
    return wrap;
  };

  const campaignsView = () => {
    const camps = (data && data.campaigns) || [];
    if (!camps.length) {
      return el(
        "div",
        {
          style:
            "color:var(--text-3);font-size:13px;padding:20px;text-align:center;border:1px dashed var(--border);border-radius:10px;",
        },
        "No campaigns touch this company yet — launch one from the Overview tab.",
      );
    }
    const wrap = el("div", {});
    for (const c of camps) wrap.appendChild(campaignRow(c));
    return wrap;
  };

  const tabMount = el("div", {});
  const drawTab = () => {
    tabMount.innerHTML = "";
    if (activeTab === "overview") tabMount.appendChild(overviewView());
    else if (activeTab === "analytics") {
      tabMount.appendChild(funnelView());
      tabMount.appendChild(activityView());
    } else if (activeTab === "signals") tabMount.appendChild(signalsView());
    else tabMount.appendChild(campaignsView());
  };

  const draw = () => {
    root.innerHTML = "";
    if (!data) {
      root.appendChild(
        el(
          "div",
          { style: "color:var(--red);font-size:13px;" },
          "Couldn't load this company's console — try again.",
        ),
      );
      return;
    }
    const co = data.company || {};
    root.appendChild(
      el(
        "div",
        {
          style:
            "display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:6px;flex-wrap:wrap;",
        },
        [
          el("div", {}, [
            el(
              "h1",
              { style: "font-size:22px;color:var(--text-1);margin:0 0 2px;font-weight:700;" },
              co.name || `Company #${companyId}`,
            ),
            el(
              "p",
              { style: "font-size:13px;color:#9aa0aa;margin:0;" },
              `${(data.campaigns || []).length} campaigns · ${data.contactCount || 0} contacts`,
            ),
          ]),
          el(
            "button",
            { class: "btn btn-secondary", type: "button", onclick: () => navigate("crm") },
            "« CRM",
          ),
        ],
      ),
    );
    const tabs = [
      ["campaigns", "Campaigns"],
      ["analytics", "Analytics"],
      ["signals", "Signals"],
      ["overview", "Overview"],
    ];
    const tabBar = el("div", { style: "display:flex;gap:6px;margin:10px 0 14px;flex-wrap:wrap;" });
    for (const [k, label] of tabs) {
      tabBar.appendChild(
        el(
          "button",
          {
            class: "btn btn-tiny",
            type: "button",
            style: `background:${activeTab === k ? "#2f6f43" : "#1b1b24"};color:${activeTab === k ? "#fff" : "#9aa0aa"};border:1px solid var(--border-soft);border-radius:16px;padding:5px 14px;font-size:12px;`,
            onclick: () => {
              activeTab = k;
              drawTab();
              redrawTabBar();
            },
          },
          label,
        ),
      );
    }
    const redrawTabBar = () => {
      const btns = tabBar.querySelectorAll("button");
      tabs.forEach(([k], i) => {
        const on = activeTab === k;
        btns[i].style.background = on ? "#2f6f43" : "#1b1b24";
        btns[i].style.color = on ? "#fff" : "#9aa0aa";
      });
    };
    root.append(tabBar, tabMount);
    drawTab();
  };

  root.appendChild(el("div", { style: "color:var(--text-3);font-size:13px;" }, "Loading console…"));
  await load();
  draw();
}

// ── Conversation Plays (seeded-dialogue builder) ─────────────────
// The "planted question → another account answers" social-proof capability,
// on a risk dial: authentic (safe, default-on) → soft_proof → choreographed
// (default-OFF, high ban risk). Guardrails run server-side (pair co-occurrence
// cap, persona diversity, DM consent, disclosure, BanStop). This UI never sends.
const PLAY_TIERS = [
  { key: "authentic", label: "Authentic — reply to REAL asks (safe)", risk: "🟢" },
  { key: "soft_proof", label: "Soft proof — seed + 1 recommender", risk: "🟡" },
  { key: "choreographed", label: "Choreographed — complaint → pile-on → pitch", risk: "🔴" },
];
const PLAY_DISCLOSURE = ["soft", "explicit", "none"];

async function renderConversationPlays() {
  const root = el("div", { style: "max-width:900px;margin:0 auto;padding:20px 16px;" });
  setContentNode(root);
  root.appendChild(
    el("div", { style: "margin-bottom:12px;" }, [
      el(
        "h1",
        { style: "font-size:22px;color:var(--text-1);margin:0 0 2px;font-weight:700;" },
        "Conversation Plays",
      ),
      el(
        "p",
        { style: "font-size:13px;color:#9aa0aa;margin:0;" },
        "Choreographed social proof across your accounts. Nothing sends until you activate a play.",
      ),
    ]),
  );
  // Honest risk banner.
  root.appendChild(
    el(
      "div",
      {
        style:
          "border:1px solid #4a3a10;background:#241d10;border-radius:10px;padding:10px 14px;margin-bottom:14px;color:#f0b34e;font-size:12px;line-height:1.6;",
      },
      "Tiers above 'Authentic' stage a fake asker. That is coordinated inauthentic behavior — it violates platform policy and endorsement-disclosure norms, and risks account bans. Guardrails (pair co-occurrence cap, persona diversity, disclosure, kill switch) reduce but do not remove the risk. Default is Authentic; the risky tiers are opt-in and start paused.",
    ),
  );

  const listMount = el("div", {});
  const formMount = el("div", {});
  root.append(
    formMount,
    el("h3", { style: "color:#cdd;font-size:14px;margin:18px 2px 8px;" }, "Your plays"),
    listMount,
  );

  const reload = async () => {
    listMount.innerHTML = "";
    listMount.appendChild(el("div", { style: "color:var(--text-3);font-size:13px;" }, "Loading…"));
    let plays = [];
    try {
      plays = (await api("conversation-plays")).plays || [];
    } catch {
      plays = [];
    }
    listMount.innerHTML = "";
    if (!plays.length) {
      listMount.appendChild(
        el(
          "div",
          { style: "color:var(--text-3);font-size:13px;padding:12px;" },
          "No plays yet — create one above.",
        ),
      );
      return;
    }
    for (const p of plays) listMount.appendChild(playRow(p, reload));
  };

  formMount.appendChild(buildPlayForm(reload));
  await reload();
}

function buildPlayForm(reload) {
  const inp = (ph, w = "100%") =>
    el("input", { class: "wizard-input", placeholder: ph, style: `width:${w};` });
  const name = inp("Play name (e.g. MM in launch groups)");
  const tier = el(
    "select",
    { class: "wizard-input", style: "width:100%;" },
    PLAY_TIERS.map((t) => el("option", { value: t.key }, `${t.risk} ${t.label}`)),
  );
  const topic = inp("Topic — what the conversation is about");
  const offerName = inp("Offer name (OnchainBureau)");
  const offer = inp("Offer one-liner (non-custodial market making, tight spreads)");
  const cta = inp("Soft CTA (quick call before your TGE)");
  const platform = el("select", { class: "wizard-input", style: "width:100%;" }, [
    el("option", { value: "telegram" }, "Telegram"),
    el("option", { value: "x" }, "X / Twitter"),
  ]);
  const groups = inp("Target groups/threads (comma-separated ids)");
  const keywords = inp("Trigger keywords (comma-separated) — REQUIRED for the authentic tier");
  const complainers = inp("Asker account ids (comma-separated)");
  const recommenders = inp("Recommender account ids (comma-separated)");
  const disclosure = el(
    "select",
    { class: "wizard-input", style: "width:100%;" },
    PLAY_DISCLOSURE.map((d) => el("option", { value: d }, `disclosure: ${d}`)),
  );

  const row = (label, node) =>
    el("div", { style: "margin-bottom:8px;" }, [
      el("label", { class: "wizard-label", style: "font-size:12px;color:#9aa0aa;" }, label),
      node,
    ]);

  const create = el(
    "button",
    {
      class: "btn btn-primary",
      type: "button",
      onclick: async () => {
        if (!name.value.trim()) {
          toast("Name required", "err");
          return;
        }
        const kwList = keywords.value
          .split(",")
          .map((s) => s.trim())
          .filter(Boolean);
        // The authentic tier only ever replies when an incoming message matches a
        // keyword — with none set it can never fire. Block that footgun up front.
        if (tier.value === "authentic" && kwList.length === 0) {
          toast("Authentic plays need at least one trigger keyword", "err");
          return;
        }
        const body = {
          name: name.value.trim(),
          tier: tier.value,
          topic: topic.value.trim(),
          offer: {
            name: offerName.value.trim() || "OnchainBureau",
            offer: offer.value.trim(),
            cta: cta.value.trim(),
          },
          target: {
            platform: platform.value,
            groups: groups.value
              .split(",")
              .map((s) => s.trim())
              .filter(Boolean),
            keywords: kwList,
          },
          roles: {
            complainer_accounts: complainers.value
              .split(",")
              .map((s) => s.trim())
              .filter(Boolean),
            recommender_accounts: recommenders.value
              .split(",")
              .map((s) => s.trim())
              .filter(Boolean),
          },
          disclosureMode: disclosure.value,
        };
        create.disabled = true;
        create.textContent = "Creating…";
        let r;
        try {
          r = await api("conversation-play-create", { body });
        } catch {
          r = null;
        }
        create.disabled = false;
        create.textContent = "Create play (draft)";
        if (r && r.ok) {
          toast("Play created as draft", "ok");
          name.value = "";
          await reload();
        } else toast((r && r.error) || "Couldn't create", "err");
      },
    },
    "Create play (draft)",
  );

  return el(
    "div",
    {
      style:
        "border:1px solid var(--border);border-radius:10px;background:var(--bg-2);padding:14px;",
    },
    [
      el(
        "div",
        {
          style:
            "font-size:12px;color:#9aa0aa;margin-bottom:10px;text-transform:uppercase;letter-spacing:.5px;",
        },
        "New Conversation Play",
      ),
      row("Name", name),
      row("Tier (risk)", tier),
      row("Topic", topic),
      el("div", { style: "display:flex;gap:8px;" }, [
        el("div", { style: "flex:1;" }, [row("Offer name", offerName)]),
        el("div", { style: "flex:2;" }, [row("Offer", offer)]),
      ]),
      row("CTA", cta),
      el("div", { style: "display:flex;gap:8px;" }, [
        el("div", { style: "flex:1;" }, [row("Platform", platform)]),
        el("div", { style: "flex:2;" }, [row("Targets", groups)]),
      ]),
      row("Trigger keywords", keywords),
      el("div", { style: "display:flex;gap:8px;" }, [
        el("div", { style: "flex:1;" }, [row("Asker accounts", complainers)]),
        el("div", { style: "flex:1;" }, [row("Recommender accounts", recommenders)]),
      ]),
      row("Disclosure", disclosure),
      el("div", { style: "margin-top:8px;" }, [create]),
    ],
  );
}

function playRow(p, reload) {
  const TIER_RISK = { authentic: "🟢", soft_proof: "🟡", choreographed: "🔴" };
  const STATE_FG = { active: "#5fd08a", paused: "#f0b34e", draft: "#9aa0aa", done: "#777" };
  const btn = (label, fn) =>
    el(
      "button",
      {
        class: "btn btn-tiny",
        type: "button",
        style:
          "background:#1b1b24;border:1px solid var(--border-soft);border-radius:7px;padding:4px 10px;font-size:12px;color:#cdd;",
        onclick: fn,
      },
      label,
    );

  const preview = async () => {
    let r;
    try {
      r = await api("conversation-play-preview", { id: p.id, body: {} });
    } catch {
      r = null;
    }
    const body = el("div", {});
    if (!r || !r.ok) body.appendChild(el("div", { style: "color:var(--red);" }, "Preview failed."));
    else {
      body.appendChild(
        el(
          "div",
          { class: "muted small", style: "margin-bottom:8px;" },
          `tier ${r.tier} · disclosure ${r.disclosure} · ${r.dryRun ? "dry-run placeholders" : "live drafts"}`,
        ),
      );
      for (const [k, v] of Object.entries(r.messages || {})) {
        body.appendChild(
          el("div", { style: "margin-bottom:8px;" }, [
            el("div", { style: "font-size:11px;color:var(--text-3);text-transform:uppercase;" }, k),
            el("div", { style: "color:#cdd;font-size:13px;line-height:1.5;" }, v),
          ]),
        );
      }
    }
    openDrawer(`Preview — ${p.name}`, body);
  };
  const toggle = async () => {
    const next = p.status === "active" ? "paused" : "active";
    let r;
    try {
      r = await api("conversation-play-status", { id: p.id, body: { status: next } });
    } catch {
      r = null;
    }
    if (r && r.ok) {
      toast(next === "active" ? "Play activated" : "Play paused", "ok");
      reload();
    } else toast((r && r.error) || "Couldn't update", "err");
  };
  const del = async () => {
    let r;
    try {
      r = await api("conversation-play-delete", { id: p.id });
    } catch {
      r = null;
    }
    if (r && r.ok) {
      toast("Play deleted", "ok");
      reload();
    } else toast("Couldn't delete", "err");
  };

  return el(
    "div",
    {
      style:
        "display:flex;align-items:center;gap:12px;padding:11px 14px;border:1px solid var(--border);border-radius:10px;background:var(--bg-2);margin-bottom:8px;",
    },
    [
      el("span", { title: p.tier }, TIER_RISK[p.tier] || "•"),
      el("div", { style: "flex:1;min-width:0;" }, [
        el(
          "div",
          {
            style:
              "color:#e8e8ef;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
          },
          p.name,
        ),
        el(
          "div",
          { style: "color:var(--text-4);font-size:11px;" },
          `${p.tier} · ${(p.target && p.target.platform) || "—"}`,
        ),
      ]),
      el(
        "span",
        { style: `font-size:11px;color:${STATE_FG[p.status] || "#999"};text-transform:uppercase;` },
        p.status,
      ),
      btn("Preview", preview),
      btn(p.status === "active" ? "Pause" : "Activate", toggle),
      btn("🗑", del),
    ],
  );
}
