// Data Collection — Hi-Fi (Sluicebox Design System)
// Same scenario + content as the wireframe, repainted with brand tokens.

const { useState, useEffect, useMemo, useRef, useCallback, Fragment } = React;

const cx = (...xs) => xs.filter(Boolean).join(" ");

// Brand icon map (relative to project root)
const ICON = (name) => `assets/sluicebox/icons/${name}.png`;

// Frontend↔backend compat. Must match backend's `APP_VERSION`
// (backend/app/version.py). Bump in the same commit as any
// frontend↔backend incompatibility (new route, payload shape change,
// renamed endpoint, new SSE event the frontend depends on). The boot
// check below blocks the app from mounting until /version matches.
const EXPECTED_VERSION = "app-50";

// Where the API lives when ?api= isn't given. Served by the backend itself
// (production single-container mode) → same origin. Served by `make web` on
// :5500 (or opened from file://) → the local backend on :8771 (not :8001 —
// another Sluicebox worktree commonly squats that port in dev).
const _DEFAULT_API_ORIGIN =
  window.location.port === "5500" || window.location.origin === "null"
    ? "http://127.0.0.1:8771"
    : window.location.origin;
// Backend-served implies live — no ?live=1 needed in production. ?live=0
// still forces mock data anywhere.
const _DEFAULT_LIVE = _DEFAULT_API_ORIGIN === window.location.origin;

// =========================================================================
// HTTP Basic auth — single shared credential, gated by the backend.
// =========================================================================
// Credentials live in sessionStorage as base64("user:pass"). Rather than
// thread an Authorization header through the ~100 fetch() call sites, we
// wrap window.fetch once: any request to the API origin gets the header
// attached automatically. The AuthGate component (bottom of file) captures
// the credentials and renders a login screen on 401.
const AUTH_STORAGE_KEY = "tariff_basic_auth";
const _API_ORIGIN = new URLSearchParams(window.location.search).get("api") || _DEFAULT_API_ORIGIN;

(function installAuthFetch() {
  const _origFetch = window.fetch.bind(window);
  window.fetch = (input, init) => {
    const url = typeof input === "string" ? input : (input && input.url) || "";
    const creds = sessionStorage.getItem(AUTH_STORAGE_KEY);
    if (creds && url.startsWith(_API_ORIGIN)) {
      const headers = new Headers((init && init.headers) || {});
      if (!headers.has("Authorization")) headers.set("Authorization", `Basic ${creds}`);
      init = { ...init, headers };
    }
    return _origFetch(input, init);
  };
})();

// Drop the stored credentials and reload — the AuthGate re-mounts and shows
// the login screen since /auth/check now 401s.
function authLogout() {
  sessionStorage.removeItem(AUTH_STORAGE_KEY);
  window.location.reload();
}

// The signed-in email (Basic creds are base64 "email:password"), for display.
function authCurrentEmail() {
  const creds = sessionStorage.getItem(AUTH_STORAGE_KEY);
  if (!creds) return null;
  try {
    return atob(creds).split(":")[0] || null;
  } catch {
    return null;
  }
}

// Trigger a file download from an authenticated endpoint. Browser-native
// navigation (window.location.href = url, <a href download>, etc.)
// does NOT carry the Basic-auth header our fetch-wrapper attaches —
// because the wrapper only intercepts `window.fetch` calls, not page
// navigations. The result is the browser popping its own Basic-auth
// dialog. Workaround: fetch the file ourselves as a blob (goes through
// the wrapper, auth attached), then synthesize a click on a temp
// anchor to save it. The filename comes from the response's
// Content-Disposition header when present, else from a fallback.
async function _authedDownload(url, fallbackLabel) {
  try {
    const r = await fetch(url);
    if (!r.ok) {
      alert(`Download failed: ${r.status} ${r.statusText}`);
      return;
    }
    // Parse `filename="…"` out of Content-Disposition if the server set
    // it. Falls back to the URL's final path segment.
    const cd = r.headers.get("Content-Disposition") || "";
    const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(cd);
    const filenameFromCd = m ? decodeURIComponent(m[1]) : null;
    const filenameFromUrl = (() => {
      try {
        const p = new URL(url, window.location.href).pathname;
        const tail = p.split("/").filter(Boolean).pop() || "";
        return tail.includes(".") ? tail : null;
      } catch { return null; }
    })();
    const filename = filenameFromCd || filenameFromUrl || (fallbackLabel || "download");
    const blob = await r.blob();
    const objUrl = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = objUrl;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    // Defer revoke so the browser has actually kicked off the save.
    setTimeout(() => URL.revokeObjectURL(objUrl), 1000);
  } catch (e) {
    alert(`Download failed: ${e?.message || e}`);
  }
}

// =========================================================================
// Live-backend hook — opt in via ?live=1&campaign=<id>[&api=<url>]
// =========================================================================
// Reflect campaign / recipient state into the URL so a hard-reload doesn't
// drop you back onto an old campaign. Uses replaceState — no history clutter.
function _setUrlParam(key, value) {
  const url = new URL(window.location.href);
  if (value) url.searchParams.set(key, value);
  else url.searchParams.delete(key);
  window.history.replaceState({}, "", url.toString());
}

function useLiveBackend() {
  const params = new URLSearchParams(window.location.search);
  const liveMode = params.get("live") ? params.get("live") === "1" : _DEFAULT_LIVE;
  const apiBase = params.get("api") || _DEFAULT_API_ORIGIN;
  const campaignId = params.get("campaign") || null;
  const recipientFromUrl = params.get("recipient") || null;

  const [dashboard, setDashboard] = useState(null);
  const [selectedRecipientId, _setSelectedRecipientId] = useState(recipientFromUrl);
  const setSelectedRecipientId = useCallback((id) => {
    _setSelectedRecipientId(id);
    _setUrlParam("recipient", id);
  }, []);
  const [threadView, setThreadView] = useState(null);
  const [error, setError] = useState(null);
  const [templates, setTemplates] = useState(null);
  const [wizardCampaignId, _setWizardCampaignId] = useState(campaignId);
  const setWizardCampaignId = useCallback((id) => {
    _setWizardCampaignId(id);
    _setUrlParam("campaign", id);
    // Switching campaigns invalidates any selected recipient
    if (id !== campaignId) {
      _setSelectedRecipientId(null);
      _setUrlParam("recipient", null);
    }
  }, [campaignId]);
  const [wizardCampaignName, setWizardCampaignName] = useState(null);

  const activeCampaignId = wizardCampaignId || campaignId;

  const refreshDashboard = useCallback(async () => {
    if (!liveMode || !activeCampaignId) return;
    try {
      const r = await fetch(`${apiBase}/campaigns/${activeCampaignId}/dashboard`);
      if (!r.ok) throw new Error(`dashboard ${r.status}`);
      setDashboard(await r.json());
      setError(null);
    } catch (e) {
      setError(String(e));
    }
  }, [apiBase, activeCampaignId, liveMode]);

  // Drop cached dashboard / thread when the active campaign changes, so the
  // UI doesn't briefly show stale data from a previous campaign.
  useEffect(() => {
    setDashboard(null);
    setThreadView(null);
  }, [activeCampaignId]);

  const refreshThread = useCallback(async (recId) => {
    if (!liveMode || !recId) return;
    try {
      const r = await fetch(`${apiBase}/recipients/${recId}/thread-view`);
      if (!r.ok) throw new Error(`thread ${r.status}`);
      setThreadView(await r.json());
      setError(null);
    } catch (e) {
      setError(String(e));
    }
  }, [apiBase, liveMode]);

  const fetchTemplates = useCallback(async () => {
    if (!liveMode) return;
    try {
      const r = await fetch(`${apiBase}/templates`);
      if (!r.ok) throw new Error(`templates ${r.status}`);
      setTemplates(await r.json());
    } catch (e) {
      setError(String(e));
    }
  }, [apiBase, liveMode]);

  const createFromTemplate = useCallback(async (template_id, name) => {
    const r = await fetch(`${apiBase}/campaigns/from-template`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ template_id, name }),
    });
    if (!r.ok) throw new Error(`create ${r.status}`);
    const data = await r.json();
    setWizardCampaignId(data.id);
    setWizardCampaignName(data.name);
    return data;
  }, [apiBase]);

  const uploadRecipientsCsv = useCallback(async (file) => {
    if (!wizardCampaignId) throw new Error("no campaign yet — pick a template first");
    const fd = new FormData();
    fd.append("file", file);
    const r = await fetch(`${apiBase}/recipients/csv?campaign_id=${wizardCampaignId}`, {
      method: "POST",
      body: fd,
    });
    if (!r.ok) throw new Error(`csv ${r.status}: ${await r.text()}`);
    return await r.json();
  }, [apiBase, wizardCampaignId]);

  const fetchCampaign = useCallback(async (id) => {
    const r = await fetch(`${apiBase}/campaigns/${id}`);
    if (!r.ok) throw new Error(`campaign ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const patchCampaign = useCallback(async (id, payload) => {
    const r = await fetch(`${apiBase}/campaigns/${id}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    if (!r.ok) throw new Error(`patch ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const fetchFields = useCallback(async (id) => {
    const r = await fetch(`${apiBase}/campaigns/${id}/fields`);
    if (!r.ok) throw new Error(`fields ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const addField = useCallback(async (id, body) => {
    const r = await fetch(`${apiBase}/campaigns/${id}/fields`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error(`add ${r.status}: ${await r.text()}`);
    return await r.json();
  }, [apiBase]);

  const patchField = useCallback(async (cid, fid, body) => {
    const r = await fetch(`${apiBase}/campaigns/${cid}/fields/${fid}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error(`patch field ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const deleteField = useCallback(async (cid, fid) => {
    const r = await fetch(`${apiBase}/campaigns/${cid}/fields/${fid}`, { method: "DELETE" });
    if (!r.ok) throw new Error(`delete ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const addRecipient = useCallback(async (campaignId, body) => {
    const r = await fetch(`${apiBase}/recipients`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ campaign_id: campaignId, ...body }),
    });
    if (!r.ok) throw new Error(`add recipient ${r.status}: ${await r.text()}`);
    return await r.json();
  }, [apiBase]);

  const deleteRecipient = useCallback(async (rid) => {
    const r = await fetch(`${apiBase}/recipients/${rid}`, { method: "DELETE" });
    if (!r.ok) throw new Error(`delete recipient ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const setEmailOverride = useCallback(async (id, body) => {
    const r = await fetch(`${apiBase}/campaigns/${id}/email-override`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error(`set override ${r.status}: ${await r.text()}`);
    return await r.json();
  }, [apiBase]);

  const clearEmailOverride = useCallback(async (id) => {
    const r = await fetch(`${apiBase}/campaigns/${id}/email-override`, { method: "DELETE" });
    if (!r.ok) throw new Error(`clear override ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const fetchContacts = useCallback(async (q = "") => {
    const url = q ? `${apiBase}/recipients/contacts?q=${encodeURIComponent(q)}` : `${apiBase}/recipients/contacts`;
    const r = await fetch(url);
    if (!r.ok) throw new Error(`contacts ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const previewEmail = useCallback(async (id) => {
    const r = await fetch(`${apiBase}/campaigns/${id}/preview-email`);
    if (!r.ok) throw new Error(`preview ${r.status}`);
    return await r.json();
  }, [apiBase]);

  // Multi-recipient preview — one rendered first-touch per recipient,
  // each scoped to that supplier's auto-assigned parts. Triggers the
  // LLM-driven parts-to-recipient assignment lazily on first call.
  const previewEmails = useCallback(async (id) => {
    const r = await fetch(`${apiBase}/campaigns/${id}/preview-emails`);
    if (!r.ok) throw new Error(`preview-emails ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const launchCampaign = useCallback(async (id) => {
    const r = await fetch(`${apiBase}/campaigns/${id}/launch`, { method: "POST" });
    if (!r.ok) throw new Error(`launch ${r.status}: ${await r.text()}`);
    return await r.json();
  }, [apiBase]);

  const seedFromDocument = useCallback(async (id, file) => {
    const fd = new FormData();
    fd.append("file", file);
    const r = await fetch(`${apiBase}/campaigns/${id}/seed-from-document`, {
      method: "POST",
      body: fd,
    });
    if (!r.ok) throw new Error(`seed ${r.status}: ${await r.text()}`);
    return await r.json();
  }, [apiBase]);

  const fetchAudit = useCallback(async (id) => {
    const r = await fetch(`${apiBase}/campaigns/${id}/audit`);
    if (!r.ok) throw new Error(`audit ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const fetchTemplateMapping = useCallback(async (id) => {
    const r = await fetch(`${apiBase}/campaigns/${id}/customer-template/mapping`);
    if (!r.ok) throw new Error(`mapping ${r.status}`);
    return await r.json();
  }, [apiBase]);

  const uploadCustomerTemplate = useCallback(async (id, file) => {
    const fd = new FormData();
    fd.append("file", file);
    const r = await fetch(`${apiBase}/campaigns/${id}/customer-template`, {
      method: "POST",
      body: fd,
    });
    if (!r.ok) throw new Error(`upload ${r.status}: ${await r.text()}`);
    return await r.json();
  }, [apiBase]);

  useEffect(() => { refreshDashboard(); }, [refreshDashboard]);
  useEffect(() => { fetchTemplates(); }, [fetchTemplates]);
  useEffect(() => {
    if (selectedRecipientId) refreshThread(selectedRecipientId);
  }, [selectedRecipientId, refreshThread]);

  return {
    liveMode, apiBase, campaignId,
    dashboard, refreshDashboard,
    selectedRecipientId, setSelectedRecipientId,
    threadView, refreshThread,
    templates, fetchTemplates,
    wizardCampaignId, setWizardCampaignId, wizardCampaignName,
    createFromTemplate, uploadRecipientsCsv,
    fetchCampaign, patchCampaign,
    fetchFields, addField, patchField, deleteField,
    addRecipient, deleteRecipient, fetchContacts,
    previewEmail, previewEmails, setEmailOverride, clearEmailOverride,
    launchCampaign, seedFromDocument,
    fetchAudit, fetchTemplateMapping, uploadCustomerTemplate,
    activeCampaignId,
    error,
  };
}

// =========================================================================
// Sidebar
// =========================================================================
function Sidebar({ screen, setScreen, collapsed, setCollapsed, inboxCount, branding, apiBase, onBrandingChanged }) {
  // Brand editor popover. Click the account chip to toggle. Closes on
  // outside-click + ESC. Form values seed from current `branding` each
  // time the editor opens so re-opening after Save shows fresh values.
  //
  // Positioning: the popover uses `position: fixed` because the
  // sidebar's `overflow-y: auto` clips anything that extends above the
  // chip's row. Coordinates are derived from the chip's bounding rect
  // each time it opens.
  const [brandEdit, setBrandEdit] = useState(false);
  const [brandAnchor, setBrandAnchor] = useState(null); // { bottom, left, width } in viewport coords
  const [bForm, setBForm] = useState({ name: "", name_long: "", color: "#CC0000", initials_override: "" });
  const [bSaving, setBSaving] = useState(false);
  const [bErr, setBErr] = useState("");
  const brandChipRef = useRef(null);
  const brandPopRef = useRef(null);
  useEffect(() => {
    if (!brandEdit) return;
    function onDoc(e) {
      const inPop = brandPopRef.current && brandPopRef.current.contains(e.target);
      const inChip = brandChipRef.current && brandChipRef.current.contains(e.target);
      if (!inPop && !inChip) setBrandEdit(false);
    }
    function onKey(e) { if (e.key === "Escape") setBrandEdit(false); }
    document.addEventListener("mousedown", onDoc);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDoc);
      document.removeEventListener("keydown", onKey);
    };
  }, [brandEdit]);
  function openBrandEditor(e) {
    e.preventDefault();
    e.stopPropagation();
    setBForm({
      name: branding?.name || "",
      name_long: branding?.name_long || "",
      color: branding?.color || "#CC0000",
      initials_override: "",  // never seed with the derived initials — leave blank so user only types when they want to override
    });
    setBErr("");
    // Measure chip position so the popover (position: fixed) lands
    // right above it, regardless of the sidebar's overflow clipping.
    if (brandChipRef.current) {
      const r = brandChipRef.current.getBoundingClientRect();
      setBrandAnchor({ top: r.top, left: r.left, width: r.width });
    }
    setBrandEdit(true);
  }
  async function saveBranding() {
    if (bSaving) return;
    const name = (bForm.name || "").trim();
    const nameLong = (bForm.name_long || "").trim();
    const color = (bForm.color || "").trim();
    if (!name || !nameLong) { setBErr("Name and long name are required."); return; }
    setBSaving(true); setBErr("");
    try {
      const r = await fetch(`${apiBase}/branding`, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name, name_long: nameLong, color,
          initials_override: (bForm.initials_override || "").trim(),
        }),
      });
      if (!r.ok) {
        const t = await r.text();
        setBErr(t || `Save failed (${r.status})`);
        return;
      }
      if (onBrandingChanged) await onBrandingChanged();
      setBrandEdit(false);
    } catch (e) {
      setBErr(String(e?.message || e));
    } finally {
      setBSaving(false);
    }
  }
  // One workspace entry holds Collections / Inbox / Sent as tabs. The
  // entry stays "active" for any of those three screen ids — they all
  // resolve to the same workspace UI under the hood.
  const _isWorkspace = (s) => s === "list" || s === "inbox" || s === "sent" || s === "contacts";
  const setupItems = [
    ["list", inboxCount ? `Data collections (${inboxCount})` : "Data collections", null],
  ];
  const runtimeItems = [];
  const refItems = [];

  const idx = setupItems.findIndex(([id]) => id === screen);

  return (
    <aside className={cx("side", collapsed && "collapsed")}>
      <button
        className="side-toggle"
        onClick={() => setCollapsed(!collapsed)}
        title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
        aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
      >
        {collapsed ? "›" : "‹"}
      </button>
      <div className="brand">
        <img className="brand-logo" src="assets/sluicebox/SB_Logo_white.png" alt="Sluicebox" />
      </div>
      <div style={{ marginTop: -8 }}>
        <div className="brand-sub">Component Carbon Intelligence™</div>
      </div>

      <div className="nav-group">
        <div className="nav-label">Workspace</div>
        {setupItems.map(([id, name, num], i) => {
          // The Data collections entry is "active" for any of the three
          // workspace tabs (list / inbox / sent). For nested screens
          // (collection / recipient) it also stays highlighted as the
          // root they live under.
          const isActive = id === "list"
            ? (_isWorkspace(screen) || screen === "collection" || screen === "recipient")
            : screen === id;
          const done = num != null && idx > i && idx >= 0;
          return (
            <button
              key={id}
              className={cx("nav-item", isActive && "active", done && "done")}
              onClick={() => setScreen(id)}
            >
              <span className="nav-num">{num != null ? (done ? "✓" : num) : "✦"}</span>
              <span>{name}</span>
            </button>
          );
        })}
      </div>

      <div className="nav-group">
        <div className="nav-label">Operate</div>
        {runtimeItems.map(([id, name]) => (
          <button
            key={id}
            className={cx("nav-item", screen === id && "active")}
            onClick={() => setScreen(id)}
          >
            <span className="nav-num">·</span>
            <span>{name}</span>
          </button>
        ))}
      </div>

      {refItems.length > 0 && (
        <div className="nav-group">
          <div className="nav-label">Reference</div>
          {refItems.map(([id, name]) => (
            <button
              key={id}
              className={cx("nav-item", screen === id && "active")}
              onClick={() => setScreen(id)}
            >
              <span className="nav-num">·</span>
              <span>{name}</span>
            </button>
          ))}
        </div>
      )}

      <button
        ref={brandChipRef}
        className="side-account" type="button"
        onClick={openBrandEditor}
        title={`${branding?.name_long || "Customer"} · click to edit brand`}
      >
        <span className="side-account-logo" aria-hidden="true">
          <svg viewBox="0 0 32 32" width="20" height="20">
            <rect x="0" y="0" width="32" height="32" rx="6" fill={branding?.color || "#CC0000"} />
            <text x="16" y="21" textAnchor="middle" fontFamily="Arial Black, Arial, sans-serif" fontWeight="900" fontSize="13" fill="#FFFFFF" letterSpacing="-0.5">{branding?.initials || "??"}</text>
          </svg>
        </span>
        <span className="side-account-text">
          <span className="side-account-name">{branding?.name_long || "Customer"}</span>
        </span>
        <span className="side-account-chev" aria-hidden="true">⋯</span>
      </button>
      {brandEdit && brandAnchor ? (
          <div
            ref={brandPopRef}
            role="dialog"
            aria-label="Edit customer brand"
            style={{
              // `position: fixed` so the popover escapes the sidebar's
              // overflow:auto clipping. Anchored to the chip via the
              // bounding-rect measurement on open. `bottom` is derived
              // from viewport-height - chip.top so the popover floats
              // ABOVE the chip with an 8px gap.
              position: "fixed",
              bottom: `calc(100vh - ${brandAnchor.top}px + 8px)`,
              left: brandAnchor.left,
              width: brandAnchor.width,
              background: "white",
              color: "var(--fg-0)",
              border: "1px solid var(--border-1)",
              borderRadius: 10,
              boxShadow: "0 10px 30px rgba(0,0,0,.25)",
              padding: 12,
              zIndex: 1000,
              fontFamily: "inherit",
              boxSizing: "border-box",
            }}
            onClick={(e) => e.stopPropagation()}
          >
            <div style={{ fontSize: 11, color: "var(--fg-2)", textTransform: "uppercase", letterSpacing: ".04em", marginBottom: 8, fontWeight: 600 }}>
              Customer brand
            </div>
            <label style={{ display: "block", fontSize: 11, color: "var(--fg-2)", marginBottom: 2 }}>Short name</label>
            <input
              type="text" value={bForm.name}
              onChange={(e) => setBForm({ ...bForm, name: e.target.value })}
              placeholder="DigiKey"
              style={{ width: "100%", boxSizing: "border-box", padding: "7px 10px", fontSize: 13, border: "1px solid var(--border-2)", borderRadius: 7, marginBottom: 8, fontFamily: "inherit" }}
            />
            <label style={{ display: "block", fontSize: 11, color: "var(--fg-2)", marginBottom: 2 }}>Long name (sidebar)</label>
            <input
              type="text" value={bForm.name_long}
              onChange={(e) => setBForm({ ...bForm, name_long: e.target.value })}
              placeholder="DigiKey Electronics"
              style={{ width: "100%", boxSizing: "border-box", padding: "7px 10px", fontSize: 13, border: "1px solid var(--border-2)", borderRadius: 7, marginBottom: 8, fontFamily: "inherit" }}
            />
            <div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
              <div style={{ flex: 1 }}>
                <label style={{ display: "block", fontSize: 11, color: "var(--fg-2)", marginBottom: 2 }}>Color</label>
                <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
                  <input
                    type="color" value={bForm.color}
                    onChange={(e) => setBForm({ ...bForm, color: e.target.value })}
                    style={{ width: 32, height: 32, padding: 0, border: "1px solid var(--border-2)", borderRadius: 6, cursor: "pointer", background: "white" }}
                  />
                  <input
                    type="text" value={bForm.color}
                    onChange={(e) => setBForm({ ...bForm, color: e.target.value })}
                    placeholder="#CC0000"
                    style={{ flex: 1, boxSizing: "border-box", padding: "7px 10px", fontSize: 12, fontFamily: "ui-monospace, monospace", border: "1px solid var(--border-2)", borderRadius: 7 }}
                  />
                </div>
              </div>
              <div style={{ width: 90 }}>
                <label style={{ display: "block", fontSize: 11, color: "var(--fg-2)", marginBottom: 2 }}>Initials</label>
                <input
                  type="text" value={bForm.initials_override}
                  onChange={(e) => setBForm({ ...bForm, initials_override: e.target.value.slice(0, 3) })}
                  placeholder={branding?.initials || "auto"}
                  maxLength={3}
                  style={{ width: "100%", boxSizing: "border-box", padding: "7px 10px", fontSize: 13, fontFamily: "ui-monospace, monospace", border: "1px solid var(--border-2)", borderRadius: 7, textAlign: "center", textTransform: "uppercase" }}
                />
              </div>
            </div>
            <div style={{ fontSize: 10.5, color: "var(--fg-3)", marginBottom: 10, lineHeight: 1.4 }}>
              Used in the sidebar logo, "on behalf of …" in emails, and the PDF cover wordmark.
              Leave initials blank to auto-derive from the long name.
            </div>
            {bErr ? <div style={{ fontSize: 11, color: "#c44", marginBottom: 8 }}>{bErr}</div> : null}
            <div style={{ display: "flex", justifyContent: "flex-end", gap: 6 }}>
              <button
                type="button" onClick={() => setBrandEdit(false)} disabled={bSaving}
                style={{ padding: "6px 12px", fontSize: 12, border: "1px solid var(--border-2)", borderRadius: 7, background: "white", color: "var(--fg-1)", cursor: "pointer", fontFamily: "inherit" }}
              >Cancel</button>
              <button
                type="button" onClick={saveBranding} disabled={bSaving}
                style={{ padding: "6px 14px", fontSize: 12, border: 0, borderRadius: 7, background: "var(--sb-ultra-violet)", color: "white", fontWeight: 600, cursor: bSaving ? "not-allowed" : "pointer", fontFamily: "inherit", opacity: bSaving ? 0.6 : 1 }}
              >{bSaving ? "Saving…" : "Save"}</button>
            </div>
          </div>
        ) : null}

      <button className="side-logout" type="button" onClick={authLogout} title={authCurrentEmail() ? `Sign out ${authCurrentEmail()}` : "Sign out"}>
        <span className="side-logout-icon" aria-hidden="true">⏻</span>
        <span className="side-logout-text">Sign out</span>
      </button>
    </aside>
  );
}

// =========================================================================
// Wizard stepper + foot
// =========================================================================
const STEPS = [
  ["templates", "Template"],
  ["brief", "Brief"],
  ["schema", "Schema"],
  ["recipients", "Recipients"],
  ["review", "Review"],
];

function Stepper({ screen, setScreen }) {
  const idx = STEPS.findIndex(([id]) => id === screen);
  if (idx < 0) return null;
  return (
    <div className="stepper">
      {STEPS.map(([id, label], i) => {
        const state = i < idx ? "done" : i === idx ? "active" : "";
        return (
          <button key={id} className={cx("step", state)} onClick={() => setScreen(id)}>
            <span className="num">{i < idx ? "✓" : i + 1}</span>
            <span style={{ minWidth: 0, overflow: "hidden" }}>
              <span className="lbl-name">{label}</span>
              <span className="lbl-meta">Step {i + 1}/{STEPS.length}</span>
            </span>
          </button>
        );
      })}
    </div>
  );
}

function WizardFoot({ setScreen, prev, next, nextLabel = "Next" }) {
  return (
    <div className="wizard-foot">
      <div className="row">
        {prev ? (
          <button className="btn ghost" onClick={() => setScreen(prev)}>← Back</button>
        ) : <span />}
        <span className="meta" style={{ marginLeft: 12 }}>
          <span className="kbd">⌘</span> <span className="kbd">S</span> save draft
        </span>
      </div>
      {next && (
        <button className="btn primary" onClick={() => setScreen(next)}>
          {nextLabel} →
        </button>
      )}
    </div>
  );
}

// =========================================================================
// Overview
// =========================================================================
function Overview({ setScreen }) {
  return (
    <>
      <div className="hero-card" data-screen-label="overview">
        <div className="eyebrow" style={{ color: "rgba(220,237,255,.85)" }}>Hi-fi prototype · v0.2</div>
        <h1>Configure once, the agent does the rest.</h1>
        <p>
          A walkthrough of the proposed Data Collection framework, anchored on
          the DigiKey Section 232 tariff-origin scenario. Click around the
          sidebar — the 5-step setup is the primary path.
        </p>
        <div className="row" style={{ gap: 12 }}>
          <button className="btn warm lg" onClick={() => setScreen("templates")}>
            Start setup →
          </button>
        </div>
      </div>

      <div className="cols-2 mb-6">
        <div className="card spotlight">
          <h2 className="h">Schema, Brief, Recipients → the agent does the rest.</h2>
          <p className="text" style={{ marginTop: 12 }}>
            Today every new question type (rare-earth, tariff, PCN) is custom pipeline
            work. The framework flips that: a CSM configures four primitives, the
            runtime adapts.
          </p>
          <div className="row" style={{ marginTop: 16, gap: 8, flexWrap: "wrap" }}>
            <span className="chip acc">Campaign</span>
            <span className="chip acc">Context Brief</span>
            <span className="chip acc">Data Schema</span>
            <span className="chip acc">Recipients</span>
          </div>
        </div>

        <div className="card amethyst">
          <div className="eyebrow">Today's scenario</div>
          <h2 className="h">DigiKey · Section 232 origin trace</h2>
          <p className="text" style={{ marginTop: 12, color: "rgba(220,237,255,.85)" }}>
            New weight-based rules from the March 2026 EO. We need smelt / cast /
            diffusion / assembly country per SKU across 247 suppliers. Mill certs
            are gold-standard.
          </p>
          <hr className="divider" style={{ borderColor: "rgba(220,237,255,.18)" }} />
          <div className="row between" style={{ gap: 12 }}>
            <span className="meta" style={{ color: "rgba(220,237,255,.7)" }}>247 suppliers</span>
            <span className="meta" style={{ color: "rgba(220,237,255,.7)" }}>14-day window</span>
            <span className="meta" style={{ color: "rgba(220,237,255,.7)" }}>5 data points</span>
          </div>
        </div>
      </div>

      <div className="cols-3">
        <div className="path-card" onClick={() => setScreen("templates")}>
          <div className="num">Path A · Setup</div>
          <div className="icon-tile-lg"><img src={ICON("Collect")} alt="" /></div>
          <h3 className="h-2">5–10 min wizard</h3>
          <p className="text" style={{ marginTop: 6 }}>Five linear steps. Sensible defaults; ship in 5, tune in 10.</p>
          <div className="row" style={{ marginTop: 16 }}>
            <span className="btn quiet">Start setup →</span>
          </div>
        </div>
        <div className="path-card" onClick={() => setScreen("thread")}>
          <div className="num">Path B · Runtime</div>
          <div className="icon-tile-lg"><img src={ICON("Mail")} alt="" /></div>
          <h3 className="h-2">Watch the agent work</h3>
          <p className="text" style={{ marginTop: 6 }}>A live supplier thread — outbound, inbound parsing, follow-up loop.</p>
          <div className="row" style={{ marginTop: 16 }}>
            <span className="btn quiet">Open thread →</span>
          </div>
        </div>
        <div className="path-card" onClick={() => setScreen("audit")}>
          <div className="num">Path C · Output</div>
          <div className="icon-tile-lg"><img src={ICON("Document")} alt="" /></div>
          <h3 className="h-2">Audit & export</h3>
          <p className="text" style={{ marginTop: 6 }}>Every value clickable to source. The trust artifact.</p>
          <div className="row" style={{ marginTop: 16 }}>
            <span className="btn quiet">See audit view →</span>
          </div>
        </div>
      </div>
    </>
  );
}

// =========================================================================
// Pick template
// =========================================================================
const TEMPLATES = [
  { id: "blank", name: "Blank", desc: "Start from zero. Add data points one at a time.", fields: 0, mins: 12, tag: "" },
  { id: "carbon", name: "Carbon (current scope)", desc: "Existing 8 data points. Backwards-compatible parity with today's agent.", fields: 8, mins: 4, tag: "Parity" },
  { id: "carbon-x", name: "Carbon-extended", desc: "+ cumulative energy demand, non-fossil resource use, EOL splits per country.", fields: 14, mins: 6, tag: "Tom · Apr 22" },
  { id: "rare-earth", name: "Rare-Earth Provenance", desc: "Multi-tier supplier traversal. Tier-2/3 prompts baked in.", fields: 11, mins: 7, tag: "Western Digital" },
  { id: "tariff", name: "Tariff Origin (Section 232)", desc: "Country of smelt / cast / diffusion / assembly. Weight-based EO logic.", fields: 5, mins: 5, tag: "DigiKey · today's pick" },
  { id: "pcn", name: "PCN", desc: "Product Change Notification. Supplier-initiated structured flow.", fields: 9, mins: 8, tag: "v1.1" },
];

function ScreenTemplates({ setScreen, tw }) {
  const [picked, setPicked] = useState("tariff");
  return (
    <div data-screen-label="templates">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Step 1 of 5</div>
          <h1 className="screen-title">Pick a starter template</h1>
          <p className="screen-sub">
            Templates are forkable schemas — not hardcoded pipelines. Anything you
            change later overrides the template; nothing is locked.
          </p>
        </div>
      </div>

      <Stepper screen="templates" setScreen={setScreen} />

      <div className="cols-3">
        {TEMPLATES.map((t) => (
          <div
            key={t.id}
            className={cx("template-card", picked === t.id && "picked")}
            onClick={() => setPicked(t.id)}
          >
            {picked === t.id && <span className="chip acc picked-badge">✓ selected</span>}
            <div className="eyebrow acc" style={{ marginBottom: 6 }}>{t.tag || "—"}</div>
            <h3 className="h-2">{t.name}</h3>
            <p className="text" style={{ marginTop: 8, minHeight: 56 }}>{t.desc}</p>
            <div className="tpl-meta">
              <span>{t.fields} data points</span>
            </div>
          </div>
        ))}
      </div>

      <WizardFoot setScreen={setScreen} prev={null} next="brief" />
    </div>
  );
}

// =========================================================================
// Brief
// =========================================================================
function ScreenBrief({ setScreen, tw }) {
  const [name, setName] = useState("DigiKey Section 232 Origin Trace — Q2 2026");
  const [brief, setBrief] = useState(
    `Per the March 2026 executive order, Section 232 is now weight-based: aggregate Cu/steel/Al content under 15% of total weight is exempt; over 15% or unknown defaults to a 200% tariff.

We need smelt/cast country, country of diffusion, and country of assembly per SKU. Suppliers may not know the new rules — explain briefly if they push back. Mill certificates are the gold-standard source.`
  );
  const [clarifications, setClarifications] = useState([
    "Smelter country must be the actual smelter, not the foundry or mill.",
    "If multiple diffusion sites, ask for the primary one.",
    "If Cu content is given as a range, take the midpoint.",
    "If a supplier says 'unknown', re-ask once with extra context.",
  ]);
  const [draft, setDraft] = useState("");

  return (
    <div data-screen-label="brief">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Step 2 of 5</div>
          <h1 className="screen-title">Name + Context Brief</h1>
          <p className="screen-sub">
            2–4 sentences. Plain English: what, why, what you'll do with it. The
            agent ingests this as the topic frame for every supplier conversation.
          </p>
        </div>
        <span className="screen-tag">The hard part</span>
      </div>

      <Stepper screen="brief" setScreen={setScreen} />

      <div className="cols-side">
        <div className="col">
          <div className="card">
            <div className="eyebrow">Campaign name</div>
            <input className="field lg" value={name} onChange={(e) => setName(e.target.value)} />
          </div>

          <div className="card">
            <div className="row between mb-3">
              <div className="eyebrow">Context Brief</div>
            </div>
            <textarea
              className="field area"
              style={{ minHeight: 220, fontSize: 14 }}
              value={brief}
              onChange={(e) => setBrief(e.target.value)}
            />
            <div className="row mt-4" style={{ gap: 8, flexWrap: "wrap" }}>
              <button className="btn ghost sm">+ Insert regulation excerpt</button>
              <button className="btn ghost sm">+ Link source doc</button>
            </div>
          </div>

          <div className="card">
            <div className="row between">
              <div className="eyebrow">Things to clarify with suppliers</div>
              <span className="meta">agent applies these automatically</span>
            </div>
            <p className="meta mt-4" style={{ marginBottom: 12 }}>
              Edge cases the agent should self-resolve before bothering you
              (disambiguation, fallbacks, when to escalate).
            </p>
            <div className="col" style={{ gap: 8 }}>
              {clarifications.map((c, i) => (
                <div key={i} style={{
                  display: "flex", alignItems: "center", gap: 10,
                  padding: "10px 14px",
                  border: "1px solid var(--border-1)",
                  borderRadius: "var(--radius-md)",
                  background: "var(--bg-soft)",
                }}>
                  <span style={{ color: "var(--sb-ultra-violet)", fontWeight: 700 }}>→</span>
                  <input
                    className="field"
                    style={{ flex: 1, border: "none", padding: 0, background: "transparent", boxShadow: "none" }}
                    value={c}
                    onChange={(e) => setClarifications((cs) => cs.map((v, j) => j === i ? e.target.value : v))}
                  />
                  <button
                    className="btn quiet sm"
                    onClick={() => setClarifications((cs) => cs.filter((_, j) => j !== i))}
                  >✕</button>
                </div>
              ))}
              <div className="row" style={{ gap: 8, marginTop: 4 }}>
                <input
                  className="field"
                  style={{ flex: 1 }}
                  placeholder="+ Add a clarification (e.g. 'If percent is a range, take the midpoint')"
                  value={draft}
                  onChange={(e) => setDraft(e.target.value)}
                  onKeyDown={(e) => {
                    if (e.key === "Enter" && draft.trim()) {
                      setClarifications((cs) => [...cs, draft.trim()]);
                      setDraft("");
                    }
                  }}
                />
                <button
                  className="btn primary sm"
                  onClick={() => {
                    if (draft.trim()) {
                      setClarifications((cs) => [...cs, draft.trim()]);
                      setDraft("");
                    }
                  }}
                >Add</button>
              </div>
            </div>
          </div>
        </div>

        <div className="col">
          <div className="card spotlight">
            <div className="eyebrow acc">What the agent learns from this</div>
            <ul style={{ paddingLeft: 18, margin: "12px 0 0", lineHeight: 1.7, fontSize: 13.5, color: "var(--fg-1)" }}>
              <li><strong>Topic frame:</strong> Section 232, tariff origin, weight rule</li>
              <li><strong>Tone hints:</strong> regulatory; explain if pushed back</li>
              <li><strong>Anchor source:</strong> mill certificates</li>
              <li><strong>Stakes signal:</strong> 200% tariff downside → high urgency</li>
              <li><strong>Self-resolve rules:</strong> {clarifications.length} clarifications</li>
            </ul>
            <hr className="divider" />
            <div className="eyebrow acc">Sample supplier Q the agent can now answer</div>
            <p className="text" style={{ marginTop: 8 }}>
              <em>"Why are you asking about smelter country now?"</em> — agent can
              reference the EO + weight-based rule without escalating.
            </p>
          </div>
        </div>
      </div>

      <WizardFoot setScreen={setScreen} prev="templates" next="schema" />
    </div>
  );
}

// =========================================================================
// Schema
// =========================================================================
const SEED_FIELDS = [
  {
    name: "country_of_smelt",
    label: "Country of smelt",
    type: "string",
    required: true,
    desc: "Country where the primary metal was smelted from ore. Refer to mill certificates if available.",
  },
  {
    name: "country_of_diffusion",
    label: "Country of diffusion",
    type: "string",
    required: true,
    desc: "Country where the wafer was diffused / fab step occurred.",
  },
  {
    name: "country_of_assembly",
    label: "Country of assembly",
    type: "string",
    required: true,
    desc: "Country where the part was packaged / assembled.",
  },
  {
    name: "cu_content_pct",
    label: "Copper content (%)",
    type: "number",
    required: false,
    desc: "Aggregate Cu content as % of total part weight.",
  },
  {
    name: "mill_certificate",
    label: "Mill certificate",
    type: "file",
    required: false,
    desc: "PDF mill cert or equivalent traceability doc.",
  },
];

function ScreenSchema({ setScreen, tw }) {
  const [fields, setFields] = useState(SEED_FIELDS);
  const [open, setOpen] = useState(0);
  const toggleReq = (i) => setFields((fs) => fs.map((f, j) => j === i ? { ...f, required: !f.required } : f));
  const moveField = (from, to) => {
    if (from === to || from < 0 || to < 0) return;
    setFields((fs) => {
      const next = [...fs];
      const [item] = next.splice(from, 1);
      next.splice(to, 0, item);
      return next;
    });
    setOpen(-1);
  };
  const addField = () => {
    setFields((fs) => {
      const n = fs.length + 1;
      const next = {
        name: `new_field_${n}`,
        label: `New field ${n}`,
        type: "string",
        required: false,
        desc: "Describe what suppliers should put here.",
      };
      const out = [...fs, next];
      setOpen(out.length - 1);
      return out;
    });
  };

  return (
    <div data-screen-label="schema">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Step 3 of 5</div>
          <h1 className="screen-title">Define the Data Schema</h1>
          <p className="screen-sub">
            Add, remove, or reorder data points. Each one has a per-supplier label
            and an agent_hint (ambiguity policy, escalation). The agent handles
            synonyms, casing, and spelling variants on its own.
          </p>
        </div>
        <span className="screen-tag">The heart of it</span>
      </div>

      <Stepper screen="schema" setScreen={setScreen} />

      <div className="note mb-4">
        <strong>✨ Auto-populated from your Brief.</strong> The 5 data points
        below were inferred from what you wrote in step 2 — review, edit, or add
        more. Re-run inference any time the Brief changes.
        <div className="row mt-4" style={{ gap: 8 }}>
          <button className="btn ghost sm">↻ Re-infer from Brief</button>
          <button className="btn ghost sm" onClick={() => setScreen("brief")}>← Edit Brief</button>
        </div>
      </div>

      <div className="row between mb-4">
        <div />
        <div className="row">
          <button className="btn primary sm" onClick={addField}>+ Add data point</button>
        </div>
      </div>

      {tw.schemaVariant === "Notion-list" && (
        <SchemaVariantList fields={fields} open={open} setOpen={setOpen} toggleReq={toggleReq} setScreen={setScreen} addField={addField} moveField={moveField} />
      )}
      {tw.schemaVariant === "Spreadsheet" && (
        <SchemaVariantSheet fields={fields} toggleReq={toggleReq} addField={addField} />
      )}
      {tw.schemaVariant === "Card-form" && (
        <SchemaVariantCards fields={fields} toggleReq={toggleReq} addField={addField} />
      )}

      <WizardFoot setScreen={setScreen} prev="brief" next="recipients" />
    </div>
  );
}

function SchemaVariantList({ fields, open, setOpen, toggleReq, setScreen, moveField }) {
  const [dragIdx, setDragIdx] = React.useState(null);
  const [overIdx, setOverIdx] = React.useState(null);
  return (
    <div className="col" style={{ gap: 10 }}>
      {fields.map((f, i) => {
        const isOpen = open === i;
        if (isOpen) {
          return (
            <div key={f.name} className="field-row expanded">
              <div className="row between mb-4">
                <div className="eyebrow acc">Editing data point · #{i + 1}</div>
                <button className="btn primary sm" onClick={() => setOpen(-1)}>Done ✓</button>
              </div>
              <div className="cols-2">
                <div className="col">
                  <div>
                    <div className="eyebrow">Data point label</div>
                    <input className="field" defaultValue={f.label} />
                    <div className="meta" style={{ marginTop: 4 }}>This is what suppliers see in the email.</div>
                  </div>
                  <div>
                    <div className="eyebrow">Description for supplier</div>
                    <textarea className="field area" defaultValue={f.desc} />
                  </div>
                  <div className="row" style={{ gap: 16 }}>
                    <label
                      className={cx("check", f.required && "checked")}
                      onClick={() => toggleReq(i)}
                    >
                      <span className="box">{f.required ? "✓" : ""}</span>
                      <span>Required</span>
                    </label>
                  </div>
                </div>
                <div className="col">
                  <div className="note">
                    <strong>🧠 Handled by the agent</strong>
                    <p className="meta" style={{ marginTop: 8 }}>
                      You don't need to teach the agent how to read this data point.
                      Spelling variants, synonyms, weird casing, and ambiguous
                      answers are resolved automatically — using the
                      clarification rules you wrote in the <strong>Brief</strong>.
                    </p>
                    <button className="btn ghost sm mt-4" onClick={() => setScreen("brief")}>
                      ← Edit Brief
                    </button>
                  </div>
                </div>
              </div>
            </div>
          );
        }
        return (
          <div
            key={f.name}
            className="field-row"
            draggable
            onDragStart={(e) => {
              setDragIdx(i);
              e.dataTransfer.effectAllowed = "move";
            }}
            onDragEnter={(e) => {
              e.preventDefault();
              if (dragIdx != null && dragIdx !== i) setOverIdx(i);
            }}
            onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }}
            onDragLeave={() => { if (overIdx === i) setOverIdx(null); }}
            onDrop={(e) => {
              e.preventDefault();
              if (dragIdx != null) moveField(dragIdx, i);
              setDragIdx(null);
              setOverIdx(null);
            }}
            onDragEnd={() => { setDragIdx(null); setOverIdx(null); }}
            style={{
              opacity: dragIdx === i ? 0.4 : 1,
              borderColor: overIdx === i ? "var(--sb-ultra-violet)" : undefined,
              boxShadow: overIdx === i ? "var(--shadow-glow)" : undefined,
              cursor: dragIdx === i ? "grabbing" : undefined,
              transition: "opacity .15s, border-color .15s, box-shadow .15s",
            }}
          >
            <div className="grip" style={{ cursor: "grab" }} title="Drag to reorder">⋮⋮</div>
            <div>
              <div className="row" style={{ gap: 8 }}>
                <span className="label-name">{f.label}</span>
              </div>
            </div>
            <button
              className="req-toggle"
              data-on={f.required ? "true" : "false"}
              onClick={(e) => { e.stopPropagation(); toggleReq(i); }}
              title="Click to toggle required"
              type="button"
            >
              <span className="req-track"><span className="req-knob" /></span>
              <span className="req-label">{f.required ? "Required" : "Optional"}</span>
            </button>
            <div className="meta" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
              {f.desc}
            </div>
            <button className="btn ghost sm" onClick={() => setOpen(i)}>Edit</button>
          </div>
        );
      })}
    </div>
  );
}

function SchemaVariantSheet({ fields, toggleReq, addField }) {
  return (
    <div className="card" style={{ padding: 0, overflow: "hidden" }}>
      <table className="table">
        <thead>
          <tr>
            <th style={{ width: 32 }}></th>
            <th>Data point label</th>
            <th>Required?</th>
            <th>Description</th>
            <th style={{ width: 60 }}></th>
          </tr>
        </thead>
        <tbody>
          {fields.map((f, i) => (
            <tr key={f.name}>
              <td style={{ color: "var(--fg-3)", textAlign: "center" }}>⋮⋮</td>
              <td><strong>{f.label}</strong></td>
              <td>
                <label className={cx("check", f.required && "checked")} onClick={() => toggleReq(i)}>
                  <span className="box">{f.required ? "✓" : ""}</span>
                </label>
              </td>
              <td className="meta" style={{ maxWidth: 360, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{f.desc}</td>
              <td className="row-action"><button className="btn quiet sm">⋯</button></td>
            </tr>
          ))}
          <tr>
            <td colSpan={5} style={{ textAlign: "center", padding: 16 }}>
              <button className="btn ghost sm" onClick={addField}>+ Add row</button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  );
}

function SchemaVariantCards({ fields, toggleReq, addField }) {
  return (
    <div className="cols-2" style={{ gap: 18 }}>
      {fields.map((f, i) => (
        <div key={f.name} className="card">
          <div className="row between">
            <div className="eyebrow acc">Data point #{i + 1}</div>
          </div>
          <h3 className="h-2 mt-4">{f.label}</h3>
          <hr className="divider" />
          <div className="eyebrow">Description for supplier</div>
          <p className="text">{f.desc}</p>
          <div className="row mt-4" style={{ gap: 12 }}>
            <label className={cx("check", f.required && "checked")} onClick={() => toggleReq(i)}>
              <span className="box">{f.required ? "✓" : ""}</span>
              <span>Required</span>
            </label>
            <span className="chip acc">🧠 agent rules</span>
          </div>
        </div>
      ))}
      <button onClick={addField} className="card dashed" style={{ display: "grid", placeItems: "center", minHeight: 220, cursor: "pointer", border: "2px dashed var(--border-2)" }}>
        <div className="text-center">
          <div style={{ fontSize: 36, color: "var(--fg-3)" }}>+</div>
          <div className="text">Add data point</div>
        </div>
      </button>
    </div>
  );
}

// =========================================================================
// Recipients
// =========================================================================
const RECIPIENTS = [
  ["Texas Instruments", "compliance@ti.com", "TI · 14 parts", ["LM358P", "TPS62840", "MSP430F5529", "TLV9001", "SN74HC04N", "+ 9 more"]],
  ["Analog Devices", "supply@analog.com", "ADI · 9 parts", ["AD8232", "ADXL345", "LTC3588", "AD7124-8", "+ 5 more"]],
  ["NXP Semiconductors", "trade.compliance@nxp.com", "NXP · 22 parts", ["LPC1768", "MK22FN512", "PCA9685", "MFRC522", "+ 18 more"]],
  ["Infineon", "section232@infineon.com", "IFX · 18 parts", ["IRFZ44N", "BSS138", "TLE5012B", "IM69D130", "+ 14 more"]],
  ["STMicroelectronics", "tariff@st.com", "ST · 11 parts", ["STM32F103", "L298N", "LSM6DSO", "VL53L1X", "+ 7 more"]],
  ["onsemi", "compliance@onsemi.com", "ON · 7 parts", ["NCV7240", "FDC6331L", "LM2596", "+ 4 more"]],
  ["Microchip", "ops@microchip.com", "MCHP · 13 parts", ["ATmega328P", "PIC16F877A", "MCP2515", "+ 10 more"]],
  ["...241 more", "—", "—", []],
];

function ScreenRecipients({ setScreen, tw }) {
  const [showAdd, setShowAdd] = useState(false);
  const [newName, setNewName] = useState("");
  const [newEmail, setNewEmail] = useState("");
  const [newScope, setNewScope] = useState("");
  const [extras, setExtras] = useState([]);
  const [openParts, setOpenParts] = useState(null);

  // Selection state — which suppliers (by name) are IN the campaign.
  // Default: every uploaded named supplier is selected; placeholder "...241 more" is excluded.
  const isSelectable = (name) => name && !name.startsWith("...");
  const initiallySelected = () => {
    const set = new Set();
    RECIPIENTS.forEach(([n]) => { if (isSelectable(n)) set.add(n); });
    return set;
  };
  const [selected, setSelected] = useState(initiallySelected);

  const toggleOne = (name) => {
    setSelected((s) => {
      const n = new Set(s);
      if (n.has(name)) n.delete(name); else n.add(name);
      return n;
    });
  };

  const allRows = [
    ...RECIPIENTS.filter(([n]) => isSelectable(n)),
    ...extras,
  ];
  const allNames = allRows.map(([n]) => n);
  const allChecked = allNames.length > 0 && allNames.every((n) => selected.has(n));
  const someChecked = allNames.some((n) => selected.has(n)) && !allChecked;
  const toggleAll = () => {
    setSelected((s) => {
      if (allChecked) return new Set();
      return new Set(allNames);
    });
  };
  const totalUploadedCount = 247; // matches the CSV row count
  // "selected" tracks the visible/named ones; the placeholder bucket of 241 is selected by default too.
  const namedVisibleSelected = allNames.filter((n) => selected.has(n)).length;

  const submit = () => {
    if (!newName.trim() || !newEmail.trim()) return;
    setExtras((xs) => [...xs, [newName.trim(), newEmail.trim(), newScope.trim() || "—", []]]);
    setNewName(""); setNewEmail(""); setNewScope("");
    setShowAdd(false);
  };

  return (
    <div data-screen-label="recipients">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Step 4 of 5</div>
          <h1 className="screen-title">Upload recipients + parts</h1>
          <p className="screen-sub">
            CSV: supplier name, contact email, and the part numbers / SKUs you
            want each supplier to answer about. The parts list is attached to
            that supplier's outbound email.
          </p>
        </div>
      </div>

      <Stepper screen="recipients" setScreen={setScreen} />

      <div className="cols-side">
        <div className="card">
          <div className="row between">
            <h3 className="h-2">Recipients</h3>
            <div className="row" style={{ gap: 10 }}>
              <span className="chip ok">✓ {totalUploadedCount} rows loaded</span>
              <span className="chip acc">{selected.size} selected for campaign</span>
            </div>
          </div>
          <div className="row mt-4" style={{ gap: 8 }}>
            <button className="btn primary sm">📎 Upload supplier list (CSV)</button>
            <button className="btn ghost sm" onClick={() => setShowAdd(true)}>+ Add supplier manually</button>
            <button className="btn ghost sm">↗ Pull from existing list</button>
          </div>
          <div className="meta mt-4">
            ↳ Loaded from <span className="mono">digikey_q2_suppliers.csv</span> · supplier name, contact email, and the part numbers / SKUs each supplier should answer about.
          </div>
          <hr className="divider" />
          <table className="table">
            <thead>
              <tr>
                <th style={{ width: 36 }}>
                  <label className={cx("check", allChecked && "checked")} onClick={toggleAll} title={allChecked ? "Deselect all" : "Select all"}>
                    <span className="box">{allChecked ? "✓" : someChecked ? "–" : ""}</span>
                  </label>
                </th>
                <th>Supplier</th>
                <th>Contact</th>
                <th>Parts in scope</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {RECIPIENTS.map(([n, e, s, parts], idx) => {
                const selectable = isSelectable(n);
                const checked = selected.has(n);
                return (
                <Fragment key={n}>
                  <tr style={{ opacity: selectable && !checked ? 0.55 : 1 }}>
                    <td>
                      {selectable ? (
                        <label className={cx("check", checked && "checked")} onClick={() => toggleOne(n)}>
                          <span className="box">{checked ? "✓" : ""}</span>
                        </label>
                      ) : null}
                    </td>
                    <td><strong>{n}</strong></td>
                    <td className="mono" style={{ color: "var(--fg-2)" }}>{e}</td>
                    <td>
                      {parts && parts.length > 0 ? (
                        <button
                          className="btn quiet sm"
                          style={{ padding: "2px 8px" }}
                          onClick={() => setOpenParts(openParts === idx ? null : idx)}
                        >
                          {openParts === idx ? "▾" : "▸"} {s}
                        </button>
                      ) : (
                        <span className="meta">{s}</span>
                      )}
                    </td>
                    <td className="row-action"><button className="btn quiet sm">⋯</button></td>
                  </tr>
                  {openParts === idx && parts && parts.length > 0 && (
                    <tr>
                      <td colSpan={5} style={{ background: "var(--bg-soft)", padding: "12px 16px" }}>
                        <div className="eyebrow" style={{ marginBottom: 8 }}>Part numbers in this campaign</div>
                        <div className="row" style={{ gap: 6, flexWrap: "wrap" }}>
                          {parts.map((p, i) => (
                            <span key={i} className="chip mono">{p}</span>
                          ))}
                        </div>
                        <div className="meta" style={{ marginTop: 10 }}>
                          ↳ Attached to {n}'s outbound email as <span className="mono">{n.toLowerCase().replace(/[^a-z]/g, "")}_q2_parts.csv</span>
                        </div>
                      </td>
                    </tr>
                  )}
                </Fragment>
              );})}
              {extras.map(([n, e, s], i) => {
                const checked = selected.has(n);
                return (
                <tr key={"x-" + i} style={{ background: "var(--bg-soft)", opacity: !checked ? 0.55 : 1 }}>
                  <td>
                    <label className={cx("check", checked && "checked")} onClick={() => toggleOne(n)}>
                      <span className="box">{checked ? "✓" : ""}</span>
                    </label>
                  </td>
                  <td>{n} <span className="chip ok" style={{ marginLeft: 6 }}>new</span></td>
                  <td className="mono" style={{ color: "var(--fg-2)" }}>{e}</td>
                  <td className="meta">{s}</td>
                  <td className="row-action">
                    <button className="btn quiet sm" onClick={() => setExtras((xs) => xs.filter((_, j) => j !== i))}>✕</button>
                  </td>
                </tr>
              );})}
            </tbody>
          </table>
        </div>

        <div className="col">
          <div className="card">
            <div className="eyebrow">Auto-detected columns</div>
            <table className="table" style={{ marginTop: 8 }}>
              <tbody>
                <tr><td><span className="mono">company_name</span></td><td>→</td><td><strong>Supplier name</strong></td></tr>
                <tr><td><span className="mono">primary_email</span></td><td>→</td><td><strong>Contact</strong></td></tr>
                <tr><td><span className="mono">part_numbers</span></td><td>→</td><td><strong>Parts</strong></td></tr>
                <tr><td><span className="mono">region</span></td><td>→</td><td className="meta">(unmapped)</td></tr>
              </tbody>
            </table>
          </div>

          <div className="card">
            <div className="eyebrow">Start date & time</div>
            <div className="row mt-4" style={{ gap: 10 }}>
              <input className="field" type="date" defaultValue="2026-05-12" style={{ flex: 1 }} />
              <input className="field" type="time" defaultValue="09:00" style={{ width: 130 }} />
            </div>
          </div>

          <div className="card">
            <div className="eyebrow">Escalation</div>
            <div className="row mt-4" style={{ gap: 8, flexWrap: "wrap" }}>
              <span className="text">Escalate to CSM after</span>
              <select className="field" defaultValue="2" style={{ width: 70, padding: "8px 10px" }}>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
                <option>5</option>
              </select>
              <span className="text">consecutive non-responses.</span>
            </div>
          </div>
        </div>
      </div>

      <WizardFoot setScreen={setScreen} prev="schema" next="review" />

      {showAdd && (
        <div
          onClick={() => setShowAdd(false)}
          style={{
            position: "fixed", inset: 0,
            background: "rgba(23, 0, 59, 0.55)",
            backdropFilter: "blur(8px)",
            display: "flex", alignItems: "center", justifyContent: "center",
            zIndex: 1000, padding: 20,
          }}
        >
          <div
            onClick={(e) => e.stopPropagation()}
            className="card"
            style={{
              width: 460, maxWidth: "100%",
              boxShadow: "var(--shadow-3)",
            }}
          >
            <div className="row between">
              <h3 className="h-2">Add supplier manually</h3>
              <button className="btn quiet sm" onClick={() => setShowAdd(false)}>✕</button>
            </div>
            <p className="meta" style={{ marginTop: 4 }}>
              One-off addition — joins this campaign only.
            </p>
            <hr className="divider" />
            <div className="col" style={{ gap: 14 }}>
              <div>
                <div className="eyebrow">Supplier name</div>
                <input
                  className="field"
                  placeholder="e.g. Renesas Electronics"
                  value={newName}
                  onChange={(e) => setNewName(e.target.value)}
                  autoFocus
                />
              </div>
              <div>
                <div className="eyebrow">Contact email</div>
                <input
                  className="field"
                  type="email"
                  placeholder="compliance@renesas.com"
                  value={newEmail}
                  onChange={(e) => setNewEmail(e.target.value)}
                  onKeyDown={(e) => { if (e.key === "Enter") submit(); }}
                />
              </div>
              <div>
                <div className="eyebrow">Scope <span className="meta">(optional)</span></div>
                <input
                  className="field"
                  placeholder="e.g. Renesas · 6 SKUs"
                  value={newScope}
                  onChange={(e) => setNewScope(e.target.value)}
                  onKeyDown={(e) => { if (e.key === "Enter") submit(); }}
                />
              </div>
            </div>
            <div className="row" style={{ justifyContent: "flex-end", gap: 8, marginTop: 20 }}>
              <button className="btn ghost sm" onClick={() => setShowAdd(false)}>Cancel</button>
              <button
                className="btn primary sm"
                onClick={submit}
                disabled={!newName.trim() || !newEmail.trim()}
              >
                + Add supplier
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// =========================================================================
// Review
// =========================================================================
// Reusable multi-value pill input (for email recipient lists, etc.)
function PillField({ list, setList, draft, setDraft, placeholder, compact }) {
  const commit = (raw) => {
    const v = raw.trim().replace(/[,;]+$/, "");
    if (v && !list.includes(v)) setList([...list, v]);
    setDraft("");
  };
  return (
    <div className={cx("pill-input", compact && "pill-input-compact")}>
      {list.map((em, i) => (
        <span key={em + i} className="email-pill">
          {em}
          <button
            type="button"
            className="email-pill-x"
            onClick={() => setList(list.filter((_, j) => j !== i))}
            title="Remove"
          >×</button>
        </span>
      ))}
      <input
        className="pill-input-field"
        value={draft}
        onChange={(e) => setDraft(e.target.value)}
        onKeyDown={(e) => {
          if ((e.key === "Enter" || e.key === "," || e.key === ";") && draft.trim()) {
            e.preventDefault();
            commit(draft);
          } else if (e.key === "Backspace" && !draft && list.length > 0) {
            setList(list.slice(0, -1));
          }
        }}
        onBlur={() => { if (draft.trim()) commit(draft); }}
        placeholder={list.length === 0 ? placeholder : "add another…"}
      />
    </div>
  );
}

function ScreenReview({ setScreen, tw }) {
  const [tone, setTone] = useState("Neutral");
  const [ccMe, setCcMe] = useState(true);
  const [ccEmails, setCcEmails] = useState(["sara.chen@digikey.com"]);
  const [ccDraft, setCcDraft] = useState("");
  const [fromList, setFromList] = useState(["Sluicebox <supplier-agent@sluicebox.io>"]);
  const [toList, setToList] = useState(["compliance@ti.com"]);
  const [fromDraft, setFromDraft] = useState("");
  const [toDraft, setToDraft] = useState("");
  const [subject, setSubject] = useState("DigiKey · Section 232 origin trace request — 14 SKUs");
  const bodyRef = React.useRef(null);
  return (
    <div data-screen-label="review">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Step 5 of 5</div>
          <h1 className="screen-title">Review the first email — then launch</h1>
          <p className="screen-sub">
            One example outbound. Edit tone or signature once and it propagates.
            On approve, the campaign goes live and the agent starts sending.
          </p>
        </div>
        <span className="screen-tag">Ship it</span>
      </div>

      <Stepper screen="review" setScreen={setScreen} />

      <div className="cols-side">
        <div className="email">
          <div className="from email-headers">
            <div className="hdr-row">
              <label>From</label>
              <PillField list={fromList} setList={setFromList} draft={fromDraft} setDraft={setFromDraft} placeholder="sender@company.com" />
            </div>
            <div className="hdr-row">
              <label>To</label>
              <PillField list={toList} setList={setToList} draft={toDraft} setDraft={setToDraft} placeholder="recipient@company.com" />
            </div>
            {ccMe && ccEmails.length > 0 && (
              <div className="hdr-row">
                <label>Cc</label>
                <div className="hdr-pill-display">
                  {ccEmails.map((em, i) => (
                    <span key={em + i} className="email-pill">{em}</span>
                  ))}
                </div>
              </div>
            )}
            <div className="hdr-row">
              <label>Subject</label>
              <input className="hdr-input hdr-subject" value={subject} onChange={(e) => setSubject(e.target.value)} />
            </div>
          </div>
          <div className="email-body" ref={bodyRef} contentEditable suppressContentEditableWarning>
            <h4>Hi team,</h4>
            <p>
              DigiKey is collecting country-of-origin data on roughly 250 supplier
              lines ahead of the new Section 232 weight-based rule (March 2026 EO).
              We're reaching out on their behalf to gather the inputs they need to
              file.
            </p>
            <div className="attach-block" contentEditable={false}>
              <div className="eyebrow acc">📎 Attached · ti_q2_parts.csv (14 rows)</div>
              <div className="row" style={{ gap: 6, flexWrap: "wrap", marginTop: 8 }}>
                {["LM358P", "TPS62840", "MSP430F5529", "TLV9001", "SN74HC04N", "TPS54331", "LM2596S-5.0", "+ 7 more"].map((p) => (
                  <span key={p} className="chip mono">{p}</span>
                ))}
              </div>
              <div className="meta" style={{ marginTop: 8 }}>
                ↳ The 14 TI part numbers from your recipients CSV.
              </div>
            </div>
            <p>For each part in <span className="mono">ti_q2_parts.csv</span>, please confirm:</p>
            <ol>
              <li><strong>Country of smelt</strong> — primary metal smelter location (mill cert preferred)</li>
              <li><strong>Country of diffusion</strong> — wafer fab country</li>
              <li><strong>Country of assembly</strong> — packaging / final assembly</li>
              <li><strong>Copper content (%)</strong> — by total weight, if known</li>
              <li><strong>Mill certificate</strong> — PDF attachment if available</li>
            </ol>
            <p>
              <em>Background:</em> the new rule is weight-based — aggregate Cu/Al/steel
              under 15% is exempt; over 15% or unknown defaults to a 200% tariff.
              Happy to walk through if helpful.
            </p>
            <p>You can reply directly to this email — I'll pick up the data.</p>
            <p>
              Thanks,<br />
              <em>Sluicebox supplier agent · on behalf of DigiKey</em>
            </p>
          </div>
          <div className="email-edit-hint">
            <span className="meta">✏️ Click anywhere to edit. Headers and body are fully editable.</span>
          </div>
        </div>

        <div className="col">
          <div className="card">
            <div className="eyebrow">Tone</div>
            <div className="row mt-4" style={{ gap: 6, flexWrap: "wrap" }}>
              {["Formal", "Neutral", "Warm", "Urgent"].map((t) => (
                <button
                  key={t}
                  className={cx("btn sm", tone === t ? "primary" : "ghost")}
                  onClick={() => setTone(t)}
                >
                  {t}
                </button>
              ))}
            </div>
            <hr className="divider" />
            <div className="eyebrow">Sender signature</div>
            <input className="field mt-4" defaultValue="Sluicebox supplier agent · on behalf of DigiKey" />
            <hr className="divider" />
            <div className="eyebrow">Reply-to</div>
            <input className="field mt-4" defaultValue="ops+digikey-q2@sluicebox.io" />
            <hr className="divider" />
            <div className="row between" style={{ alignItems: "flex-start" }}>
              <div>
                <div className="eyebrow">CC me on every email</div>
                <div className="meta" style={{ marginTop: 4 }}>You'll receive a copy of every outbound email.</div>
              </div>
              <button
                className="req-toggle"
                data-on={ccMe ? "true" : "false"}
                onClick={() => setCcMe((v) => !v)}
                type="button"
              >
                <span className="req-track"><span className="req-knob" /></span>
                <span className="req-label">{ccMe ? "On" : "Off"}</span>
              </button>
            </div>
            {ccMe && (
              <div className="mt-4">
                <PillField
                  list={ccEmails}
                  setList={setCcEmails}
                  draft={ccDraft}
                  setDraft={setCcDraft}
                  placeholder="name@company.com"
                />
              </div>
            )}
          </div>

          <div className="card spotlight">
            <div className="eyebrow acc">Pre-launch checklist</div>
            <div className="col mt-4" style={{ gap: 8 }}>
              {[
                ["Brief written", true],
                ["5 data points defined", true],
                ["247 recipients", true],
                ["Sender domain verified", true],
              ].map(([n, ok]) => (
                <div key={n} className="row" style={{ gap: 8, fontSize: 13 }}>
                  <span style={{
                    width: 18, height: 18, borderRadius: 4,
                    background: ok ? "var(--success)" : "var(--bg-soft)",
                    color: "white",
                    display: "inline-flex", alignItems: "center", justifyContent: "center",
                    fontSize: 11, fontWeight: "bold",
                  }}>{ok ? "✓" : ""}</span>
                  <span>{n}</span>
                </div>
              ))}
            </div>
          </div>

          <button
            className="btn warm lg"
            style={{ width: "100%", padding: "16px 18px", fontSize: 16, justifyContent: "center" }}
            onClick={() => setScreen("dash")}
          >
            🚀 Launch campaign
          </button>
          <span className="meta text-center" style={{ display: "block" }}>
            First emails go out within ~5 min.
          </span>
        </div>
      </div>
    </div>
  );
}

// =========================================================================
// Dashboard
// =========================================================================
const SUPPLIER_ROWS = [
  ["Texas Instruments", "complete", 5, 5, "0.94", "2 days"],
  ["Analog Devices", "complete", 5, 5, "0.91", "1 day"],
  ["NXP Semiconductors", "in-progress", 4, 5, "0.78", "3 days"],
  ["Infineon", "in-progress", 3, 5, "0.66", "5 days"],
  ["STMicroelectronics", "follow-up", 2, 5, "0.51", "6 days"],
  ["onsemi", "flagged", 1, 5, "0.34", "9 days"],
  ["Microchip", "no-reply", 0, 5, "—", "—"],
  ["Renesas", "complete", 5, 5, "0.88", "2 days"],
];

function ScreenDash({ setScreen }) {
  const [filter, setFilter] = useState("all");
  const filterFn = (st) => {
    if (filter === "all") return true;
    if (filter === "flagged") return st === "flagged" || st === "follow-up";
    if (filter === "no-reply") return st === "no-reply";
    return true;
  };
  const visibleRows = SUPPLIER_ROWS.filter(([, st]) => filterFn(st));
  return (
    <div data-screen-label="dashboard">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Live campaign</div>
          <h1 className="screen-title">DigiKey · Q2 origin trace</h1>
          <p className="screen-sub">
            Day 7 of 14. The agent is mid-flight — 132 of 247 suppliers have
            responded; the rest are in various follow-up states.
          </p>
        </div>
        <div className="state-banner">
          <span className="state-dot live" />
          <span>Live · 7d / 14d</span>
        </div>
      </div>

      <div className="cols-3 mb-6">
        <div className="card">
          <div className="eyebrow">Suppliers responded</div>
          <div className="metric-num">132 <span style={{ color: "var(--fg-3)", fontSize: 24 }}>/ 247</span></div>
          <span className="chip ok">53% complete</span>
        </div>
        <div className="card">
          <div className="eyebrow">Data points supplier-confirmed</div>
          <div className="metric-num acc">417 <span style={{ color: "var(--fg-3)", fontSize: 24 }}>/ 660</span></div>
          <span className="chip acc">63% confirmed</span>
        </div>
        <div className="card">
          <div className="eyebrow">Flagged for review</div>
          <div className="metric-num warm">14</div>
          <span className="chip warn">Needs CSM</span>
        </div>
      </div>

      <div className="card mb-6">
        <div className="row between">
          <div className="eyebrow">Campaign progress</div>
          <span className="meta">53% complete · 14 flagged · 7 days remaining</span>
        </div>
        <div className="progress-bar mt-4">
          <i className="ok" style={{ width: "53%" }} />
          <i className="pending" style={{ width: "27%" }} />
          <i className="flag" style={{ width: "6%" }} />
          <i className="miss" style={{ width: "14%" }} />
        </div>
        <div className="legend mt-4">
          <span className="ok"><i />Complete (132)</span>
          <span className="pending"><i />In progress (67)</span>
          <span className="flag"><i />Flagged (14)</span>
          <span className="miss"><i />No reply (34)</span>
        </div>
      </div>

      <div className="cols-wide">
        <div className="card" style={{ padding: 0 }}>
          <div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border-1)" }}>
            <div className="row between">
              <h3 className="h-2">Suppliers</h3>
              <div className="row" style={{ gap: 6 }}>
                <button className={cx("btn sm", filter === "all" ? "primary" : "ghost")} onClick={() => setFilter("all")}>All <span className="meta" style={{ marginLeft: 4 }}>{SUPPLIER_ROWS.length}</span></button>
                <button className={cx("btn sm", filter === "flagged" ? "primary" : "ghost")} onClick={() => setFilter("flagged")}>Flagged <span className="meta" style={{ marginLeft: 4 }}>{SUPPLIER_ROWS.filter(([, st]) => st === "flagged" || st === "follow-up").length}</span></button>
                <button className={cx("btn sm", filter === "no-reply" ? "primary" : "ghost")} onClick={() => setFilter("no-reply")}>No reply <span className="meta" style={{ marginLeft: 4 }}>{SUPPLIER_ROWS.filter(([, st]) => st === "no-reply").length}</span></button>
              </div>
            </div>
          </div>
          <table className="table">
            <thead>
              <tr>
                <th>Supplier</th>
                <th>State</th>
                <th>Data points</th>
                <th>Last reply</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {visibleRows.map(([n, st, f, tot, c, lr]) => (
                <tr key={n} style={{ cursor: "pointer" }} onClick={() => setScreen(st === "flagged" || st === "follow-up" || st === "in-progress" ? "thread" : "audit")}>
                  <td><strong>{n}</strong></td>
                  <td>
                    <span className={cx("chip",
                      st === "complete" && "ok",
                      st === "in-progress" && "muted",
                      st === "follow-up" && "warn",
                      st === "flagged" && "warn",
                      st === "no-reply" && "muted")}>
                      {st}
                    </span>
                  </td>
                  <td>{f} / {tot}</td>
                  <td className="meta">{lr}</td>
                  <td className="row-action"><span className="row-arrow">→</span></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>

        <div className="col">
          <div className="card">
            <div className="eyebrow">Activity feed</div>
            <div className="mt-4">
              {[
                ["📨", "Sent first-touch to 47 suppliers", "12m ago", ""],
                ["✓", "TI replied · 5/5 data points parsed", "1h ago", "ok"],
                ["🤖", "Agent generated follow-up to STM", "2h ago", ""],
                ["⚠", "Flagged onsemi · 'unknown' twice", "4h ago", "warm"],
                ["📥", "ADI mill certificate parsed (PDF, 3 pp)", "5h ago", "ok"],
                ["📨", "Sent first-touch to 42 suppliers", "yesterday", ""],
              ].map(([ic, msg, when, kind], i) => (
                <div className="feed-item" key={i}>
                  <span className={cx("icon-tile", kind)}>{ic}</span>
                  <div>{msg}</div>
                  <div className="when">{when}</div>
                </div>
              ))}
            </div>
          </div>

          <div className="card spotlight">
            <div className="eyebrow acc">Needs your attention</div>
            <h3 className="h-2 mt-4">9 suppliers need you</h3>
            <ul className="text mt-4" style={{ paddingLeft: 18, margin: "8px 0 4px", lineHeight: 1.6 }}>
              <li><strong>5</strong> non-responsive after 3 follow-ups — ready to escalate</li>
              <li><strong>2</strong> escalated by the agent (out-of-scope clarification questions)</li>
              <li><strong>2</strong> bounced — bad email on file</li>
            </ul>
            <button className="btn primary mt-4" onClick={() => setScreen("thread")}>Open review queue →</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// =========================================================================
// Thread
// =========================================================================
function ScreenThread({ setScreen }) {
  const [editing, setEditing] = useState(false);
  const [attestOpen, setAttestOpen] = useState(false);
  const [draft, setDraft] = useState(
    `Thanks Marie — got the workbook. Three quick things to close out:

1. Country of smelt — we need smelter-level (not foundry). Mill cert page 1 usually has it.
2. Copper content (% by total weight) — approximate is fine.
3. Mill cert page 2 references a separate lot — could you confirm whether the same origins apply, or send the matching cert?

If a fresh mill cert isn't easy to pull, [[ATT]] one-page attestation pre-filled with what we already have. You can sign it digitally in your browser — or, if you prefer, the same form is attached as a PDF for offline signature.

Just reply with whatever you have.`
  );

  return (
    <div data-screen-label="thread">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Supplier thread</div>
          <h1 className="screen-title">STMicroelectronics · Section 232</h1>
          <p className="screen-sub">
            DigiKey · 11 SKUs · day 4 of 14 · 2 of 5 data points mapped
          </p>
        </div>
        <button className="btn ghost sm" onClick={() => setScreen("dash")}>← Dashboard</button>
      </div>

      <div style={{ maxWidth: 760, margin: "0 auto" }}>
        <Msg
          dir="out"
          when="day 0 · May 12"
          who="Sluicebox → tariff@st.com"
          subject="DigiKey · Section 232 origin trace — 11 SKUs"
          body={
            <>
              Hi STM — DigiKey is gathering smelt / diffusion / assembly country and
              Cu content for ~250 supplier lines ahead of the new Section 232
              weight-based rule. For each of the 11 attached SKUs… <em style={{ color: "var(--fg-3)" }}>(collapsed)</em>
            </>
          }
          dataNote="Asked for: smelt · diffusion · assembly · Cu % · mill cert"
        />

        <Msg
          dir="in"
          when="day 3 · May 15"
          who="Marie at STM → Sluicebox"
          subject="Re: DigiKey · Section 232 origin trace"
          body={
            <>
              Please find attached <span className="hl">stm_origin_q2.xlsx</span>. Smelter
              country we don't always track at the part level — let me know if
              foundry-level is enough. —Marie
            </>
          }
          attachments={["stm_origin_q2.xlsx", "STM_mill_certs.pdf (4 pp)"]}
          extracted={[
            ["country_of_diffusion", "France (Crolles)", "xlsx · D2"],
            ["country_of_assembly", "Morocco", "xlsx · E2"],
          ]}
          gaps={[
            ["country_of_smelt", "supplier said foundry-level only"],
            ["cu_content_pct", "column not in workbook"],
            ["mill_certificate · pg 2", "references a separate lot"],
          ]}
        />

        <div className="msg draft">
          <div className="row between">
            <div>
              <span className="chip acc">📝 Draft · ready to send</span>
              <span className="meta" style={{ marginLeft: 10 }}>day 4 · follow-up · scheduled in 2h</span>
            </div>
            {!editing && (
              <button className="btn ghost sm" onClick={() => setEditing(true)}>✎ Edit</button>
            )}
          </div>

          <div className="meta mt-4">
            <strong>To:</strong> tariff@st.com &nbsp;·&nbsp;
            <strong>Subject:</strong> Re: Section 232 origin trace — 3 quick clarifications
          </div>

          {!editing ? (
            <div
              className="body"
              style={{ marginTop: 12, whiteSpace: "pre-wrap", cursor: "text" }}
              onClick={() => setEditing(true)}
            >
              {draft.split("[[ATT]]").map((part, i, arr) => (
                <Fragment key={i}>
                  {part}
                  {i < arr.length - 1 && (
                    <a
                      href="#"
                      onClick={(e) => { e.preventDefault(); e.stopPropagation(); setAttestOpen(true); }}
                      style={{
                        color: "var(--sb-ultra-violet)",
                        textDecoration: "underline",
                        fontWeight: 600,
                      }}
                    >we've attached a one-page attestation</a>
                  )}
                </Fragment>
              ))}
            </div>
          ) : (
            <textarea
              autoFocus
              className="field area"
              style={{ marginTop: 12, minHeight: 220 }}
              value={draft}
              onChange={(e) => setDraft(e.target.value)}
            />
          )}

          <div
            style={{
              display: "flex", alignItems: "center", gap: 10,
              width: "100%", marginTop: 12, padding: "10px 12px",
              background: "var(--bg-soft)", border: "1px solid var(--bd-1)",
              borderRadius: 10,
            }}
          >
            <span style={{ fontSize: 18 }}>📎</span>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 13, fontWeight: 600 }}>STM_attestation_STM32F407VGT6.pdf</div>
              <div className="meta" style={{ fontSize: 11, marginTop: 2 }}>
                Pre-filled 1-pager · sign digitally via the link above, or print and sign offline
              </div>
            </div>
          </div>

          <hr className="divider" />

          <div className="row between" style={{ flexWrap: "wrap", gap: 8 }}>
            <span className="meta">
              Asks for the <strong>3 open data points</strong> only · follow-up <strong>1 / 3</strong>
            </span>
            <div className="row" style={{ gap: 6 }}>
              {editing && <button className="btn ghost sm" onClick={() => setEditing(false)}>Done editing</button>}
              <button className="btn ghost sm">⏸ Hold</button>
              <button className="btn primary sm">📨 Send now</button>
            </div>
          </div>
        </div>

        <div className="meta text-center mt-6">
          ⏳ Awaiting reply · auto-escalates if no response by follow-up #3
        </div>
      </div>

      {attestOpen && <AttestationModal onClose={() => setAttestOpen(false)} />}
    </div>
  );
}

// ---------------------------------------------------------------
// Attestation modal — supplier signs a 1-pager in browser
// ---------------------------------------------------------------
function AttestationModal({ onClose }) {
  const [smelt, setSmelt] = useState("France");
  const [diff, setDiff] = useState("France (Crolles)");
  const [asm, setAsm] = useState("Morocco (Bouskoura)");
  const [cu, setCu] = useState("~ 1.0");
  const [signature, setSignature] = useState("");
  const [signed, setSigned] = useState(false);
  const [signedAt, setSignedAt] = useState(null);

  const sign = () => {
    if (!signature.trim()) return;
    setSigned(true);
    setSignedAt(new Date().toLocaleString("en-US", { dateStyle: "long", timeStyle: "short" }));
  };

  const inputStyle = {
    border: "none", borderBottom: "1px dashed #999", background: "#fffceb",
    padding: "2px 6px", fontFamily: "Georgia, serif", fontSize: 13,
    color: "#1a1a1a", minWidth: 200,
  };

  return (
    <div
      onClick={onClose}
      style={{
        position: "fixed", inset: 0, zIndex: 1000,
        background: "rgba(23, 0, 59, 0.6)", backdropFilter: "blur(8px)",
        display: "flex", alignItems: "center", justifyContent: "center", padding: 32,
      }}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          width: "min(820px, 100%)", maxHeight: "calc(100vh - 64px)",
          background: "var(--bg-1)", borderRadius: 14, border: "1px solid var(--bd-1)",
          boxShadow: "0 30px 80px rgba(0,0,0,.4)",
          display: "flex", flexDirection: "column", overflow: "hidden",
        }}
      >
        {/* Window chrome */}
        <div style={{
          display: "flex", alignItems: "center", gap: 12,
          padding: "10px 14px", borderBottom: "1px solid var(--bd-1)", background: "var(--bg-soft)",
        }}>
          <div style={{ display: "flex", gap: 6 }}>
            <span style={{ width: 12, height: 12, borderRadius: 6, background: "#ff5f57" }} />
            <span style={{ width: 12, height: 12, borderRadius: 6, background: "#febc2e" }} />
            <span style={{ width: 12, height: 12, borderRadius: 6, background: "#28c840" }} />
          </div>
          <div className="mono" style={{ fontSize: 13, color: "var(--fg-2)", flex: 1, textAlign: "center" }}>
            STM_attestation_STM32F407VGT6.pdf
          </div>
          <button onClick={onClose} style={{
            background: "transparent", border: "none", cursor: "pointer",
            color: "var(--fg-2)", fontSize: 18, lineHeight: 1, padding: 4,
          }}>✕</button>
        </div>

        {/* Document surface */}
        <div style={{ flex: 1, overflow: "auto", padding: 24, background: "#525659" }}>
          <div style={{
            maxWidth: 700, margin: "0 auto", background: "#fff", color: "#1a1a1a",
            padding: "44px 52px", boxShadow: "0 4px 16px rgba(0,0,0,.3)",
            fontFamily: "Georgia, serif", fontSize: 13, lineHeight: 1.55,
          }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", borderBottom: "2px solid #1a1a1a", paddingBottom: 12, marginBottom: 20 }}>
              <div>
                <div style={{ fontSize: 18, fontWeight: 700, letterSpacing: 0.5 }}>STMICROELECTRONICS</div>
                <div style={{ fontSize: 11, color: "#666", marginTop: 2 }}>Supplier Origin Attestation</div>
              </div>
              <div style={{ fontSize: 11, color: "#666", textAlign: "right" }}>
                Ref. STM-2026-Q2-04827<br />
                Requested by: DigiKey
              </div>
            </div>

            <h2 style={{ fontSize: 15, margin: "0 0 12px", textAlign: "center", letterSpacing: 1 }}>
              ORIGIN ATTESTATION — IN LIEU OF MILL CERTIFICATE
            </h2>

            <p style={{ margin: "0 0 12px" }}>
              Please review and confirm the part-level origins below. Where we
              already had a value (from <span className="mono">stm_origin_q2.xlsx</span> or prior
              certs), it has been pre-filled. Edit anything that's wrong, then sign
              at the bottom — no mill-cert PDF needed.
            </p>

            <table style={{ width: "100%", borderCollapse: "collapse", margin: "16px 0", fontSize: 13 }}>
              <tbody>
                <tr><td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd", width: 200, color: "#666" }}>Part Number</td><td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd", fontFamily: "monospace" }}>STM32F407VGT6</td></tr>
                <tr><td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd", color: "#666" }}>Lot</td><td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd", fontFamily: "monospace" }}>L-2026-Q2-STM-04827</td></tr>
                <tr>
                  <td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd", color: "#666" }}>Country of Smelt</td>
                  <td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd" }}>
                    <input type="text" value={smelt} onChange={(e) => setSmelt(e.target.value)} disabled={signed} style={inputStyle} />
                  </td>
                </tr>
                <tr>
                  <td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd", color: "#666" }}>Diffusion Site</td>
                  <td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd" }}>
                    <input type="text" value={diff} onChange={(e) => setDiff(e.target.value)} disabled={signed} style={inputStyle} />
                  </td>
                </tr>
                <tr>
                  <td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd", color: "#666" }}>Assembly Site</td>
                  <td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd" }}>
                    <input type="text" value={asm} onChange={(e) => setAsm(e.target.value)} disabled={signed} style={inputStyle} />
                  </td>
                </tr>
                <tr>
                  <td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd", color: "#666" }}>Cu Content (% wt.)</td>
                  <td style={{ padding: "8px 8px", borderBottom: "1px solid #ddd" }}>
                    <input type="text" value={cu} onChange={(e) => setCu(e.target.value)} disabled={signed} style={inputStyle} />
                  </td>
                </tr>
              </tbody>
            </table>

            <h3 style={{ fontSize: 13, margin: "20px 0 8px", padding: "6px 10px", background: "#fff3cd", border: "2px solid #ffc107", display: "inline-block" }}>
              ▸ Supplier Attestation
            </h3>
            <p style={{ margin: "0 0 10px", padding: "10px 12px", background: "#fffbe6", borderLeft: "3px solid #ffc107" }}>
              The undersigned, on behalf of STMicroelectronics, certifies that the
              origins disclosed above are accurate to the best of their knowledge
              and consistent with internal QA records. This attestation is provided
              in lieu of a mill certificate where an updated cert is not readily
              available.
            </p>

            {/* Signature */}
            <div style={{ marginTop: 28 }}>
              <div style={{ fontSize: 11, color: "#666", marginBottom: 4, textTransform: "uppercase", letterSpacing: 0.5 }}>
                Authorized signature
              </div>
              {signed ? (
                <>
                  <div style={{
                    borderBottom: "1px solid #1a1a1a", paddingBottom: 2,
                    fontFamily: "'Brush Script MT', 'Snell Roundhand', cursive",
                    fontSize: 28, color: "#1a3d8f", minHeight: 40, maxWidth: 320,
                  }}>{signature}</div>
                  <div style={{ fontSize: 11, color: "#666", marginTop: 4 }}>
                    Marie Laurent · QA Manager · STMicroelectronics<br />
                    Signed {signedAt}
                  </div>
                </>
              ) : (
                <>
                  <input
                    type="text"
                    value={signature}
                    onChange={(e) => setSignature(e.target.value)}
                    placeholder="Type full name to sign"
                    style={{
                      width: 320, padding: "6px 8px", fontSize: 16,
                      border: "1px dashed #999", background: "#fffceb",
                      fontFamily: "'Brush Script MT', cursive",
                      color: "#1a3d8f",
                    }}
                  />
                  <div style={{ fontSize: 11, color: "#666", marginTop: 4 }}>
                    Marie Laurent · QA Manager · STMicroelectronics
                  </div>
                </>
              )}
            </div>

            {signed && (
              <div style={{
                marginTop: 24, padding: "10px 14px", background: "#e6f4ea",
                border: "1px solid #34a853", color: "#1e6b3a", fontSize: 12,
              }}>
                ✓ Attestation signed and cryptographically sealed. SHA-256:{" "}
                <span style={{ fontFamily: "monospace" }}>9d2c…a417</span>
              </div>
            )}

            <div style={{ marginTop: 24, fontSize: 10, color: "#999", textAlign: "center", borderTop: "1px solid #ddd", paddingTop: 8 }}>
              Sluicebox Origin Trace · Page 1 of 1 · Returns automatically to DigiKey upon signing
            </div>
          </div>
        </div>

        {/* Footer */}
        <div style={{
          padding: "12px 16px", borderTop: "1px solid var(--bd-1)",
          background: "var(--bg-soft)", display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12,
        }}>
          <div className="meta">
            {signed
              ? "Signed · auto-returned to DigiKey"
              : "Edit values if needed, then type your name to sign"}
          </div>
          <div className="row" style={{ gap: 8 }}>
            <button className="btn ghost sm" onClick={onClose}>Close</button>
            {!signed
              ? <button className="btn primary sm" disabled={!signature.trim()} style={{ opacity: signature.trim() ? 1 : 0.5, cursor: signature.trim() ? "pointer" : "not-allowed" }} onClick={sign}>Sign attestation</button>
              : <button className="btn primary sm" onClick={onClose}>↓ Download signed PDF</button>}
          </div>
        </div>
      </div>
    </div>
  );
}

function Msg({ dir, when, who, subject, body, attachments, extracted, gaps, dataNote }) {
  const isOut = dir === "out";
  return (
    <div className={cx("msg", isOut ? "out" : "in")}>
      <div className="row between">
        <span className={cx("chip", isOut ? "acc" : "muted")}>
          {isOut ? "📨 outbound" : "📥 inbound"}
        </span>
        <span className="meta">{when}</span>
      </div>
      <div className="who">{who}</div>
      <div className="subject">{subject}</div>
      <div className="body">{body}</div>

      {attachments && (
        <div className="row mt-4" style={{ gap: 6, flexWrap: "wrap" }}>
          {attachments.map((a) => <span key={a} className="chip muted">📎 {a}</span>)}
        </div>
      )}

      {dataNote && (
        <p className="meta mt-4" style={{ fontStyle: "italic" }}>{dataNote}</p>
      )}

      {(extracted || gaps) && (
        <div className="extracted">
          <div className="eyebrow acc">🧠 What the agent got from this message</div>
          {extracted && extracted.length > 0 && (
            <div className="col mt-4" style={{ gap: 6 }}>
              {extracted.map(([k, v, src]) => (
                <div className="row between" key={k}>
                  <span className="mono">{k}</span>
                  <span className="row" style={{ gap: 8 }}>
                    <strong>{v}</strong>
                    <span className="meta">{src}</span>
                  </span>
                </div>
              ))}
            </div>
          )}
          {gaps && gaps.length > 0 && (
            <div className="col mt-4" style={{ gap: 6 }}>
              <div className="meta">Still open:</div>
              {gaps.map(([k, why]) => (
                <div className="row between" key={k}>
                  <span className="mono">{k}</span>
                  <span className="meta">{why}</span>
                </div>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// =========================================================================
// Audit
// =========================================================================
const AUDIT_ROWS = [
  ["TI", "SN74AHC1G14DBVR", ["USA", 0.96], ["Taiwan", 0.94], ["Philippines", 0.92], ["3.4", 0.88], ["✓", 0.95]],
  ["ADI", "AD8232ACPZ-R7", ["USA", 0.91], ["Ireland", 0.89], ["Thailand", 0.87], ["1.2", 0.84], ["✓", 0.91]],
  ["NXP", "MK22FN512VLH12", ["Mexico", 0.74], ["USA", 0.81], ["Malaysia", 0.78], ["—", null], ["—", null]],
  ["Infineon", "IRFZ44N", ["Austria", 0.71], ["Germany", 0.66], ["Indonesia", 0.62], ["8.7", 0.58], ["✓", 0.74]],
  ["STM", "STM32F407VGT6", ["—", null], ["France", 0.94], ["Morocco", 0.92], ["—", null], ["~", 0.41]],
  ["onsemi", "FQP30N06L", ["?", 0.34], ["?", 0.31], ["?", 0.28], ["—", null], ["—", null]],
];

function confChip(c) {
  if (c == null) return <span className="chip muted">—</span>;
  if (c >= 0.85) return <span className="chip ok">{c}</span>;
  if (c >= 0.6) return <span className="chip acc">{c}</span>;
  return <span className="chip warn">⚠ {c}</span>;
}

function ScreenAudit({ setScreen }) {
  const [picked, setPicked] = useState({ row: 1, col: 0 });
  const [openMenu, setOpenMenu] = useState(null);
  const [sourceOpen, setSourceOpen] = useState(null); // 'excel' | 'pdf' | null
  const [auditPkg, setAuditPkg] = useState(null); // null | "campaign" | rowIdx
  const cell = AUDIT_ROWS[picked.row][2 + picked.col];
  const supplier = AUDIT_ROWS[picked.row][0];
  const part = AUDIT_ROWS[picked.row][1];
  const fieldLabels = ["smelt", "diffusion", "assembly", "Cu%", "mill cert"];
  // Source type per column: mill cert → PDF; everything else → spreadsheet.
  const sourceKind = picked.col === 4 ? "pdf" : "excel";
  const filename = sourceKind === "pdf"
    ? `${supplier.toLowerCase()}_${part}_mill_cert.pdf`
    : `${supplier.toLowerCase()}_q2_origin.xlsx`;

  useEffect(() => {
    const close = () => setOpenMenu(null);
    if (openMenu != null) {
      window.addEventListener("click", close);
      return () => window.removeEventListener("click", close);
    }
  }, [openMenu]);

  return (
    <div data-screen-label="audit">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Trust artifact</div>
          <h1 className="screen-title">Audit & export</h1>
          <p className="screen-sub">
            Every value carries a clickable provenance trail — message, doc, page,
            cell. <span className="hl">Without this</span> regulated customers
            can't ship the output.
          </p>
        </div>
        <div className="col" style={{ alignItems: "flex-end", gap: 8 }}>
          <button className="btn quiet sm">+ Upload customer template to fill</button>
        </div>
      </div>

      <div className="cols-wide">
        <div className="card" style={{ padding: 0 }}>
          <div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border-1)" }}>
            <div className="row between" style={{ position: "relative" }}>
              <h3 className="h-2">digikey_tariff_exposure_v3.xlsx</h3>
              <button
                className="btn primary sm"
                onClick={(e) => { e.stopPropagation(); setOpenMenu(openMenu === "all" ? null : "all"); }}
              >↓ Download</button>
              {openMenu === "all" && (
                <div
                  onClick={(e) => e.stopPropagation()}
                  style={{
                    position: "absolute",
                    top: "calc(100% + 6px)",
                    right: 0,
                    background: "white",
                    border: "1px solid var(--border-1)",
                    borderRadius: "var(--radius-md)",
                    boxShadow: "var(--shadow-2)",
                    padding: 6,
                    minWidth: 240,
                    zIndex: 50,
                  }}
                >
                  <div className="eyebrow" style={{ padding: "6px 10px 4px" }}>Entire campaign</div>
                  {[
                    ["↗ Audit package (PDF)", "signed attestation + sources", () => { setAuditPkg("campaign"); setOpenMenu(null); }],
                    ["↗ Full table as JSON", "with provenance"],
                    ["↗ Full table as CSV", "flat values"],
                    ["↗ DigiKey workbook", "customer template"],
                    ["↗ Source bundle (.zip)", "all attachments + threads"],
                    ["↗ Audit JSON", "every cell + provenance"],
                  ].map(([t, sub, onClick]) => (
                    <button
                      key={t}
                      className="btn quiet sm"
                      style={{ display: "block", width: "100%", textAlign: "left", padding: "8px 10px", borderRadius: "var(--radius-sm)" }}
                      onClick={onClick || (() => setOpenMenu(null))}
                    >
                      <div style={{ fontSize: 13 }}>{t}</div>
                      <div className="meta" style={{ fontSize: 11 }}>{sub}</div>
                    </button>
                  ))}
                </div>
              )}
            </div>
          </div>
          <table className="table">
            <thead>
              <tr>
                <th>Supplier</th>
                <th>SKU</th>
                <th>B · Smelt</th>
                <th>C · Diffusion</th>
                <th>D · Assembly</th>
                <th>G · Cu %</th>
                <th>H · Mill cert</th>
                <th style={{ width: 60, textAlign: "right" }}></th>
              </tr>
            </thead>
            <tbody>
              {AUDIT_ROWS.map((r, ri) => (
                <tr key={ri}>
                  <td><strong>{r[0]}</strong></td>
                  <td className="mono" style={{ fontSize: 12, color: "var(--fg-2)" }}>{r[1]}</td>
                  {r.slice(2).map((c, ci) => {
                    const isPicked = picked.row === ri && picked.col === ci;
                    return (
                      <td
                        key={ci}
                        className={cx("audit-cell", isPicked && "picked")}
                        onClick={() => setPicked({ row: ri, col: ci })}
                      >
                        <div className="cell-val">{c[0]}</div>
                      </td>
                    );
                  })}
                  <td style={{ position: "relative", textAlign: "right" }}>
                    <button
                      className="btn ghost sm"
                      onClick={(e) => { e.stopPropagation(); setOpenMenu(openMenu === ri ? null : ri); }}
                    >↓</button>
                    {openMenu === ri && (
                      <div
                        onClick={(e) => e.stopPropagation()}
                        style={{
                          position: "absolute",
                          top: "calc(100% - 4px)",
                          right: 8,
                          background: "white",
                          border: "1px solid var(--border-1)",
                          borderRadius: "var(--radius-md)",
                          boxShadow: "var(--shadow-2)",
                          padding: 6,
                          minWidth: 220,
                          zIndex: 50,
                          textAlign: "left",
                        }}
                      >
                        <div className="eyebrow" style={{ padding: "6px 10px 4px" }}>
                          {r[0]} · download
                        </div>
                        {[
                          ["↗ Audit package (PDF)", "signed attestation + sources", () => { setAuditPkg(ri); setOpenMenu(null); }],
                          ["↗ Row as JSON", "with provenance"],
                          ["↗ Row as CSV", "flat values"],
                          ["↗ DigiKey workbook row", "customer template"],
                          ["↗ Source bundle (.zip)", "all attachments + thread"],
                          ["↗ Audit JSON", "every cell + provenance"],
                        ].map(([t, sub, onClick]) => (
                          <button
                            key={t}
                            className="btn quiet sm"
                            style={{
                              display: "block",
                              width: "100%",
                              textAlign: "left",
                              padding: "8px 10px",
                              borderRadius: "var(--radius-sm)",
                            }}
                            onClick={onClick || (() => setOpenMenu(null))}
                          >
                            <div style={{ fontSize: 13 }}>{t}</div>
                            <div className="meta" style={{ fontSize: 11 }}>{sub}</div>
                          </button>
                        ))}
                      </div>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>

        <div className="col">
          <div className="card">
            <div className="eyebrow">Source preview</div>
            <h3 className="h-2 mt-4">
              {AUDIT_ROWS[picked.row][0]} · {["smelt", "diffusion", "assembly", "Cu%", "mill cert"][picked.col]}
            </h3>
            <hr className="divider" />
            <div className="eyebrow">Value</div>
            <div className="metric-num" style={{ fontSize: 36, marginTop: 8 }}>
              {cell[0]}
            </div>
            <hr className="divider" />
            <div className="eyebrow">Source</div>
            <button
              type="button"
              onClick={() => setSourceOpen(sourceKind)}
              style={{
                display: "block", width: "100%", textAlign: "left",
                background: "var(--bg-soft)", border: "1px solid var(--bd-1)",
                borderRadius: 10, padding: "12px 14px", marginTop: 12,
                cursor: "pointer", transition: "all .15s",
              }}
              onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--sb-ultra-violet)"; e.currentTarget.style.background = "var(--bg-2)"; }}
              onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--bd-1)"; e.currentTarget.style.background = "var(--bg-soft)"; }}
            >
              {sourceKind === "excel" ? (
                <>
                  <div className="text">📥 <strong>Inbound message</strong> · day 2 from <em>supply@{supplier.toLowerCase().replace(/\s+/g, "")}.com</em></div>
                  <div className="text" style={{ marginTop: 6 }}>
                    📎 <span className="hl mono">{filename}</span> · sheet <span className="mono">"Origin"</span> · cell <span className="mono">{["B12", "C12", "D12", "E12", "F12"][picked.col]}</span>
                  </div>
                  <div className="meta" style={{ marginTop: 8, color: "var(--sb-ultra-violet)" }}>↗ Open spreadsheet</div>
                </>
              ) : (
                <>
                  <div className="text">📥 <strong>Inbound message</strong> · day 2 from <em>supply@{supplier.toLowerCase().replace(/\s+/g, "")}.com</em></div>
                  <div className="text" style={{ marginTop: 6 }}>
                    📎 <span className="hl mono">{filename}</span> · page <span className="mono">1</span> · §<span className="mono">"Mill Certification"</span>
                  </div>
                  <div className="meta" style={{ marginTop: 8, color: "var(--sb-ultra-violet)" }}>↗ Open PDF</div>
                </>
              )}
            </button>
            <hr className="divider" />
            <div className="eyebrow">Extraction</div>
            <p className="text mt-4">
              {sourceKind === "excel" ? (
                <>Agent extracted <span className="hl">{cell[0]}</span> via ISO-3166-2 normalization;
                facility detail stripped. Matched
                <em> "{["smelter location", "diffusion site", "assembly site", "Cu content %", "mill cert"][picked.col]}"</em> ↔ <em> "{fieldLabels[picked.col]}"</em> automatically.</>
              ) : (
                <>Agent verified <span className="hl">{cell[0]}</span> by locating the
                <em> "Mill Certification"</em> section header and the supplier's signature/stamp on page 1.</>
              )}
            </p>
            <button className="btn ghost sm mt-4">↗ Open source thread</button>
          </div>
        </div>
      </div>

      {sourceOpen && (
        <SourcePreviewModal
          kind={sourceOpen}
          supplier={supplier}
          part={part}
          filename={filename}
          col={picked.col}
          value={cell[0]}
          onClose={() => setSourceOpen(null)}
        />
      )}

      {auditPkg !== null && (
        <AuditPackageModal
          scope={auditPkg}
          onClose={() => setAuditPkg(null)}
        />
      )}
    </div>
  );
}

// ---------------------------------------------------------------
// Source preview modal — Excel / PDF
// ---------------------------------------------------------------
function SourcePreviewModal({ kind, supplier, part, filename, col, value, onClose }) {
  return (
    <div
      onClick={onClose}
      style={{
        position: "fixed", inset: 0, zIndex: 1000,
        background: "rgba(23, 0, 59, 0.6)", backdropFilter: "blur(8px)",
        display: "flex", alignItems: "center", justifyContent: "center",
        padding: 32,
      }}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          width: "min(960px, 100%)", maxHeight: "calc(100vh - 64px)",
          background: "var(--bg-1)", borderRadius: 14,
          border: "1px solid var(--bd-1)",
          boxShadow: "0 30px 80px rgba(0,0,0,.4)",
          display: "flex", flexDirection: "column", overflow: "hidden",
        }}
      >
        {/* Window chrome */}
        <div style={{
          display: "flex", alignItems: "center", gap: 12,
          padding: "10px 14px", borderBottom: "1px solid var(--bd-1)",
          background: "var(--bg-soft)",
        }}>
          <div style={{ display: "flex", gap: 6 }}>
            <span style={{ width: 12, height: 12, borderRadius: 6, background: "#ff5f57" }} />
            <span style={{ width: 12, height: 12, borderRadius: 6, background: "#febc2e" }} />
            <span style={{ width: 12, height: 12, borderRadius: 6, background: "#28c840" }} />
          </div>
          <div className="mono" style={{ fontSize: 13, color: "var(--fg-2)", flex: 1, textAlign: "center" }}>
            {filename}
          </div>
          <button
            onClick={onClose}
            style={{
              background: "transparent", border: "none", cursor: "pointer",
              color: "var(--fg-2)", fontSize: 18, lineHeight: 1, padding: 4,
            }}
          >✕</button>
        </div>

        {kind === "excel"
          ? <ExcelPreview supplier={supplier} col={col} value={value} />
          : <PdfPreview supplier={supplier} part={part} value={value} />}
      </div>
    </div>
  );
}

function ExcelPreview({ supplier, col, value }) {
  // Highlighted cell coords
  const colLetters = ["A", "B", "C", "D", "E", "F"];
  const targetCell = ["B12", "C12", "D12", "E12", "F12"][col];
  const targetCol = ["B", "C", "D", "E", "F"][col];
  const headers = ["Part Number", "Country of smelt", "Diffusion site", "Assembly site", "Cu content %", "Mill cert"];
  const rows = [
    ["", "", "", "", "", ""],
    [`${supplier} — Q2 2026 Origin Workbook`, "", "", "", "", ""],
    ["Sheet: Origin", "", "", "", "", ""],
    ["", "", "", "", "", ""],
    headers,
    ["AD8232ACPZ-R7", "USA", "Ireland", "Thailand", "1.2", "✓"],
    ["AD9361BBCZ", "USA", "Taiwan", "Philippines", "0.9", "✓"],
    ["ADM3251EARWZ", "USA", "Ireland", "Thailand", "1.4", "✓"],
    ["AD7768BSTZ", "USA", "Ireland", "Malaysia", "1.1", "✓"],
    ["LTC2378-20", "USA", "Taiwan", "Thailand", "1.3", "✓"],
    ["LT8640SIV", "USA", "Taiwan", "Philippines", "1.0", "✓"],
    ["AD8232ACPZ", "USA — Westborough", "Ireland — Limerick", "Thailand — Ayutthaya", "1.2", "✓"],
  ];

  return (
    <div style={{ display: "flex", flexDirection: "column", flex: 1, overflow: "hidden", background: "#fff", color: "#1a1a1a" }}>
      {/* Toolbar */}
      <div style={{ padding: "6px 12px", borderBottom: "1px solid #e0e0e0", background: "#f3f3f3", display: "flex", alignItems: "center", gap: 12, fontSize: 12 }}>
        <span style={{ fontWeight: 600 }}>📊 Excel</span>
        <span style={{ color: "#666" }}>File · Home · Insert · Data · Review</span>
      </div>
      {/* Formula bar */}
      <div style={{ padding: "6px 12px", borderBottom: "1px solid #e0e0e0", background: "#fafafa", display: "flex", alignItems: "center", gap: 8, fontSize: 13 }}>
        <span className="mono" style={{ width: 60, padding: "2px 8px", border: "1px solid #ccc", background: "#fff" }}>{targetCell}</span>
        <span style={{ color: "#999" }}>fx</span>
        <span className="mono" style={{ flex: 1, padding: "2px 8px", border: "1px solid #ccc", background: "#fff" }}>
          {col === 4 ? value : `${value}${col === 0 ? " — Westborough" : col === 1 ? " — Limerick" : col === 2 ? " — Ayutthaya" : ""}`}
        </span>
      </div>
      {/* Sheet */}
      <div style={{ flex: 1, overflow: "auto" }}>
        <table style={{ borderCollapse: "collapse", fontSize: 13, width: "100%" }}>
          <thead>
            <tr>
              <th style={{ width: 40, background: "#f3f3f3", border: "1px solid #d4d4d4", padding: "4px 6px", color: "#666", fontWeight: 500 }}></th>
              {colLetters.map((L) => (
                <th key={L} style={{ background: "#f3f3f3", border: "1px solid #d4d4d4", padding: "4px 8px", color: "#666", fontWeight: 500, minWidth: 130, textAlign: "center" }}>{L}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {rows.map((r, ri) => {
              const rowNum = ri + 1;
              const isHeaderRow = ri === 4;
              const isTargetRow = rowNum === 12;
              return (
                <tr key={ri}>
                  <td style={{ background: "#f3f3f3", border: "1px solid #d4d4d4", padding: "4px 6px", color: "#666", textAlign: "center", fontWeight: 500 }}>{rowNum}</td>
                  {r.map((v, ci) => {
                    const isTarget = isTargetRow && colLetters[ci] === targetCol;
                    return (
                      <td
                        key={ci}
                        style={{
                          border: isTarget ? "2px solid #217346" : "1px solid #d4d4d4",
                          padding: "4px 8px",
                          background: isTarget ? "#d4edda" : isHeaderRow ? "#e8f0fe" : ri === 1 ? "#fff" : "#fff",
                          fontWeight: isHeaderRow || (ri === 1 && ci === 0) ? 600 : 400,
                          fontSize: ri === 1 && ci === 0 ? 14 : 13,
                          color: isHeaderRow ? "#1a1a1a" : "#333",
                          whiteSpace: "nowrap",
                        }}
                      >
                        {v}
                      </td>
                    );
                  })}
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      {/* Sheet tabs */}
      <div style={{ padding: "6px 12px", borderTop: "1px solid #e0e0e0", background: "#f3f3f3", display: "flex", gap: 4, fontSize: 12 }}>
        <span style={{ padding: "4px 12px", background: "#fff", border: "1px solid #d4d4d4", borderBottom: "none", fontWeight: 600 }}>Origin</span>
        <span style={{ padding: "4px 12px", color: "#666" }}>Parts</span>
        <span style={{ padding: "4px 12px", color: "#666" }}>Notes</span>
      </div>
    </div>
  );
}

function PdfPreview({ supplier, part, value }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", flex: 1, overflow: "hidden", background: "#525659" }}>
      {/* PDF toolbar */}
      <div style={{ padding: "6px 12px", borderBottom: "1px solid #3a3d40", background: "#323639", display: "flex", alignItems: "center", gap: 12, fontSize: 12, color: "#ccc" }}>
        <span style={{ fontWeight: 600 }}>📄 PDF</span>
        <span style={{ color: "#999" }}>Page 1 of 3</span>
        <span style={{ flex: 1 }} />
        <span style={{ color: "#999" }}>100% · ↓ Download</span>
      </div>
      {/* Page */}
      <div style={{ flex: 1, overflow: "auto", padding: 24, display: "flex", justifyContent: "center" }}>
        <div style={{
          width: "100%", maxWidth: 700, background: "#fff", color: "#1a1a1a",
          padding: "40px 48px", boxShadow: "0 4px 16px rgba(0,0,0,.3)",
          fontFamily: "Georgia, serif", fontSize: 13, lineHeight: 1.55,
        }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", borderBottom: "2px solid #1a1a1a", paddingBottom: 12, marginBottom: 20 }}>
            <div>
              <div style={{ fontSize: 18, fontWeight: 700, letterSpacing: 0.5 }}>{supplier.toUpperCase()}</div>
              <div style={{ fontSize: 11, color: "#666", marginTop: 2 }}>Quality Assurance · Mill Certification</div>
            </div>
            <div style={{ fontSize: 11, color: "#666", textAlign: "right" }}>
              Cert No. {supplier.slice(0, 2).toUpperCase()}-2026-Q2-{Math.floor(Math.random() * 9000 + 1000)}<br />
              Issued: 14 May 2026
            </div>
          </div>

          <h2 style={{ fontSize: 16, margin: "0 0 12px", textAlign: "center", letterSpacing: 1 }}>
            CERTIFICATE OF MILL ORIGIN
          </h2>
          <p style={{ margin: "0 0 12px" }}>
            This document certifies that the part described below was manufactured
            from raw materials whose smelter, diffusion, and assembly origins are
            disclosed and traceable per ISO-9001 §7.5.3.
          </p>

          <table style={{ width: "100%", borderCollapse: "collapse", margin: "16px 0", fontSize: 12 }}>
            <tbody>
              <tr><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd", width: 180, color: "#666" }}>Part Number</td><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd", fontFamily: "monospace" }}>{part}</td></tr>
              <tr><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd", color: "#666" }}>Lot</td><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd", fontFamily: "monospace" }}>L-2026-Q2-04827</td></tr>
              <tr><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd", color: "#666" }}>Country of Smelt</td><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd" }}>USA — Westborough, MA</td></tr>
              <tr><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd", color: "#666" }}>Diffusion Site</td><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd" }}>Limerick, Ireland</td></tr>
              <tr><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd", color: "#666" }}>Assembly Site</td><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd" }}>Ayutthaya, Thailand</td></tr>
              <tr><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd", color: "#666" }}>Cu Content (% wt.)</td><td style={{ padding: "6px 8px", borderBottom: "1px solid #ddd" }}>1.2%</td></tr>
            </tbody>
          </table>

          <h3 style={{ fontSize: 13, margin: "20px 0 8px", padding: "6px 10px", background: "#fff3cd", border: "2px solid #ffc107", display: "inline-block" }}>
            ▸ Mill Certification
          </h3>
          <p style={{ margin: "0 0 10px", padding: "10px 12px", background: "#fffbe6", borderLeft: "3px solid #ffc107" }}>
            We hereby certify that the materials, processes, and origins disclosed
            herein are accurate to the best of our knowledge and are supported by
            internal QA records retained for a minimum of seven (7) years.
            <br /><br />
            <strong>Status: {value === "✓" ? "✓ CERTIFIED" : value}</strong>
          </p>

          <div style={{ marginTop: 32, display: "flex", justifyContent: "space-between", alignItems: "flex-end" }}>
            <div>
              <div style={{ borderTop: "1px solid #1a1a1a", paddingTop: 4, width: 200, fontSize: 11, color: "#666" }}>
                Authorized Signature
              </div>
              <div style={{ marginTop: 4, fontFamily: "'Brush Script MT', cursive", fontSize: 22, color: "#1a3d8f" }}>
                M. Henderson
              </div>
              <div style={{ fontSize: 11, color: "#666" }}>Director, Quality Assurance</div>
            </div>
            <div style={{
              border: "2px solid #c00", color: "#c00", padding: "8px 16px",
              transform: "rotate(-8deg)", fontWeight: 700, fontSize: 11, letterSpacing: 1,
              textAlign: "center",
            }}>
              QA APPROVED<br />
              <span style={{ fontSize: 9 }}>14 MAY 2026</span>
            </div>
          </div>

          <div style={{ marginTop: 24, fontSize: 10, color: "#999", textAlign: "center" }}>
            Page 1 of 3 · {supplier} Confidential · Do not distribute without authorization
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------
// Audit package modal — signed PDF attestation
// ---------------------------------------------------------------
function AuditPackageModal({ scope, onClose }) {
  const isCampaign = scope === "campaign";
  const row = !isCampaign ? AUDIT_ROWS[scope] : null;
  const supplier = row ? row[0] : null;
  const part = row ? row[1] : null;

  const [signature, setSignature] = useState("");
  const [signed, setSigned] = useState(false);
  const [signedAt, setSignedAt] = useState(null);

  const sign = () => {
    if (!signature.trim()) return;
    setSigned(true);
    setSignedAt(new Date().toLocaleString("en-US", { dateStyle: "long", timeStyle: "short" }));
  };

  const title = isCampaign ? "DigiKey · Q2 Origin Trace" : `${supplier} — ${part}`;
  const certNo = isCampaign
    ? "DK-2026-Q2-AUDIT-0142"
    : `${supplier?.slice(0, 2).toUpperCase()}-2026-Q2-${String(scope).padStart(4, "0")}`;

  // Data summary
  const fieldNames = ["Country of smelt", "Diffusion site", "Assembly site", "Cu content (% wt.)", "Mill cert"];
  const dataRows = isCampaign ? AUDIT_ROWS : [row];

  return (
    <div
      onClick={onClose}
      style={{
        position: "fixed", inset: 0, zIndex: 1000,
        background: "rgba(23, 0, 59, 0.6)", backdropFilter: "blur(8px)",
        display: "flex", alignItems: "center", justifyContent: "center", padding: 32,
      }}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          width: "min(900px, 100%)", maxHeight: "calc(100vh - 64px)",
          background: "var(--bg-1)", borderRadius: 14, border: "1px solid var(--bd-1)",
          boxShadow: "0 30px 80px rgba(0,0,0,.4)",
          display: "flex", flexDirection: "column", overflow: "hidden",
        }}
      >
        {/* Window chrome */}
        <div style={{
          display: "flex", alignItems: "center", gap: 12,
          padding: "10px 14px", borderBottom: "1px solid var(--bd-1)", background: "var(--bg-soft)",
        }}>
          <div style={{ display: "flex", gap: 6 }}>
            <span style={{ width: 12, height: 12, borderRadius: 6, background: "#ff5f57" }} />
            <span style={{ width: 12, height: 12, borderRadius: 6, background: "#febc2e" }} />
            <span style={{ width: 12, height: 12, borderRadius: 6, background: "#28c840" }} />
          </div>
          <div className="mono" style={{ fontSize: 13, color: "var(--fg-2)", flex: 1, textAlign: "center" }}>
            audit_package_{isCampaign ? "campaign" : (supplier || "").toLowerCase().replace(/\s+/g, "_")}.pdf
          </div>
          <button onClick={onClose} style={{
            background: "transparent", border: "none", cursor: "pointer",
            color: "var(--fg-2)", fontSize: 18, lineHeight: 1, padding: 4,
          }}>✕</button>
        </div>

        {/* PDF surface */}
        <div style={{ flex: 1, overflow: "auto", padding: 24, background: "#525659" }}>
          <div style={{
            maxWidth: 760, margin: "0 auto", background: "#fff", color: "#1a1a1a",
            padding: "44px 52px", boxShadow: "0 4px 16px rgba(0,0,0,.3)",
            fontFamily: "Georgia, serif", fontSize: 13, lineHeight: 1.55,
          }}>
            {/* Header */}
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", borderBottom: "2px solid #1a1a1a", paddingBottom: 12, marginBottom: 20 }}>
              <div>
                <div style={{ fontSize: 18, fontWeight: 700, letterSpacing: 0.5 }}>SLUICEBOX</div>
                <div style={{ fontSize: 11, color: "#666", marginTop: 2 }}>Origin Trace · Audit Attestation Package</div>
              </div>
              <div style={{ fontSize: 11, color: "#666", textAlign: "right" }}>
                Cert No. {certNo}<br />
                Issued: {new Date().toLocaleDateString("en-US", { dateStyle: "long" })}
              </div>
            </div>

            <h1 style={{ fontSize: 18, margin: "0 0 6px", textAlign: "center", letterSpacing: 1 }}>
              AUDIT ATTESTATION
            </h1>
            <h2 style={{ fontSize: 13, margin: "0 0 18px", textAlign: "center", color: "#666", fontWeight: 400, letterSpacing: 1 }}>
              {isCampaign ? "Campaign-level · all suppliers" : "Line item · single part"}
            </h2>

            <p style={{ margin: "0 0 12px" }}>
              <strong>{title}</strong>
            </p>

            <p style={{ margin: "0 0 12px" }}>
              The undersigned hereby attests that the data points enumerated below
              were collected from the named suppliers via Sluicebox Origin Trace
              between <strong>{new Date(Date.now() - 21 * 864e5).toLocaleDateString("en-US")}</strong> and{" "}
              <strong>{new Date().toLocaleDateString("en-US")}</strong>, and that — to the
              best of their knowledge — the values reported are correct, traceable
              to source documents, and free from material omission.
            </p>

            {/* Data summary */}
            <h3 style={{ fontSize: 13, margin: "20px 0 8px", borderBottom: "1px solid #ddd", paddingBottom: 4 }}>
              Collected data points {isCampaign && <span style={{ color: "#666", fontWeight: 400 }}>· {dataRows.length} suppliers × {fieldNames.length} fields</span>}
            </h3>
            <table style={{ width: "100%", borderCollapse: "collapse", margin: "8px 0 16px", fontSize: 11 }}>
              <thead>
                <tr style={{ background: "#f3f3f3" }}>
                  <th style={{ padding: "6px 8px", textAlign: "left", borderBottom: "1px solid #ccc" }}>Supplier</th>
                  <th style={{ padding: "6px 8px", textAlign: "left", borderBottom: "1px solid #ccc" }}>Part</th>
                  {fieldNames.map((f) => (
                    <th key={f} style={{ padding: "6px 8px", textAlign: "left", borderBottom: "1px solid #ccc" }}>{f}</th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {dataRows.map((r, i) => (
                  <tr key={i}>
                    <td style={{ padding: "6px 8px", borderBottom: "1px solid #eee", fontWeight: 600 }}>{r[0]}</td>
                    <td style={{ padding: "6px 8px", borderBottom: "1px solid #eee", fontFamily: "monospace", fontSize: 10 }}>{r[1]}</td>
                    {r.slice(2).map((c, ci) => (
                      <td key={ci} style={{ padding: "6px 8px", borderBottom: "1px solid #eee" }}>{c[0]}</td>
                    ))}
                  </tr>
                ))}
              </tbody>
            </table>

            {/* Source records */}
            <h3 style={{ fontSize: 13, margin: "20px 0 8px", borderBottom: "1px solid #ddd", paddingBottom: 4 }}>
              Source records {isCampaign ? "(summary)" : ""}
            </h3>
            <ul style={{ margin: "0 0 16px", paddingLeft: 20, fontSize: 12 }}>
              <li><strong>{isCampaign ? "47" : "3"} email threads</strong> · all messages, headers, attachments retained with cryptographic hashes</li>
              <li><strong>{isCampaign ? "32" : "2"} mill certificates (PDF)</strong> · parsed in full, original files retained</li>
              <li><strong>{isCampaign ? "18" : "1"} supplier workbooks (XLSX)</strong> · cell-level provenance preserved</li>
              <li><strong>{isCampaign ? "5" : "0"} supplier clarification questions</strong> · resolved with documented agent reasoning</li>
            </ul>

            {/* Email evidence — short example */}
            <h3 style={{ fontSize: 13, margin: "20px 0 8px", borderBottom: "1px solid #ddd", paddingBottom: 4 }}>
              Sample correspondence {isCampaign ? "(redacted)" : ""}
            </h3>
            <div style={{ background: "#fafafa", border: "1px solid #e0e0e0", padding: "10px 12px", fontSize: 11, fontFamily: "Georgia, serif", marginBottom: 16 }}>
              <div style={{ color: "#666" }}>From: supply@{(isCampaign ? "analog" : (supplier || "supplier")).toLowerCase().replace(/\s+/g, "")}.com</div>
              <div style={{ color: "#666" }}>To: origin-trace@digikey.sluicebox.ai</div>
              <div style={{ color: "#666", marginBottom: 6 }}>Subject: Re: Q2 origin trace request</div>
              <div>Hi — attached the workbook with smelter, diffusion and assembly origins for the parts you listed. Mill cert PDFs are also attached. Let me know if you need anything else. — Marie</div>
            </div>

            {/* Attestation */}
            <h3 style={{ fontSize: 13, margin: "20px 0 8px", padding: "6px 10px", background: "#fff3cd", border: "2px solid #ffc107", display: "inline-block" }}>
              ▸ Attestation of Correctness
            </h3>
            <p style={{ margin: "0 0 10px", padding: "10px 12px", background: "#fffbe6", borderLeft: "3px solid #ffc107" }}>
              By signing below, the responsible party certifies that the data points
              listed in this package are accurate, complete to the extent supplier
              responses permit, and supported by the source records summarized
              above. Material discrepancies discovered after signing must be
              re-attested via a corrective addendum.
            </p>

            {/* Signature block */}
            <div style={{ marginTop: 28, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 32 }}>
              <div>
                <div style={{ fontSize: 11, color: "#666", marginBottom: 4, textTransform: "uppercase", letterSpacing: 0.5 }}>DigiKey Representative</div>
                {signed ? (
                  <>
                    <div style={{
                      borderBottom: "1px solid #1a1a1a", paddingBottom: 2,
                      fontFamily: "'Brush Script MT', 'Snell Roundhand', cursive",
                      fontSize: 28, color: "#1a3d8f", minHeight: 40,
                    }}>{signature}</div>
                    <div style={{ fontSize: 11, color: "#666", marginTop: 4 }}>Signed {signedAt}</div>
                  </>
                ) : (
                  <>
                    <input
                      type="text"
                      value={signature}
                      onChange={(e) => setSignature(e.target.value)}
                      placeholder="Type full name to sign"
                      style={{
                        width: "100%", padding: "6px 8px", fontSize: 14,
                        border: "1px dashed #999", background: "#fffceb",
                        fontFamily: "'Brush Script MT', cursive",
                        color: "#1a3d8f",
                      }}
                    />
                    <button
                      onClick={sign}
                      disabled={!signature.trim()}
                      style={{
                        marginTop: 6, padding: "4px 10px", fontSize: 11,
                        background: signature.trim() ? "#1a3d8f" : "#ccc", color: "#fff",
                        border: "none", borderRadius: 3,
                        cursor: signature.trim() ? "pointer" : "not-allowed",
                      }}
                    >Sign</button>
                  </>
                )}
              </div>
            </div>

            {signed && (
              <div style={{
                marginTop: 24, padding: "10px 14px", background: "#e6f4ea",
                border: "1px solid #34a853", color: "#1e6b3a", fontSize: 12,
              }}>
                ✓ Document signed and cryptographically sealed. SHA-256:{" "}
                <span className="mono" style={{ fontFamily: "monospace" }}>
                  3f7a…b842
                </span>
              </div>
            )}

            <div style={{ marginTop: 24, fontSize: 10, color: "#999", textAlign: "center", borderTop: "1px solid #ddd", paddingTop: 8 }}>
              {isCampaign ? "Page 1 of 14" : "Page 1 of 4"} · Sluicebox Origin Trace · This package includes a JSON
              provenance sidecar with cryptographic hashes for every source file referenced.
            </div>
          </div>
        </div>

        {/* Footer actions */}
        <div style={{
          padding: "12px 16px", borderTop: "1px solid var(--bd-1)",
          background: "var(--bg-soft)", display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12,
        }}>
          <div className="meta">
            {signed
              ? "Signed · ready to download"
              : "Sign above, then download the sealed PDF"}
          </div>
          <div className="row" style={{ gap: 8 }}>
            <button className="btn ghost sm" onClick={onClose}>Cancel</button>
            <button
              className="btn primary sm"
              disabled={!signed}
              style={{ opacity: signed ? 1 : 0.5, cursor: signed ? "pointer" : "not-allowed" }}
              onClick={onClose}
            >↓ Download PDF</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// =========================================================================
// System map
// =========================================================================
function ScreenMap() {
  return (
    <div data-screen-label="map">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Reference</div>
          <h1 className="screen-title">System map</h1>
          <p className="screen-sub">
            Three layers, mapped to the Stefan / Elmar split. Campaign config is
            the single source of truth — everything downstream of "launch" is
            deterministic given (config + responses).
          </p>
        </div>
      </div>

      <div className="cols-3 mb-6">
        <div className="card">
          <div className="icon-tile-lg"><img src={ICON("Collect")} alt="" /></div>
          <div className="eyebrow acc">Layer 1 · Setup</div>
          <h3 className="h-2 mt-4">Config UI</h3>
          <ul className="text" style={{ paddingLeft: 18, marginTop: 8 }}>
            <li>Templates</li>
            <li>Schema editor</li>
            <li>Brief</li>
            <li>Recipients</li>
          </ul>
          <p className="meta mt-4">↓ emits Campaign config (JSON)</p>
        </div>
        <div className="card">
          <div className="icon-tile-lg" style={{ background: "color-mix(in oklab, var(--sb-orange) 12%, white)" }}>
            <img src={ICON("Mail")} alt="" />
          </div>
          <div className="eyebrow acc" style={{ color: "var(--sb-orange)" }}>Layer 2 · Runtime</div>
          <h3 className="h-2 mt-4">Agent</h3>
          <ul className="text" style={{ paddingLeft: 18, marginTop: 8 }}>
            <li>Email gen (schema-aware)</li>
            <li>Inbound parsing — body · xlsx · pdf · doc</li>
            <li>Schema mapping + validation</li>
            <li>Follow-up / clarification loop</li>
            <li>Escalation triggers</li>
          </ul>
          <p className="meta mt-4">↓ emits normalized records</p>
        </div>
        <div className="card">
          <div className="icon-tile-lg"><img src={ICON("Document")} alt="" /></div>
          <div className="eyebrow acc">Layer 3 · Output</div>
          <h3 className="h-2 mt-4">Audit & export</h3>
          <ul className="text" style={{ paddingLeft: 18, marginTop: 8 }}>
            <li>Live progress dashboard</li>
            <li>Export to template</li>
            <li>Audit trail (clickable provenance)</li>
            <li>Flagged queue + escalation</li>
          </ul>
        </div>
      </div>

      <div className="card spotlight">
        <div className="eyebrow acc">v1 scope · 4–6 weeks</div>
        <div className="cols-2 mt-4">
          <div>
            <h3 className="h-2">In</h3>
            <ul className="text" style={{ paddingLeft: 18, marginTop: 8 }}>
              <li>Setup UI for the four primitives</li>
              <li>Field types: string, number, enum (date/bool fast-follow)</li>
              <li>Email gen + inbound parsing for body + xlsx + pdf</li>
              <li>One-shot follow-up</li>
              <li>Two starter templates: Carbon (parity), Tariff Origin (new)</li>
              <li>Excel + customer-template-fill export</li>
              <li>Audit trail with source-message provenance</li>
            </ul>
          </div>
          <div>
            <h3 className="h-2">Out (v1.1+)</h3>
            <ul className="text" style={{ paddingLeft: 18, marginTop: 8 }}>
              <li>Conditional fields (B if A=X)</li>
              <li>Word + image OCR</li>
              <li>Live API / webhook</li>
              <li>Multi-language email gen</li>
              <li>Customer-facing self-serve setup</li>
              <li>PCN starter template</li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  );
}

// =========================================================================
// Root
// =========================================================================
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "schemaVariant": "Notion-list",
  "showAnnotations": true,
  "density": "Comfortable"
}/*EDITMODE-END*/;

// =========================================================================
// Live-mode screens (real backend data)
// =========================================================================
function ScreenDashLive({ setScreen, live }) {
  // Poll the dashboard every 8s so newly-arrived replies (drained by the
  // SQS consumer) show up without the CSM having to reload.
  useEffect(() => {
    if (!live.liveMode || !live.activeCampaignId) return;
    const t = setInterval(() => { live.refreshDashboard(); }, 8000);
    return () => clearInterval(t);
  }, [live.liveMode, live.activeCampaignId, live.refreshDashboard]);

  const data = live.dashboard;
  if (live.error) {
    return (
      <div data-screen-label="dashboard">
        <div className="card">
          <h2 className="h">Backend unreachable</h2>
          <p className="text" style={{ marginTop: 12 }}>
            Tried <code>{live.apiBase}/campaigns/{live.campaignId}/dashboard</code>
          </p>
          <p className="text"><strong>Error:</strong> {live.error}</p>
          <p className="text">
            Start the backend: <code>cd backend && make dev</code>
          </p>
        </div>
      </div>
    );
  }
  if (!data) {
    return <div className="card"><div className="text">Loading dashboard…</div></div>;
  }

  const { campaign, kpis, progress, recipients, activity } = data;
  const total = recipients.length || 1;
  const pct = (n) => Math.round((n / total) * 100);

  return (
    <div data-screen-label="dashboard">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Live campaign · real backend</div>
          <h1 className="screen-title">{campaign.name}</h1>
          <p className="screen-sub">{campaign.brief}</p>
        </div>
        <div className="state-banner">
          <span className="state-dot live" />
          <span>Live · {kpis.recipients_responded}/{kpis.recipients_total} responded</span>
        </div>
      </div>

      <div className="cols-3 mb-6">
        <div className="card">
          <div className="eyebrow">Suppliers responded</div>
          <div className="metric-num">{kpis.recipients_responded} <span style={{ color: "var(--fg-3)", fontSize: 24 }}>/ {kpis.recipients_total}</span></div>
          <span className="chip ok">{pct(kpis.recipients_completed)}% complete</span>
        </div>
        <div className="card">
          <div className="eyebrow">Data points supplier-confirmed</div>
          <div className="metric-num acc">{kpis.data_points_confirmed} <span style={{ color: "var(--fg-3)", fontSize: 24 }}>/ {kpis.data_points_total}</span></div>
        </div>
        <div className="card">
          <div className="eyebrow">Flagged for review</div>
          <div className="metric-num warm">{kpis.recipients_flagged}</div>
          <span className="chip warn">{kpis.recipients_flagged === 0 ? "None" : "Needs CSM"}</span>
        </div>
      </div>

      <div className="card mb-6">
        <div className="row between">
          <div className="eyebrow">Campaign progress</div>
          <button className="btn ghost sm" onClick={live.refreshDashboard}>↻ Refresh</button>
        </div>
        <div className="progress-bar mt-4">
          <i className="ok" style={{ width: `${pct(progress.complete)}%` }} />
          <i className="pending" style={{ width: `${pct(progress.in_progress)}%` }} />
          <i className="flag" style={{ width: `${pct(progress.flagged)}%` }} />
          <i className="miss" style={{ width: `${pct(progress.no_reply)}%` }} />
        </div>
        <div className="legend mt-4">
          <span className="ok"><i />Complete ({progress.complete})</span>
          <span className="pending"><i />In progress ({progress.in_progress})</span>
          <span className="flag"><i />Flagged ({progress.flagged})</span>
          <span className="miss"><i />No reply ({progress.no_reply})</span>
        </div>
      </div>

      <div className="cols-wide">
        <div className="card" style={{ padding: 0 }}>
          <div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border-1)" }}>
            <h3 className="h-2">Suppliers</h3>
          </div>
          <table className="table">
            <thead>
              <tr><th>Supplier</th><th>State</th><th>Data points</th><th>Last reply</th><th></th></tr>
            </thead>
            <tbody>
              {recipients.map((r) => (
                <tr
                  key={r.id}
                  style={{ cursor: "pointer" }}
                  onClick={() => { live.setSelectedRecipientId(r.id); setScreen("thread"); }}
                >
                  <td><strong>{r.supplier_name}</strong><div className="meta">{r.contact_email}</div></td>
                  <td>
                    <span className={cx("chip",
                      r.ui_state === "complete" && "ok",
                      r.ui_state === "in-progress" && "muted",
                      r.ui_state === "follow-up" && "warn",
                      r.ui_state === "flagged" && "warn",
                      r.ui_state === "no-reply" && "muted")}>
                      {r.ui_state}
                    </span>
                  </td>
                  <td>{r.fields_done} / {r.fields_total}{r.avg_confidence != null && <span className="meta" style={{ marginLeft: 6 }}>conf {r.avg_confidence}</span>}</td>
                  <td className="meta">{r.last_reply_relative}</td>
                  <td className="row-action"><span className="row-arrow">→</span></td>
                </tr>
              ))}
              {recipients.length === 0 && (
                <tr><td colSpan={5} className="meta" style={{ padding: 24, textAlign: "center" }}>No recipients yet</td></tr>
              )}
            </tbody>
          </table>
        </div>

        <div className="col">
          <div className="card">
            <div className="eyebrow">Activity feed</div>
            <div className="mt-4">
              {activity.map((a, i) => (
                <div className="feed-item" key={i}>
                  <span className={cx("icon-tile", a.kind)}>{a.icon}</span>
                  <div>{a.text}</div>
                  <div className="when">{a.when}</div>
                </div>
              ))}
              {activity.length === 0 && <div className="meta">No activity yet.</div>}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function ScreenTemplatesLive({ setScreen, live }) {
  const [picked, setPicked] = useState(null);
  const [name, setName] = useState("");
  const [creating, setCreating] = useState(false);
  const [createError, setCreateError] = useState(null);

  if (!live.templates) {
    return <div className="card"><div className="text">Loading templates…</div></div>;
  }

  const handleCreate = async () => {
    if (!picked) return;
    setCreating(true);
    setCreateError(null);
    try {
      await live.createFromTemplate(picked.id, name || picked.name);
      setScreen("brief");
    } catch (e) {
      setCreateError(String(e));
    } finally {
      setCreating(false);
    }
  };

  return (
    <div data-screen-label="templates">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Step 1 of 5 · live</div>
          <h1 className="screen-title">Pick a starter template</h1>
          <p className="screen-sub">Templates are forkable — your edits override the seed.</p>
        </div>
      </div>

      <Stepper screen="templates" setScreen={setScreen} />

      <div className="cols-3">
        {live.templates.map((t) => (
          <div
            key={t.id}
            className={cx("template-card", picked?.id === t.id && "picked")}
            onClick={() => setPicked(t)}
          >
            {picked?.id === t.id && <span className="chip acc picked-badge">✓ selected</span>}
            <h3 className="h-2">{t.name}</h3>
            <p className="text" style={{ marginTop: 8, minHeight: 56 }}>{t.description}</p>
            <div className="tpl-meta"><span>{t.fields.length} fields</span></div>
          </div>
        ))}
      </div>

      {picked && (
        <div className="card mt-6">
          <div className="eyebrow">Name your campaign</div>
          <input
            className="input mt-4"
            style={{ width: "100%", padding: 12 }}
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder={picked.name}
          />
          <div className="row" style={{ gap: 8, marginTop: 12 }}>
            <button className="btn primary" disabled={creating} onClick={handleCreate}>
              {creating ? "Creating…" : "Create campaign →"}
            </button>
            {createError && <span className="meta" style={{ color: "var(--warm)" }}>{createError}</span>}
          </div>
          <div className="meta mt-4">
            Brief preview: {picked.brief ? picked.brief.slice(0, 160) + "…" : <em>blank</em>}
          </div>
        </div>
      )}
    </div>
  );
}

function ScreenBriefLive({ setScreen, live }) {
  const [campaign, setCampaign] = useState(null);
  const [name, setName] = useState("");
  const [brief, setBrief] = useState("");
  const [saving, setSaving] = useState(false);
  const [savedAt, setSavedAt] = useState(null);
  const [error, setError] = useState(null);
  const [seeding, setSeeding] = useState(false);
  const [seedResult, setSeedResult] = useState(null);
  const seedFileRef = useRef(null);

  useEffect(() => {
    if (!live.wizardCampaignId) return;
    live.fetchCampaign(live.wizardCampaignId)
      .then((c) => { setCampaign(c); setName(c.name); setBrief(c.brief); })
      .catch((e) => setError(String(e)));
  }, [live.wizardCampaignId]);

  if (!live.wizardCampaignId) {
    return (
      <div className="card">
        <h2 className="h">Pick a template first</h2>
        <p className="text" style={{ marginTop: 12 }}>The brief lives on a campaign — make one in step 1.</p>
        <button className="btn primary mt-4" onClick={() => setScreen("templates")}>← Pick template</button>
      </div>
    );
  }
  if (!campaign) return <div className="card"><div className="text">Loading…</div></div>;

  const dirty = name !== campaign.name || brief !== campaign.brief;
  const handleSave = async () => {
    setSaving(true);
    setError(null);
    try {
      const updated = await live.patchCampaign(live.wizardCampaignId, { name, brief });
      setCampaign(updated);
      setSavedAt(new Date().toLocaleTimeString());
    } catch (e) { setError(String(e)); }
    finally { setSaving(false); }
  };

  const handleSeed = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    setSeeding(true);
    setError(null);
    setSeedResult(null);
    try {
      const r = await live.seedFromDocument(live.wizardCampaignId, file);
      setSeedResult(r);
      // Pre-fill the brief textarea with the suggestion (don't save yet —
      // user reviews and clicks Save).
      if (r.suggested_brief) setBrief(r.suggested_brief);
    } catch (err) {
      setError(String(err));
    } finally {
      setSeeding(false);
      if (seedFileRef.current) seedFileRef.current.value = "";
    }
  };

  const applySuggestedFields = async () => {
    if (!seedResult?.suggested_fields?.length) return;
    try {
      // First save the brief if it's been edited (e.g., pre-filled by seed
      // but never explicitly saved). Otherwise the Review-screen
      // "Brief is at least a sentence" check sees the stale empty value.
      if (dirty) {
        const updated = await live.patchCampaign(live.wizardCampaignId, { name, brief });
        setCampaign(updated);
        setSavedAt(new Date().toLocaleTimeString());
      }
      // Get existing field names to avoid duplicates
      const existing = await live.fetchFields(live.wizardCampaignId);
      const have = new Set(existing.map((f) => f.name));
      let order = (existing[existing.length - 1]?.order || 0) + 1;
      for (const f of seedResult.suggested_fields) {
        if (have.has(f.name)) continue;
        await live.addField(live.wizardCampaignId, {
          name: f.name,
          label: f.label,
          description: f.description || "",
          type: f.type || "string",
          required: f.required ?? true,
          agent_hint: {},
          order: order++,
        });
      }
      setSeedResult({ ...seedResult, applied: true });
    } catch (err) { setError(String(err)); }
  };

  // Tone/topic chips inferred from the brief — purely visual aid for the
  // "What the agent learns" panel (matches the original mock's vibe).
  const briefSize = (brief || "").length;
  const wordCount = (brief || "").trim().split(/\s+/).filter(Boolean).length;

  return (
    <div data-screen-label="brief">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Step 2 of 5 · live</div>
          <h1 className="screen-title">Name + Context Brief</h1>
          <p className="screen-sub">
            2–4 sentences. Plain English: what, why, what you'll do with it.
            The agent ingests this as the topic frame for every supplier conversation.
          </p>
        </div>
        <span className="screen-tag">The hard part</span>
      </div>

      <Stepper screen="brief" setScreen={setScreen} />

      <div className="cols-side">
        <div className="col">
          <div className="card">
            <div className="eyebrow">Campaign name</div>
            <input
              className="field lg"
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
          </div>

          <div className="card">
            <div className="row between mb-3">
              <div className="eyebrow">Context Brief</div>
              <span className="meta">{wordCount} words · {briefSize} chars</span>
            </div>
            <textarea
              className="field area"
              style={{ minHeight: 220, fontSize: 14 }}
              value={brief}
              onChange={(e) => setBrief(e.target.value)}
              placeholder="Per the [regulation], we need [data points] from suppliers because [reason]. Notes for the agent: [disambiguation, edge cases]…"
            />
            <div className="row mt-4" style={{ gap: 8, flexWrap: "wrap", alignItems: "center" }}>
              <label className="btn ghost sm" style={{ cursor: "pointer" }}>
                {seeding ? "Reading…" : "📎 Upload context doc"}
                <input
                  ref={seedFileRef}
                  type="file"
                  accept=".pdf,.docx,.txt,.xlsx,.csv,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv"
                  onChange={handleSeed}
                  disabled={seeding}
                  style={{ display: "none" }}
                />
              </label>
              <span className="meta">
                Drop a PRD / regulation / spec — the agent will pre-fill the brief and suggest schema fields.
              </span>
            </div>
            {seedResult && (
              <div className="mt-4" style={{
                padding: 12, borderRadius: 8,
                background: "var(--bg-soft)", border: "1px solid var(--border-1)",
              }}>
                <div className="eyebrow acc">Suggestions from {seedResult.filename}</div>
                <p className="meta mt-4">
                  Read {seedResult.char_count.toLocaleString()} characters · brief pre-filled above.
                </p>
                {seedResult.suggested_fields?.length > 0 && (
                  <div className="mt-4">
                    <div className="meta">
                      <strong>{seedResult.suggested_fields.length}</strong> schema fields suggested:
                    </div>
                    <div className="row" style={{ flexWrap: "wrap", gap: 6, marginTop: 6 }}>
                      {seedResult.suggested_fields.map((f) => (
                        <span key={f.name} className="chip muted" title={f.description}>
                          {f.label}{f.required ? "" : "?"}
                        </span>
                      ))}
                    </div>
                    {seedResult.applied ? (
                      <span className="chip ok mt-4">✓ Added to schema</span>
                    ) : (
                      <button className="btn primary sm mt-4" onClick={applySuggestedFields}>
                        Apply to schema →
                      </button>
                    )}
                  </div>
                )}
              </div>
            )}
          </div>
        </div>

        <div className="col">
          <div className="card spotlight">
            <div className="eyebrow acc">What the agent learns from this</div>
            <ul style={{
              paddingLeft: 18, margin: "12px 0 0", lineHeight: 1.7,
              fontSize: 13.5, color: "var(--fg-1)",
            }}>
              <li><strong>Topic frame:</strong> {wordCount > 0 ? "extracted from your brief" : "—"}</li>
              <li><strong>Tone hints:</strong> {wordCount > 5 ? "regulatory, professional" : "—"}</li>
              <li><strong>Brief length:</strong> {wordCount > 30 ? "✓ enough for the agent to work with" : "needs more detail"}</li>
              <li><strong>Self-resolve rules:</strong> tucked into the brief; the agent uses them at run-time</li>
            </ul>
            <hr className="divider" />
            <div className="eyebrow acc">Tip</div>
            <p className="text" style={{ marginTop: 8 }}>
              <em>Don't try to teach the agent vocabulary or synonyms.</em> It handles
              spelling variants automatically. Use the brief to explain the <em>policy</em>
              behind ambiguous answers — what to do if the supplier says "unknown",
              which source document is authoritative, etc.
            </p>
          </div>
        </div>
      </div>

      <div className="wizard-foot mt-6">
        <button className="btn ghost" onClick={() => setScreen("templates")}>← Back</button>
        <div className="row" style={{ gap: 8, alignItems: "center" }}>
          {error && <span className="meta" style={{ color: "var(--warm)" }}>{error}</span>}
          {savedAt && !dirty && <span className="meta">Saved {savedAt}</span>}
          <button
            className={cx("btn", dirty ? "primary" : "ghost")}
            disabled={saving || !dirty}
            onClick={handleSave}
          >
            {saving ? "Saving…" : dirty ? "Save" : "Saved"}
          </button>
          <button className="btn primary" onClick={() => setScreen("schema")}>Next →</button>
        </div>
      </div>
    </div>
  );
}

function SchemaFieldRow({ f, isFirst, isLast, isBusy, onPatch, onMove, onDelete }) {
  // Controlled inputs synced from props — so "Apply suggested schema"
  // (and any other upstream change) flows through to the visible value.
  const [label, setLabel] = useState(f.label || "");
  const [description, setDescription] = useState(f.description || "");
  useEffect(() => { setLabel(f.label || ""); }, [f.label]);
  useEffect(() => { setDescription(f.description || ""); }, [f.description]);

  const inputStyle = {
    border: "1px solid var(--border-2)", borderRadius: 8,
    padding: "8px 12px", fontSize: 14, fontFamily: "inherit",
    background: "white", color: "var(--fg-1)", outline: "none",
  };

  return (
    <div className="card" style={{ padding: 14, opacity: isBusy ? 0.5 : 1 }}>
      <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
        <div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 8 }}>
          {/* row 1: order# + label + name + type */}
          <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
            <span style={{ color: "var(--fg-3)", fontSize: 12, minWidth: 24 }}>#{f.order}</span>
            <input
              style={{ ...inputStyle, flex: 1, fontWeight: 600 }}
              value={label}
              placeholder="Label shown to suppliers"
              onChange={(e) => setLabel(e.target.value)}
              onBlur={() => label !== f.label && onPatch(f.id, { label })}
            />
            <code style={{ color: "var(--fg-3)", fontSize: 11, whiteSpace: "nowrap" }}>{f.name}</code>
          </div>
          {/* row 2: description */}
          <input
            style={{ ...inputStyle, width: "100%", fontSize: 13, color: "var(--fg-2)" }}
            value={description}
            placeholder="Description shown to suppliers in the email"
            onChange={(e) => setDescription(e.target.value)}
            onBlur={() => description !== f.description && onPatch(f.id, { description })}
          />
          {/* row 3: required toggle */}
          <div>
            <button
              className={cx("chip sm", f.required ? "ok" : "muted")}
              onClick={() => onPatch(f.id, { required: !f.required })}
            >
              {f.required ? "✓ required" : "optional"}
            </button>
          </div>
        </div>
        <div style={{ display: "flex", gap: 4, flexShrink: 0 }}>
          <button className="btn quiet sm" disabled={isFirst} onClick={() => onMove(f.id, -1)} title="Move up">↑</button>
          <button className="btn quiet sm" disabled={isLast} onClick={() => onMove(f.id, 1)} title="Move down">↓</button>
          <button className="btn quiet sm" onClick={() => onDelete(f.id)} title="Delete">✕</button>
        </div>
      </div>
    </div>
  );
}

function ScreenSchemaLive({ setScreen, live }) {
  const [fields, setFields] = useState(null);
  const [error, setError] = useState(null);
  const [adding, setAdding] = useState(false);
  const [newField, setNewField] = useState({ name: "", label: "", description: "", type: "string", required: true });
  const [busyFieldId, setBusyFieldId] = useState(null);

  const reload = useCallback(async () => {
    if (!live.wizardCampaignId) return;
    try {
      const data = await live.fetchFields(live.wizardCampaignId);
      setFields(data);
    } catch (e) { setError(String(e)); }
  }, [live.wizardCampaignId]);

  useEffect(() => { reload(); }, [reload]);

  if (!live.wizardCampaignId) {
    return (
      <div className="card">
        <h2 className="h">Pick a template first</h2>
        <button className="btn primary mt-4" onClick={() => setScreen("templates")}>← Pick template</button>
      </div>
    );
  }
  if (!fields) return <div className="card"><div className="text">Loading fields…</div></div>;

  const handleAdd = async () => {
    if (!newField.name || !newField.label) return;
    try {
      await live.addField(live.wizardCampaignId, { ...newField, agent_hint: {}, order: 0 });
      setNewField({ name: "", label: "", description: "", type: "string", required: true });
      setAdding(false);
      reload();
    } catch (e) { setError(String(e)); }
  };

  const handlePatch = async (fid, body) => {
    setBusyFieldId(fid);
    try {
      await live.patchField(live.wizardCampaignId, fid, body);
      await reload();
    } catch (e) { setError(String(e)); }
    finally { setBusyFieldId(null); }
  };

  const handleDelete = async (fid) => {
    if (!confirm("Remove this field?")) return;
    setBusyFieldId(fid);
    try {
      await live.deleteField(live.wizardCampaignId, fid);
      await reload();
    } catch (e) { setError(String(e)); }
    finally { setBusyFieldId(null); }
  };

  const move = async (fid, delta) => {
    const idx = fields.findIndex((f) => f.id === fid);
    const target = fields[idx + delta];
    if (!target) return;
    await Promise.all([
      live.patchField(live.wizardCampaignId, fid, { order: target.order }),
      live.patchField(live.wizardCampaignId, target.id, { order: fields[idx].order }),
    ]);
    reload();
  };

  return (
    <div data-screen-label="schema">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Step 3 of 5 · live</div>
          <h1 className="screen-title">Define the Data Schema</h1>
          <p className="screen-sub">
            Add, remove, or reorder data points. Each one has a per-supplier label
            and (optionally) a condition. The agent handles synonyms, casing,
            and spelling variants on its own.
          </p>
        </div>
        <span className="screen-tag">The heart of it</span>
      </div>

      <Stepper screen="schema" setScreen={setScreen} />

      <div className="note mb-4">
        <strong>✨ Schema</strong> — seeded from your template + brief. Edit
        labels and descriptions inline; toggle required via the chip.
        <div className="row mt-4" style={{ gap: 8 }}>
          <button className="btn ghost sm" onClick={() => setScreen("brief")}>← Edit Brief</button>
          {!adding && <button className="btn primary sm" onClick={() => setAdding(true)}>+ Add data point</button>}
        </div>
      </div>

      {error && <div className="card mb-4" style={{ borderColor: "var(--warm)" }}><div className="text" style={{ color: "var(--warm)" }}>{error}</div></div>}

      <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
        {fields.map((f, i) => (
          <SchemaFieldRow
            key={f.id}
            f={f}
            isFirst={i === 0}
            isLast={i === fields.length - 1}
            isBusy={busyFieldId === f.id}
            onPatch={handlePatch}
            onMove={move}
            onDelete={handleDelete}
          />
        ))}
        {fields.length === 0 && !adding && (
          <div className="card">
            <div className="text" style={{ textAlign: "center", color: "var(--fg-3)" }}>
              No fields yet. Click <strong>+ Add data point</strong> above, or go back to the
              Brief and upload a context doc to have the agent suggest a starting schema.
            </div>
          </div>
        )}
      </div>

      {adding && (
        <div className="card mt-4">
          <div className="eyebrow">New data point</div>
          <div className="cols-3 mt-4" style={{ gap: 8 }}>
            <input className="field" placeholder="name (snake_case, e.g. country_of_smelt)" value={newField.name}
              onChange={(e) => setNewField({ ...newField, name: e.target.value })} />
            <input className="field" placeholder="label (e.g. Country of smelt)" value={newField.label}
              onChange={(e) => setNewField({ ...newField, label: e.target.value })} />
            <select className="field" value={newField.type}
              onChange={(e) => setNewField({ ...newField, type: e.target.value })}>
              <option value="string">string</option>
              <option value="number">number</option>
              <option value="enum">enum</option>
              <option value="date">date</option>
              <option value="boolean">boolean</option>
              <option value="file">file</option>
              <option value="list">list</option>
            </select>
          </div>
          <textarea className="field area mt-4" placeholder="Description shown to suppliers in the email"
            value={newField.description}
            onChange={(e) => setNewField({ ...newField, description: e.target.value })}
            style={{ minHeight: 60 }} />
          <div className="row mt-4" style={{ gap: 12, alignItems: "center" }}>
            <label className="check" onClick={() => setNewField({ ...newField, required: !newField.required })}>
              <span className="box">{newField.required ? "✓" : ""}</span>
              <span>Required</span>
            </label>
            <span style={{ flex: 1 }} />
            <button className="btn ghost" onClick={() => setAdding(false)}>Cancel</button>
            <button className="btn primary" onClick={handleAdd} disabled={!newField.name || !newField.label}>Add</button>
          </div>
        </div>
      )}

      <div className="wizard-foot mt-6">
        <button className="btn ghost" onClick={() => setScreen("brief")}>← Back</button>
        <button className="btn primary" onClick={() => setScreen("recipients")}>Next →</button>
      </div>
    </div>
  );
}

function ScreenRecipientsLive({ setScreen, live }) {
  const fileRef = useRef(null);
  const [uploading, setUploading] = useState(false);
  const [result, setResult] = useState(null);
  const [error, setError] = useState(null);

  if (!live.wizardCampaignId) {
    return (
      <div className="card">
        <h2 className="h">Pick a template first</h2>
        <p className="text" style={{ marginTop: 12 }}>
          Recipients attach to a campaign — go back to step 1 to create one.
        </p>
        <button className="btn primary mt-4" onClick={() => setScreen("templates")}>
          ← Pick template
        </button>
      </div>
    );
  }

  const handleUpload = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    setUploading(true);
    setError(null);
    setResult(null);
    try {
      const r = await live.uploadRecipientsCsv(file);
      setResult(r);
    } catch (err) {
      setError(String(err));
    } finally {
      setUploading(false);
    }
  };

  return (
    <div data-screen-label="recipients">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Step 4 of 5 · live</div>
          <h1 className="screen-title">Upload recipients</h1>
          <p className="screen-sub">
            CSV or xlsx with headers like <code>supplier_name, contact_email</code> (also
            accepts <code>supplier name</code>, <code>email</code>, <code>company</code>).
            Optional <code>scope</code> column for SKU/part-number context.
          </p>
        </div>
      </div>

      <Stepper screen="recipients" setScreen={setScreen} />

      <div className="card">
        <div className="eyebrow">Drop a CSV</div>
        <div className="mt-4">
          <input
            ref={fileRef}
            type="file"
            accept=".csv,.xlsx,.xls,text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel"
            onChange={handleUpload}
            disabled={uploading}
          />
        </div>
        {uploading && <div className="text mt-4">Uploading…</div>}
        {error && <div className="text mt-4" style={{ color: "var(--warm)" }}>{error}</div>}
        {result && (
          <div className="mt-4">
            <div className="row" style={{ gap: 16 }}>
              <span className="chip ok">✓ {result.created} created</span>
              <span className="chip muted">⊝ {result.skipped_duplicate} duplicates skipped</span>
              {result.invalid?.length > 0 && (
                <span className="chip warn">⚠ {result.invalid.length} invalid</span>
              )}
            </div>
            {result.invalid?.length > 0 && (
              <div className="mt-4">
                <div className="eyebrow">Errors</div>
                <ul className="text" style={{ paddingLeft: 18, marginTop: 8 }}>
                  {result.invalid.map((e, i) => (
                    <li key={i}>row {e.row}: {e.reason}</li>
                  ))}
                </ul>
              </div>
            )}
          </div>
        )}
      </div>

      <div className="wizard-foot mt-6">
        <button className="btn ghost" onClick={() => setScreen("schema")}>← Back</button>
        <button className="btn primary" onClick={() => setScreen("review")}>Next →</button>
      </div>
    </div>
  );
}

function ScreenReviewLive({ setScreen, live }) {
  const [campaign, setCampaign] = useState(null);
  const [preview, setPreview] = useState(null);
  const [dashboard, setDashboard] = useState(null);
  const [error, setError] = useState(null);
  const [launching, setLaunching] = useState(false);
  const [launchResult, setLaunchResult] = useState(null);

  useEffect(() => {
    if (!live.wizardCampaignId) return;
    Promise.all([
      live.fetchCampaign(live.wizardCampaignId),
      live.previewEmail(live.wizardCampaignId).catch(() => null),
      fetch(`${live.apiBase}/campaigns/${live.wizardCampaignId}/dashboard`).then((r) => r.ok ? r.json() : null),
    ])
      .then(([c, p, d]) => { setCampaign(c); setPreview(p); setDashboard(d); })
      .catch((e) => setError(String(e)));
  }, [live.wizardCampaignId]);

  if (!live.wizardCampaignId) {
    return (
      <div className="card">
        <h2 className="h">Pick a template first</h2>
        <button className="btn primary mt-4" onClick={() => setScreen("templates")}>← Pick template</button>
      </div>
    );
  }
  if (!campaign) return <div className="card"><div className="text">Loading…</div></div>;

  const requiredCount = campaign.fields.filter((f) => f.required).length;
  const recipientCount = dashboard?.kpis?.recipients_total ?? 0;
  const queuedCount = dashboard?.recipients?.filter((r) => r.status === "queued").length ?? 0;
  const checks = [
    { ok: campaign.name?.length > 0, text: "Campaign has a name" },
    { ok: (campaign.brief || "").trim().length > 20, text: "Brief is at least a sentence (helps the agent disambiguate)" },
    { ok: requiredCount > 0, text: `Schema has at least one required field (${requiredCount})` },
    { ok: queuedCount > 0, text: `Recipients ready to send: ${queuedCount}` },
  ];
  const allOk = checks.every((c) => c.ok);

  const handleLaunch = async () => {
    if (!allOk || launching) return;
    if (!confirm(`Launch ${queuedCount} email(s) now?`)) return;
    setLaunching(true);
    setError(null);
    try {
      const r = await live.launchCampaign(live.wizardCampaignId);
      setLaunchResult(r);
      // After a successful launch, navigate to dashboard
      setTimeout(() => setScreen("dash"), 1200);
    } catch (e) {
      setError(String(e));
    } finally {
      setLaunching(false);
    }
  };

  return (
    <div data-screen-label="review">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Step 5 of 5 · live</div>
          <h1 className="screen-title">Review &amp; launch</h1>
          <p className="screen-sub">One last look. Launch sends the first-touch to every queued recipient.</p>
        </div>
      </div>

      <Stepper screen="review" setScreen={setScreen} />

      <div className="cols-wide">
        <div>
          <div className="card mb-6">
            <div className="eyebrow">Pre-launch checks</div>
            <ul className="text mt-4" style={{ listStyle: "none", padding: 0 }}>
              {checks.map((c, i) => (
                <li key={i} style={{ padding: "6px 0" }}>
                  <span style={{ marginRight: 8 }}>{c.ok ? "✓" : "○"}</span>
                  <span style={{ color: c.ok ? "var(--ok)" : "var(--fg-3)" }}>{c.text}</span>
                </li>
              ))}
            </ul>
          </div>

          <div className="card mb-6">
            <div className="eyebrow">First-touch preview</div>
            <p className="meta mt-4">Sample sent to <code>{preview?.sample_email || "—"}</code></p>
            {preview ? (
              <>
                <div className="text" style={{ marginTop: 12, fontWeight: 600 }}>{preview.subject}</div>
                <pre className="text" style={{ marginTop: 12, whiteSpace: "pre-wrap", fontFamily: "inherit" }}>{preview.body_text}</pre>
              </>
            ) : <div className="meta">(loading preview…)</div>}
          </div>
        </div>

        <div className="col">
          <div className="card spotlight">
            <div className="eyebrow acc">Ready to launch</div>
            <h3 className="h-2 mt-4">{campaign.name}</h3>
            <p className="meta mt-4">
              {campaign.fields.length} fields · {recipientCount} recipients · {queuedCount} queued
            </p>
            <button
              className="btn warm lg mt-4"
              disabled={!allOk || launching}
              onClick={handleLaunch}
              style={{ width: "100%" }}
            >
              {launching ? "Launching…" : `Launch ${queuedCount} email${queuedCount === 1 ? "" : "s"} →`}
            </button>
            {error && <div className="text mt-4" style={{ color: "var(--warm)" }}>{error}</div>}
            {launchResult && (
              <div className="mt-4">
                <span className="chip ok">✓ Sent: {launchResult.sent.length}</span>
                {launchResult.failed.length > 0 && (
                  <span className="chip warn" style={{ marginLeft: 8 }}>
                    ✕ Failed: {launchResult.failed.length}
                  </span>
                )}
                <p className="meta mt-4">Going to dashboard…</p>
              </div>
            )}
            {!allOk && (
              <div className="meta mt-4">Fix the unchecked items above to enable Launch.</div>
            )}
          </div>
        </div>
      </div>

      <div className="wizard-foot mt-6">
        <button className="btn ghost" onClick={() => setScreen("recipients")}>← Back</button>
        <span />
      </div>
    </div>
  );
}

function ScreenAuditLive({ setScreen, live }) {
  const [audit, setAudit] = useState(null);
  const [error, setError] = useState(null);
  const [picked, setPicked] = useState(null);  // {recipient, field_name, cell}
  const [templateInfo, setTemplateInfo] = useState(null);
  const [uploadingTemplate, setUploadingTemplate] = useState(false);
  const cid = live.activeCampaignId;

  const refreshTemplate = useCallback(() => {
    if (!cid) return;
    live.fetchTemplateMapping(cid).then(setTemplateInfo).catch(() => setTemplateInfo({ configured: false }));
  }, [cid]);

  useEffect(() => {
    if (!cid) return;
    live.fetchAudit(cid).then(setAudit).catch((e) => setError(String(e)));
    refreshTemplate();
  }, [cid, refreshTemplate]);

  const handleTemplateUpload = async (e) => {
    const file = e.target.files?.[0];
    if (!file || !cid) return;
    setUploadingTemplate(true);
    try {
      await live.uploadCustomerTemplate(cid, file);
      refreshTemplate();
    } catch (err) {
      setError(String(err));
    } finally {
      setUploadingTemplate(false);
    }
  };

  if (!cid) {
    return (
      <div className="card">
        <h2 className="h">No campaign selected</h2>
        <p className="text" style={{ marginTop: 12 }}>
          Open with <code>?live=1&amp;campaign=&lt;id&gt;</code>, or set one up via the agent first.
        </p>
        <button className="btn primary mt-4" onClick={() => setScreen("agent")}>← Set up with agent</button>
      </div>
    );
  }
  if (error) return <div className="card"><div className="text" style={{ color: "var(--warm)" }}>{error}</div></div>;
  if (!audit) return <div className="card"><div className="text">Loading audit…</div></div>;

  const dl = (kind) => `${live.apiBase}/campaigns/${cid}/export.${kind}`;
  const totalCells = audit.recipients.length * audit.fields.length;
  const filledCells = audit.recipients.reduce(
    (n, r) => n + audit.fields.filter((f) => r.values[f.name]).length, 0
  );
  const coverage = totalCells > 0 ? Math.round((filledCells / totalCells) * 100) : 0;

  return (
    <div data-screen-label="audit">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Audit &amp; export · live</div>
          <h1 className="screen-title">{audit.campaign.name}</h1>
          <p className="screen-sub">
            Every value clicks back to its source message. Coverage: {filledCells}/{totalCells} ({coverage}%).
          </p>
        </div>
        <div className="row" style={{ gap: 8 }}>
          <button type="button" className="btn ghost" onClick={() => _authedDownload(dl("csv"), "CSV")}>↓ CSV</button>
          <button type="button" className="btn ghost" onClick={() => _authedDownload(dl("xlsx"), "XLSX")}>↓ XLSX</button>
          <button type="button" className="btn ghost" onClick={() => _authedDownload(`${live.apiBase}/campaigns/${cid}/audit-package.pdf`, "Audit PDF")}>↓ Audit PDF</button>
          {templateInfo?.configured ? (
            <button type="button" className="btn primary" onClick={() => _authedDownload(`${live.apiBase}/campaigns/${cid}/export-customer-template.xlsx`, "Customer template")}>
              ↓ Customer template
            </button>
          ) : (
            <label className="btn primary" style={{ cursor: "pointer" }}>
              {uploadingTemplate ? "Uploading…" : "Upload customer template"}
              <input
                type="file"
                accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                onChange={handleTemplateUpload}
                style={{ display: "none" }}
                disabled={uploadingTemplate}
              />
            </label>
          )}
        </div>
      </div>

      <div className="cols-wide">
        <div className="card" style={{ padding: 0, overflow: "auto" }}>
          <table className="table">
            <thead>
              <tr>
                <th>Supplier</th>
                <th>State</th>
                {audit.fields.map((f) => <th key={f.id}>{f.label}{f.required && "*"}</th>)}
              </tr>
            </thead>
            <tbody>
              {audit.recipients.map((rec) => (
                <tr key={rec.id}>
                  <td><strong>{rec.supplier_name}</strong><div className="meta">{rec.contact_email}</div></td>
                  <td>
                    <span className={cx("chip",
                      rec.ui_state === "complete" && "ok",
                      rec.ui_state === "in-progress" && "muted",
                      rec.ui_state === "follow-up" && "warn",
                      rec.ui_state === "flagged" && "warn",
                      rec.ui_state === "no-reply" && "muted")}>
                      {rec.ui_state}
                    </span>
                  </td>
                  {audit.fields.map((f) => {
                    const cell = rec.values[f.name];
                    if (!cell) {
                      return <td key={f.id} className="meta">—</td>;
                    }
                    return (
                      <td
                        key={f.id}
                        style={{ cursor: "pointer", whiteSpace: "nowrap" }}
                        onClick={() => setPicked({ recipient: rec, field_name: f.name, cell })}
                      >
                        <div>{cell.value}</div>
                        <div className="meta" style={{ fontSize: 11 }}>conf {cell.confidence}</div>
                      </td>
                    );
                  })}
                </tr>
              ))}
              {audit.recipients.length === 0 && (
                <tr><td colSpan={audit.fields.length + 2} className="meta" style={{ padding: 24, textAlign: "center" }}>
                  No recipients yet
                </td></tr>
              )}
            </tbody>
          </table>
        </div>

        <div className="col">
          {picked ? (
            <div className="card">
              <div className="row between">
                <div className="eyebrow">Provenance</div>
                <button className="btn ghost sm" onClick={() => setPicked(null)}>✕</button>
              </div>
              <div className="text mt-4">
                <strong>{picked.recipient.supplier_name}</strong> — {audit.fields.find((f) => f.name === picked.field_name)?.label}
              </div>
              <div className="text" style={{ marginTop: 8, fontWeight: 600 }}>
                {picked.cell.value} <span className="chip ok" style={{ marginLeft: 6 }}>conf {picked.cell.confidence}</span>
              </div>
              <div className="meta mt-4">From message:</div>
              <div className="text" style={{ marginTop: 4 }}>
                {picked.cell.source_subject || <em>(no subject)</em>}
              </div>
              {picked.cell.source_received_at && (
                <div className="meta">{new Date(picked.cell.source_received_at).toLocaleString()}</div>
              )}
              {picked.cell.source_span?.text && (
                <>
                  <div className="meta mt-4">Verbatim span:</div>
                  <blockquote className="text" style={{
                    borderLeft: "3px solid var(--acc)",
                    paddingLeft: 12,
                    marginTop: 4,
                    fontStyle: "italic",
                  }}>"{picked.cell.source_span.text}"</blockquote>
                </>
              )}
              <div className="row mt-4" style={{ gap: 8 }}>
                <button
                  type="button"
                  className="btn ghost"
                  onClick={() => _authedDownload(`${live.apiBase}/recipients/${picked.recipient.id}/audit-package.pdf`, "Attestation PDF")}
                  style={{ flex: 1, textAlign: "center" }}
                >
                  ↓ Attestation PDF
                </button>
                <button
                  className="btn ghost"
                  onClick={() => { live.setSelectedRecipientId(picked.recipient.id); setScreen("thread"); }}
                  style={{ flex: 1 }}
                >
                  Open thread →
                </button>
              </div>
            </div>
          ) : (
            <div className="card spotlight">
              <div className="eyebrow acc">Audit trail</div>
              <p className="text mt-4">
                Click any cell in the table to see the supplier message that produced it,
                with the verbatim span the agent extracted from.
              </p>
              <div className="meta mt-4">
                The export downloads include a "provenance" sheet (xlsx) or per-cell
                confidence columns (csv).
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function ScreenThreadLive({ setScreen, live }) {
  // Poll the thread every 8s for the same reason — fresh replies and
  // auto-fired follow-ups appear without a manual reload.
  useEffect(() => {
    if (!live.liveMode || !live.selectedRecipientId) return;
    const t = setInterval(() => { live.refreshThread(live.selectedRecipientId); }, 8000);
    return () => clearInterval(t);
  }, [live.liveMode, live.selectedRecipientId, live.refreshThread]);

  if (!live.selectedRecipientId) {
    return (
      <div className="card">
        <h2 className="h">No supplier selected</h2>
        <p className="text" style={{ marginTop: 12 }}>Pick a row from the dashboard.</p>
        <button className="btn primary mt-4" onClick={() => setScreen("dash")}>← Dashboard</button>
      </div>
    );
  }
  const data = live.threadView;
  if (!data) {
    return <div className="card"><div className="text">Loading thread…</div></div>;
  }
  const { recipient, campaign, messages, fields } = data;
  return (
    <div data-screen-label="thread">
      <div className="screen-head">
        <div>
          <div className="screen-eyebrow">Supplier thread · live</div>
          <h1 className="screen-title">{recipient.supplier_name} · {campaign.name}</h1>
          <p className="screen-sub">
            {recipient.contact_email} · status <strong>{recipient.status}</strong> · followups {recipient.followup_count}
          </p>
        </div>
        <button className="btn ghost sm" onClick={() => setScreen("dash")}>← Dashboard</button>
      </div>

      <div className="cols-wide">
        <div style={{ maxWidth: 760 }}>
          {messages.map((m) => (
            <div key={m.id} className="card mb-4">
              <div className="row between">
                <div className="eyebrow">{m.direction === "out" ? "Outbound · agent" : "Inbound · supplier"}</div>
                <div className="meta">{new Date(m.created_at).toLocaleString()}</div>
              </div>
              <div className="text" style={{ marginTop: 8, fontWeight: 600 }}>{m.subject}</div>
              <pre className="text" style={{ marginTop: 12, whiteSpace: "pre-wrap", fontFamily: "inherit" }}>{m.body_text}</pre>
            </div>
          ))}
        </div>

        <div className="col">
          <div className="card">
            <div className="eyebrow">Extracted fields</div>
            <div className="mt-4">
              {fields.map((f) => (
                <div key={f.name} className="mb-4">
                  <div>
                    <strong>{f.label}</strong>
                    {f.required && <span className="meta" style={{ marginLeft: 6 }}>required</span>}
                  </div>
                  <div className="text" style={{ marginTop: 4 }}>
                    {f.extracted_value ? <span>{f.extracted_value}</span> : <span className="meta">— not yet extracted —</span>}
                  </div>
                  {f.source_span && f.source_span.text && (
                    <div className="meta" style={{ marginTop: 4, fontStyle: "italic" }}>
                      “{f.source_span.text}”
                    </div>
                  )}
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// =========================================================================
// Agent screen — chat-driven setup. Mounts inside the Hi-Fi shell, replaces
// the old 6-step wizard. Talks to the same backend as the standalone
// Agent.html via /agent/conversations + SSE. Class names are agent-prefixed
// to avoid colliding with the rest of the Hi-Fi stylesheet.
// =========================================================================

async function _streamAgentMessage(apiBase, cid, content, handlers, opts = {}) {
  const body = { content };
  if (opts.displayContent) body.display_content = opts.displayContent;
  if (opts.suppressUserInLog) body.suppress_user_in_log = true;
  const resp = await fetch(`${apiBase}/agent/conversations/${cid}/messages`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!resp.body) throw new Error("no response body");
  const reader = resp.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const blocks = buffer.split("\n\n");
    buffer = blocks.pop() || "";
    for (const block of blocks) {
      if (!block.trim()) continue;
      const lines = block.split("\n");
      let event = "message";
      const dataLines = [];
      for (const line of lines) {
        if (line.startsWith("event: ")) event = line.slice(7);
        else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
      }
      if (!dataLines.length) continue;
      let parsed;
      try { parsed = JSON.parse(dataLines.join("\n")); } catch { continue; }
      handlers(event, parsed);
    }
  }
}

// Tool-call → human-readable label. The UI shows {label, detail}; raw JSON
// is on hover (title attr) so the curious can still see it. Add a case
// here whenever a new tool is added to setup_agent.py.
function _trim(s, n = 80) {
  if (!s) return "";
  s = String(s);
  return s.length > n ? s.slice(0, n - 1) + "…" : s;
}
function describeToolCall(name, args = {}) {
  switch (name) {
    case "list_templates":
      return { label: "Browsing templates" };
    case "request_document_upload":
      return { label: "Asking for a context document" };
    case "generate_schema":
      return {
        label: args.use_template_seed === false
          ? "Generating schema from brief"
          : "Generating schema from template",
      };
    case "mark_step_complete":
      return { label: "Confirming step", detail: args.step };
    case "inspect_state":
      return { label: "Checking current state" };
    case "edit_field":
      return {
        label: "Editing field",
        detail: args.new_label
          ? `${args.name} → ${args.new_label}`
          : args.name,
      };
    case "set_field_required":
      return {
        label: args.required ? "Marking field required" : "Marking field optional",
        detail: args.name,
      };
    case "apply_template":
      return { label: "Applying template", detail: args.campaign_name || args.template_id };
    case "create_blank_campaign":
      return { label: "Creating campaign", detail: args.campaign_name };
    case "set_brief":
      return { label: "Updating brief", detail: _trim(args.brief, 70) };
    case "add_email_directive":
      return { label: "Adding email directive", detail: _trim(args.directive, 90) };
    case "clear_email_directives":
      return { label: "Clearing email directives" };
    case "add_field":
      return { label: "Adding field", detail: args.label || args.name };
    case "remove_field":
      return { label: "Removing field", detail: args.name };
    case "add_recipients": {
      const list = Array.isArray(args.recipients) ? args.recipients : [];
      const n = list.length;
      const first = list[0] || {};
      const sample = first.contact_email || first.supplier_name || null;
      const more = n > 1 ? ` (+${n - 1} more)` : "";
      return {
        label: `Adding ${n} recipient${n === 1 ? "" : "s"}`,
        detail: sample ? `${sample}${more}` : null,
      };
    }
    case "remove_recipient":
      return { label: "Removing recipient", detail: args.email };
    case "preview_email":
      return { label: "Generating email preview" };
    case "launch_campaign":
      return { label: "Launching campaign" };
    default: {
      // Fallback: humanize the name + use the first scalar arg as detail.
      const pretty = name.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
      const firstScalar = Object.entries(args).find(
        ([, v]) => v != null && typeof v !== "object"
      );
      return {
        label: pretty,
        detail: firstScalar ? _trim(firstScalar[1], 80) : null,
      };
    }
  }
}

// Reusable wrapper — each panel renders its own header (clickable to
// toggle), shows a green check when done, and hides its body when
// collapsed. The underlying panel components only describe the body.
function AgentSection({ id, title, count, done, open, onToggle, headerExtra, children, emptyText, step }) {
  return (
    <div className={cx("agent-panel", done && "done", !open && "collapsed")}>
      <header onClick={onToggle} role="button" tabIndex={0}
        onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onToggle(); } }}>
        <h3>
          {step != null ? (
            <span className={cx("agent-step-badge", done && "done")} aria-hidden="true">
              {done ? "✓" : step}
            </span>
          ) : done ? (
            <span className="agent-done-mark" aria-hidden="true">✓</span>
          ) : null}
          {title}
        </h3>
        <span className="agent-section-meta">
          {count != null ? <span className="agent-pill">{count}</span> : null}
          {headerExtra || null}
          <span className="agent-caret" aria-hidden="true">{open ? "▾" : "▸"}</span>
        </span>
      </header>
      {open ? (
        <div className={cx("agent-panel-body", emptyText && !children && "empty")}>
          {children || emptyText}
        </div>
      ) : null}
    </div>
  );
}

function AgentCampaignPanel({ snapshot, open, done, onToggle }) {
  const c = snapshot.campaign;
  if (!c) {
    return (
      <AgentSection id="campaign" title="Campaign" step={1} open={open} done={done} onToggle={onToggle}
        emptyText="No campaign yet — pick a use-case in chat to start." />
    );
  }
  return (
    <AgentSection id="campaign" title="Campaign" step={1} open={open} done={done} onToggle={onToggle}
      headerExtra={<span className={cx("agent-status", c.status)}>{c.status}</span>}>
      <div className="agent-kv">
        <div className="k">Name</div><div className="v">{c.name}</div>
        <div className="k">ID</div><div className="v" style={{ fontFamily: "ui-monospace, monospace", fontSize: 11 }}>{c.id}</div>
      </div>
    </AgentSection>
  );
}

function AgentBriefPanel({ snapshot, open, done, onToggle, live, onMutated }) {
  const c = snapshot.campaign;
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState("");
  const [busy, setBusy] = useState(false);

  if (!c) {
    return (
      <AgentSection
        id="brief"
        title="What is this data collection about?"
        step={2}
        open={open}
        done={false}
        onToggle={onToggle}
        emptyText="Pick a use-case first — the agent will seed this from the template."
      />
    );
  }

  const startEdit = (e) => {
    e.stopPropagation();
    setDraft(c.brief || "");
    setEditing(true);
  };

  const save = async () => {
    if (busy) return;
    setBusy(true);
    try {
      await live.patchCampaign(c.id, { brief: draft });
      setEditing(false);
      onMutated && onMutated();
    } catch (err) {
      alert(`Couldn't save: ${err.message}`);
    } finally {
      setBusy(false);
    }
  };

  const headerExtra = !editing && live ? (
    <button className="agent-edit-btn" onClick={startEdit} title="Edit brief">✎</button>
  ) : null;

  return (
    <AgentSection id="brief" title="What is this data collection about?" step={2} open={open} done={done} onToggle={onToggle}
      headerExtra={headerExtra}
      emptyText={!c.brief && !editing ? "Not set yet. The agent will seed this from the template." : null}>
      {editing ? (
        <div style={{ display: "flex", flexDirection: "column", gap: 12, paddingBottom: 6 }}>
          {/* Hard-cap the textarea height + disable manual resize so no
              brief, no matter how long, can push the action buttons out
              of view. The textarea scrolls internally when content
              exceeds the cap. */}
          <textarea
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            rows={5}
            style={{
              width: "100%",
              boxSizing: "border-box",
              padding: "10px 12px",
              border: "1px solid var(--border-2)",
              borderRadius: 8,
              fontSize: 13,
              fontFamily: "inherit",
              lineHeight: 1.5,
              resize: "none",
              maxHeight: 220,
              overflowY: "auto",
            }}
          />
          <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", flexShrink: 0 }}>
            <button className="btn quiet sm" onClick={() => setEditing(false)} disabled={busy}>Cancel</button>
            <button className="btn primary sm" onClick={save} disabled={busy}>{busy ? "Saving…" : "Save"}</button>
          </div>
        </div>
      ) : (
        c.brief ? <div className="agent-brief">{c.brief}</div> : null
      )}
    </AgentSection>
  );
}

// Single row inside the "Data to collect" panel. When the panel is in
// edit mode, each row has its own ✎ button to flip into an inline form
// for the label / description / required fields. Type and name aren't
// patchable on the backend (changing type after extractions exist would
// be unsafe) so they're shown read-only.
function EditableFieldRow({ field, editing, live, campaignId, onMutated, rowBusy, onDelete }) {
  const [editingThis, setEditingThis] = useState(false);
  const [label, setLabel] = useState(field.label || "");
  const [description, setDescription] = useState(field.description || "");
  const [required, setRequired] = useState(!!field.required);
  const [saving, setSaving] = useState(false);

  // Whenever the panel exits edit mode, fold this row back to readonly.
  useEffect(() => { if (!editing) setEditingThis(false); }, [editing]);

  const startEdit = () => {
    setLabel(field.label || "");
    setDescription(field.description || "");
    setRequired(!!field.required);
    setEditingThis(true);
  };

  const save = async () => {
    if (saving || !label.trim()) return;
    setSaving(true);
    try {
      await live.patchField(campaignId, field.id, {
        label: label.trim(),
        description: description.trim(),
        required,
      });
      setEditingThis(false);
      onMutated && onMutated();
    } catch (err) {
      alert(`Couldn't save: ${err.message}`);
    } finally {
      setSaving(false);
    }
  };

  if (editing && editingThis) {
    return (
      <div className="agent-fld" style={{ display: "block" }}>
        <div style={{ display: "flex", flexDirection: "column", gap: 8, paddingBottom: 4 }}>
          <input
            type="text"
            value={label}
            onChange={(e) => setLabel(e.target.value)}
            placeholder="Label"
            autoFocus
            disabled={saving}
            style={{ width: "100%", boxSizing: "border-box", padding: "8px 10px", border: "1px solid var(--border-2)", borderRadius: 6, fontSize: 13, fontFamily: "inherit", outline: "none" }}
          />
          <textarea
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            placeholder="Description (optional — helps the agent extract)"
            rows={2}
            disabled={saving}
            style={{ width: "100%", boxSizing: "border-box", padding: "8px 10px", border: "1px solid var(--border-2)", borderRadius: 6, fontSize: 12.5, fontFamily: "inherit", outline: "none", resize: "none", maxHeight: 120, overflowY: "auto", lineHeight: 1.45 }}
          />
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
            <label style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 12, color: "var(--fg-2)" }}>
              <input
                type="checkbox"
                checked={required}
                onChange={(e) => setRequired(e.target.checked)}
                disabled={saving}
              />
              Required
            </label>
            <span className="meta" style={{ fontSize: 11, color: "var(--fg-3)" }}>
              type: <code>{field.type}</code> · name: <code>{field.name}</code>
            </span>
          </div>
          <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", paddingTop: 4 }}>
            <button className="btn quiet sm" onClick={() => setEditingThis(false)} disabled={saving}>Cancel</button>
            <button className="btn primary sm" onClick={save} disabled={saving || !label.trim()}>
              {saving ? "Saving…" : "Save"}
            </button>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="agent-fld">
      <div style={{ flex: 1, minWidth: 0 }}>
        <div className="agent-fld-label">{field.label}</div>
        {field.description ? <div className="agent-fld-desc">{field.description}</div> : null}
      </div>
      <div className="agent-fld-meta">
        {field.required ? <span className="agent-req">required</span> : null}
        {editing ? (
          <>
            <button
              className="agent-edit-btn"
              onClick={startEdit}
              disabled={rowBusy}
              title="Edit"
              style={{ padding: "1px 6px", fontSize: 11 }}
            >✎</button>
            <button
              className="agent-row-del"
              onClick={onDelete}
              disabled={rowBusy}
              title="Remove"
            >
              {rowBusy ? "…" : "✕"}
            </button>
          </>
        ) : null}
      </div>
    </div>
  );
}

function AgentFieldsPanel({ fields, open, done, onToggle, live, campaignId, onMutated }) {
  const [editing, setEditing] = useState(false);
  const [busy, setBusy] = useState(null); // field_id being mutated, or "add"
  const [addLabel, setAddLabel] = useState("");
  const [addType, setAddType] = useState("string");
  const [addRequired, setAddRequired] = useState(false);

  const remove = async (fid) => {
    if (busy) return;
    if (!confirm("Remove this data point?")) return;
    setBusy(fid);
    try {
      await live.deleteField(campaignId, fid);
      onMutated && onMutated();
    } catch (err) {
      alert(`Couldn't delete: ${err.message}`);
    } finally {
      setBusy(null);
    }
  };

  const add = async () => {
    if (busy || !addLabel.trim()) return;
    setBusy("add");
    try {
      // Auto-generate a snake_case name from the label.
      const name = addLabel.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
      await live.addField(campaignId, {
        name: name || `field_${Date.now()}`,
        label: addLabel.trim(),
        type: addType,
        required: addRequired,
        description: "",
      });
      setAddLabel("");
      setAddType("string");
      setAddRequired(false);
      onMutated && onMutated();
    } catch (err) {
      alert(`Couldn't add: ${err.message}`);
    } finally {
      setBusy(null);
    }
  };

  const headerExtra = live && campaignId ? (
    <button
      className="agent-edit-btn"
      onClick={(e) => { e.stopPropagation(); setEditing((v) => !v); }}
      title={editing ? "Done editing" : "Edit data points"}
    >
      {editing ? "Done" : "✎"}
    </button>
  ) : null;

  return (
    <AgentSection id="fields" title="Data to collect" step={3} count={fields.length}
      open={open} done={done} onToggle={onToggle}
      headerExtra={headerExtra}
      emptyText={fields.length === 0 && !editing ? "No data points yet." : null}>
      {fields.map((f) => (
        <EditableFieldRow
          key={f.id}
          field={f}
          editing={editing}
          live={live}
          campaignId={campaignId}
          onMutated={onMutated}
          rowBusy={busy === f.id}
          onDelete={() => remove(f.id)}
        />
      ))}
      {editing ? (
        <div className="agent-add-row">
          <input
            type="text"
            placeholder="Label (e.g. SKU)"
            value={addLabel}
            onChange={(e) => setAddLabel(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") add(); }}
            disabled={busy === "add"}
          />
          <select
            value={addType}
            onChange={(e) => setAddType(e.target.value)}
            disabled={busy === "add"}
          >
            <option value="string">string</option>
            <option value="number">number</option>
            <option value="enum">enum</option>
            <option value="date">date</option>
            <option value="boolean">boolean</option>
            <option value="file">file</option>
            <option value="list">list</option>
          </select>
          <label style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 12 }}>
            <input
              type="checkbox"
              checked={addRequired}
              onChange={(e) => setAddRequired(e.target.checked)}
              disabled={busy === "add"}
            />
            req
          </label>
          <button
            className="btn primary sm"
            onClick={add}
            disabled={busy === "add" || !addLabel.trim()}
          >
            {busy === "add" ? "Adding…" : "+ Add"}
          </button>
        </div>
      ) : null}
    </AgentSection>
  );
}

function AgentPartsPanel({ parts, open, done, onToggle, campaignExists }) {
  const list = parts || [];
  if (!campaignExists) {
    return (
      <AgentSection
        id="parts"
        title="Parts in scope"
        step={4}
        open={open}
        done={false}
        onToggle={onToggle}
        emptyText="Pick a use-case first — then tell the agent which MPNs or URLs the data applies to."
      />
    );
  }
  return (
    <AgentSection
      id="parts"
      title="Parts in scope"
      step={4}
      count={list.length}
      open={open}
      done={done}
      onToggle={onToggle}
      emptyText={list.length === 0 ? "Tell the agent which MPNs or product URLs apply — it'll look them up and reference them in the email." : null}
    >
      {list.length > 0 ? (
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {list.map((p, i) => (
            <div key={i} style={{ padding: 10, border: "1px solid var(--border-1)", borderRadius: 8, background: "white" }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 8 }}>
                <div style={{ fontWeight: 600, fontSize: 13, color: "var(--fg-0)" }}>
                  {p.mpn || p.name || "(part)"}
                </div>
                {p.name && p.mpn ? <div style={{ fontSize: 11, color: "var(--fg-2)" }}>{p.name}</div> : null}
              </div>
              {p.description ? (
                <div style={{ fontSize: 12, color: "var(--fg-2)", marginTop: 4, lineHeight: 1.4 }}>{p.description}</div>
              ) : null}
              {(p.source_url || p.url) ? (
                <a href={p.source_url || p.url} target="_blank" rel="noreferrer"
                  style={{ fontSize: 11, color: "var(--sb-ultra-violet)", marginTop: 4, display: "inline-block", wordBreak: "break-all" }}>
                  {p.source_url || p.url}
                </a>
              ) : null}
            </div>
          ))}
        </div>
      ) : null}
    </AgentSection>
  );
}

function AgentRecipientsPanel({ recipients, open, done, onToggle, live, campaignId, onMutated }) {
  const [editing, setEditing] = useState(false);
  const [busy, setBusy] = useState(null);
  const [addName, setAddName] = useState("");
  const [addEmail, setAddEmail] = useState("");
  const [contacts, setContacts] = useState([]);

  // Load the cross-campaign contacts list when edit mode opens, for the
  // datalist autocomplete on the email input. Filtered to suppliers
  // not already on this campaign so the dropdown is only useful suggestions.
  useEffect(() => {
    if (!editing || !live?.fetchContacts) return;
    let cancelled = false;
    (async () => {
      try {
        const data = await live.fetchContacts();
        if (!cancelled) {
          const onCampaign = new Set((recipients || []).map((r) => r.contact_email.toLowerCase()));
          setContacts(data.filter((c) => !onCampaign.has(c.email.toLowerCase())));
        }
      } catch {}
    })();
    return () => { cancelled = true; };
  }, [editing, live, recipients]);

  // When the user picks an email from the datalist, auto-fill the name
  // from the matching contact (only if the user hasn't typed a name).
  function onEmailChange(value) {
    setAddEmail(value);
    if (!addName.trim()) {
      const match = contacts.find((c) => c.email.toLowerCase() === value.trim().toLowerCase());
      if (match) setAddName(match.name);
    }
  }

  const remove = async (rid) => {
    if (busy) return;
    if (!confirm("Remove this recipient?\n\nDeletes their messages, drafts, and extractions too.")) return;
    setBusy(rid);
    try {
      await live.deleteRecipient(rid);
      onMutated && onMutated();
    } catch (err) {
      alert(`Couldn't delete: ${err.message}`);
    } finally {
      setBusy(null);
    }
  };

  const add = async () => {
    if (busy || !addEmail.trim()) return;
    setBusy("add");
    try {
      await live.addRecipient(campaignId, {
        supplier_name: (addName.trim() || addEmail.split("@")[0]),
        contact_email: addEmail.trim().toLowerCase(),
      });
      setAddName("");
      setAddEmail("");
      onMutated && onMutated();
    } catch (err) {
      alert(`Couldn't add: ${err.message}`);
    } finally {
      setBusy(null);
    }
  };

  const headerExtra = live && campaignId ? (
    <button
      className="agent-edit-btn"
      onClick={(e) => { e.stopPropagation(); setEditing((v) => !v); }}
      title={editing ? "Done editing" : "Edit recipients"}
    >
      {editing ? "Done" : "✎"}
    </button>
  ) : null;

  return (
    <AgentSection id="recipients" title="Recipients" step={5} count={recipients.length}
      open={open} done={done} onToggle={onToggle}
      headerExtra={headerExtra}
      emptyText={recipients.length === 0 && !editing ? "No recipients yet — paste a list in chat." : null}>
      {recipients.map((r) => (
        <div key={r.id} className="agent-rcpt" style={{ position: "relative" }}>
          <div className="agent-rcpt-name">{r.supplier_name}</div>
          <div className="agent-rcpt-email">{r.contact_email}</div>
          <div style={{ marginTop: 4 }}>
            <span className={cx("agent-status", r.status)}>{r.status.replace(/_/g, " ")}</span>
          </div>
          {editing ? (
            <button
              className="agent-row-del"
              onClick={() => remove(r.id)}
              disabled={busy === r.id}
              title="Remove"
              style={{ position: "absolute", top: 8, right: 8 }}
            >
              {busy === r.id ? "…" : "✕"}
            </button>
          ) : null}
        </div>
      ))}
      {editing ? (
        <div className="agent-add-row">
          <input
            type="text"
            placeholder="Name (optional)"
            value={addName}
            onChange={(e) => setAddName(e.target.value)}
            disabled={busy === "add"}
            style={{ flex: 1 }}
          />
          <input
            type="email"
            list="recipients-contacts-list"
            placeholder={contacts.length > 0 ? "type or pick from existing…" : "email@supplier.com"}
            value={addEmail}
            onChange={(e) => onEmailChange(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") add(); }}
            disabled={busy === "add"}
            style={{ flex: 2 }}
          />
          {/* Native datalist — typing in the email input shows matching
              past suppliers; picking one auto-fills the name. Filtered
              earlier to exclude suppliers already on this campaign. */}
          {contacts.length > 0 ? (
            <datalist id="recipients-contacts-list">
              {contacts.map((c) => (
                <option key={c.email} value={c.email}>{c.name}</option>
              ))}
            </datalist>
          ) : null}
          <button
            className="btn primary sm"
            onClick={add}
            disabled={busy === "add" || !addEmail.trim()}
          >
            {busy === "add" ? "Adding…" : "+ Add"}
          </button>
          {contacts.length > 0 ? (
            <div className="meta" style={{ width: "100%", fontSize: 11, color: "var(--fg-3)", marginTop: 2 }}>
              {contacts.length} existing supplier{contacts.length === 1 ? "" : "s"} available — start typing to pick.
            </div>
          ) : null}
        </div>
      ) : null}
    </AgentSection>
  );
}

function AgentDirectivesPanel({ directives, open, onToggle }) {
  if (!directives || directives.length === 0) return null;
  return (
    <AgentSection id="directives" title="Email directives" count={directives.length}
      open={open} done={false} onToggle={onToggle}>
      <ul style={{ margin: 0, paddingLeft: 18, fontSize: 13 }}>
        {directives.map((d, i) => <li key={i} style={{ marginBottom: 6 }}>{d}</li>)}
      </ul>
    </AgentSection>
  );
}

function AgentEmailPreviewPanel({ preview, previewSet, open, done, onToggle, live, campaignId, onMutated, gated, recipientCount, campaignStatus, loading, onLaunchState }) {
  const [editing, setEditing] = useState(false);
  const [subject, setSubject] = useState("");
  const [body, setBody] = useState("");
  const [busy, setBusy] = useState(false);
  const [launching, setLaunching] = useState(false);
  const [launchResult, setLaunchResult] = useState(null);
  // Index into previewSet.previews when in multi-recipient mode.
  // Single-recipient mode keeps using `preview`.
  const [previewIdx, setPreviewIdx] = useState(0);

  // Multi-recipient mode: pick the active preview by index. Falls back
  // to the single-recipient `preview` prop for older / 1-recipient
  // campaigns. `previewSet.unassigned_mpns` is what the LLM router
  // couldn't confidently place — surfaced as a small warning below
  // the cycle nav.
  const previews = previewSet?.previews || null;
  const unassignedMpns = previewSet?.unassigned_mpns || [];
  const safeIdx = previews ? Math.min(previewIdx, previews.length - 1) : 0;
  const activePreview = previews && previews.length > 0
    ? {
        subject: previews[safeIdx].subject,
        body_text: previews[safeIdx].body_text,
        body_html: previews[safeIdx].body_html,
        sample_supplier_name: previews[safeIdx].recipient_name,
        sample_email: previews[safeIdx].recipient_email,
        is_override: !!previews[safeIdx].is_override,
        updated_at: previews[safeIdx].updated_at ? new Date(previews[safeIdx].updated_at).getTime() : null,
        part_count: previews[safeIdx].part_count,
      }
    : preview;

  const stamp = activePreview?.updated_at ? new Date(activePreview.updated_at).toLocaleTimeString() : null;
  const isOverride = !!activePreview?.is_override;

  // Show the generating state while a fetch is in flight AND what we'd
  // otherwise render is wrong: nothing at all, or the stale singular
  // preview ([Sample Supplier]) when the per-recipient set is what's
  // coming. A refresh that merely updates an already-correct view keeps
  // the current content — no spinner flash.
  const generating = !!loading && !editing
    && (recipientCount >= 2 ? !previews : !activePreview);

  const startEdit = (e) => {
    e.stopPropagation();
    setSubject(activePreview?.subject || "");
    setBody(activePreview?.body_text || "");
    setEditing(true);
  };

  const save = async () => {
    if (busy || !subject.trim() || !body.trim()) return;
    setBusy(true);
    try {
      await live.setEmailOverride(campaignId, { subject: subject.trim(), body_text: body });
      setEditing(false);
      onMutated && onMutated();
    } catch (err) {
      alert(`Couldn't save: ${err.message}`);
    } finally {
      setBusy(false);
    }
  };

  const revert = async () => {
    if (busy) return;
    if (!confirm("Revert to the agent-generated email?\n\nYour edits will be lost.")) return;
    setBusy(true);
    try {
      await live.clearEmailOverride(campaignId);
      setEditing(false);
      onMutated && onMutated();
    } catch (err) {
      alert(`Couldn't revert: ${err.message}`);
    } finally {
      setBusy(false);
    }
  };

  const headerExtra = (() => {
    if (editing || generating) return null;
    const items = [];
    if (isOverride) {
      items.push(<span key="edited" className="agent-pill" style={{ background: "var(--sb-pale-amethyst)", color: "var(--sb-amethyst)" }} title="This first-touch is using your edits, not the agent's draft">edited</span>);
    } else if (activePreview) {
      items.push(<span key="stamp" className="agent-pill" title="time of last regeneration">updated {stamp}</span>);
    }
    if (activePreview && live && campaignId) {
      items.push(<button key="edit" className="agent-edit-btn" onClick={startEdit} title="Edit email" style={{ marginLeft: 4 }}>✎</button>);
    }
    return items.length ? <>{items}</> : null;
  })();

  return (
    <AgentSection id="email" title="First-touch email preview" step={6}
      open={open} done={done} onToggle={onToggle}
      headerExtra={headerExtra}
      emptyText={!activePreview && !editing && !generating
        ? (gated ? "Add recipients to populate the first-touch draft." : "Ask the agent for a preview before launch.")
        : null}>
      {generating ? (
        <div className="agent-thinking" aria-live="polite" style={{ padding: "10px 0" }}>
          <span className="agent-thinking-spinner" />
          <div className="agent-thinking-label">
            {recipientCount >= 2
              ? "Agent is generating the emails — one per supplier…"
              : "Agent is generating the email preview…"}
          </div>
        </div>
      ) : editing ? (
        <div style={{ display: "flex", flexDirection: "column", gap: 10, paddingBottom: 6 }}>
          <div className="meta" style={{ fontSize: 11, color: "var(--fg-3)" }}>
            Tokens <code>{"{{first_name}}"}</code> and <code>{"{{supplier}}"}</code> are
            substituted per recipient at send time.
          </div>
          <input
            type="text"
            value={subject}
            onChange={(e) => setSubject(e.target.value)}
            placeholder="Subject"
            disabled={busy}
            style={{ width: "100%", boxSizing: "border-box", padding: "10px 12px", border: "1px solid var(--border-2)", borderRadius: 8, fontSize: 13, fontFamily: "inherit", outline: "none", fontWeight: 600 }}
          />
          <textarea
            value={body}
            onChange={(e) => setBody(e.target.value)}
            rows={10}
            disabled={busy}
            placeholder="Email body"
            style={{ width: "100%", boxSizing: "border-box", padding: "10px 12px", border: "1px solid var(--border-2)", borderRadius: 8, fontSize: 13, fontFamily: "inherit", outline: "none", lineHeight: 1.55, resize: "none", maxHeight: 320, overflowY: "auto" }}
          />
          <div style={{ display: "flex", gap: 8, justifyContent: "space-between", flexShrink: 0 }}>
            <div>
              {isOverride ? (
                <button className="btn quiet sm" onClick={revert} disabled={busy} title="Drop the override; agent regenerates from brief + schema">↺ Revert to agent</button>
              ) : null}
            </div>
            <div style={{ display: "flex", gap: 8 }}>
              <button className="btn quiet sm" onClick={() => setEditing(false)} disabled={busy}>Cancel</button>
              <button className="btn primary sm" onClick={save} disabled={busy || !subject.trim() || !body.trim()}>
                {busy ? "Saving…" : "Save"}
              </button>
            </div>
          </div>
        </div>
      ) : activePreview ? (
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          {/* Cycle nav — only when there are ≥2 per-recipient previews.
              Lets the operator click through one draft per supplier
              before launching. Each draft is scoped to that supplier's
              auto-assigned parts. */}
          {previews && previews.length >= 2 ? (
            <div style={{
              display: "flex", alignItems: "center", justifyContent: "space-between",
              gap: 8, padding: "8px 10px", borderRadius: 8,
              background: "var(--bg-1)", border: "1px solid var(--border-1)",
            }}>
              <button
                className="filter-chip"
                onClick={() => setPreviewIdx((i) => (i - 1 + previews.length) % previews.length)}
                style={{ padding: "4px 10px", fontSize: 12 }}
                title="Previous supplier"
              >‹</button>
              <div style={{ flex: 1, textAlign: "center", minWidth: 0 }}>
                <div style={{ fontSize: 11, color: "var(--fg-2)", textTransform: "uppercase", letterSpacing: ".04em" }}>
                  Supplier {safeIdx + 1} of {previews.length}
                </div>
                <div style={{ fontWeight: 600, fontSize: 13, color: "var(--fg-0)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                  {previews[safeIdx].recipient_name}
                </div>
                <div style={{ fontSize: 11, color: "var(--fg-2)" }}>
                  {previews[safeIdx].part_count} part{previews[safeIdx].part_count === 1 ? "" : "s"}
                </div>
              </div>
              <button
                className="filter-chip"
                onClick={() => setPreviewIdx((i) => (i + 1) % previews.length)}
                style={{ padding: "4px 10px", fontSize: 12 }}
                title="Next supplier"
              >›</button>
            </div>
          ) : null}
          {previews && previews.length >= 2 && unassignedMpns.length > 0 ? (
            <div style={{ fontSize: 11, color: "var(--warm, #a86a00)", padding: "4px 10px" }}>
              {unassignedMpns.length} part{unassignedMpns.length === 1 ? "" : "s"} couldn't be routed to a supplier yet
              ({unassignedMpns.slice(0, 3).join(", ")}{unassignedMpns.length > 3 ? "…" : ""}) — assign manually before launch.
            </div>
          ) : null}
          <div className="agent-email">
            <div className="agent-email-subject">{activePreview.subject}</div>
            <div className="agent-email-meta">
              To: {activePreview.sample_supplier_name || activePreview.sample_supplier} &lt;{activePreview.sample_email}&gt;
              {activePreview.part_count != null && !(previews && previews.length >= 2)
                ? ` · ${activePreview.part_count} part${activePreview.part_count === 1 ? "" : "s"}`
                : ""}
            </div>
            {activePreview.body_html ? (
              /* HTML variant — what mail clients actually render (incl.
                 the parts table). Backend-generated, inline-styled. */
              <div
                className="agent-email-body"
                style={{ whiteSpace: "normal", fontFamily: "inherit" }}
                dangerouslySetInnerHTML={{ __html: activePreview.body_html }}
              />
            ) : (
              <div className="agent-email-body">{activePreview.body_text}</div>
            )}
          </div>
          {/* Launch button — always available from the panel so it works
              in both auto-pilot (no chat present_choices) and step-by-step
              flows. Disabled until a recipient exists; hidden once the
              campaign is already active (status flips on first launch). */}
          {live && campaignId && campaignStatus === "draft" ? (
            <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
              <button
                className="btn primary"
                style={{ width: "100%", padding: "12px 14px", justifyContent: "center", fontSize: 14 }}
                disabled={launching || !recipientCount}
                onClick={async () => {
                  if (launching) return;
                  const n = recipientCount || 0;
                  if (!n) return;
                  if (!confirm(`Launch first-touch to ${n} recipient${n === 1 ? "" : "s"}?`)) return;
                  setLaunching(true);
                  setLaunchResult(null);
                  onLaunchState && onLaunchState({ phase: "sending" });
                  try {
                    const r = await live.launchCampaign(campaignId);
                    const sentCount = Array.isArray(r?.sent) ? r.sent.length : (r?.sent ?? n);
                    setLaunchResult({ ok: true, sent: sentCount });
                    onLaunchState && onLaunchState({ phase: "done", sent: sentCount });
                    onMutated && onMutated();
                  } catch (err) {
                    setLaunchResult({ ok: false, error: err.message });
                    onLaunchState && onLaunchState(null);
                  } finally {
                    setLaunching(false);
                  }
                }}
              >
                {launching ? "Launching…" : `🚀 Launch campaign${recipientCount ? ` (${recipientCount})` : ""}`}
              </button>
              {!recipientCount ? (
                <div style={{ fontSize: 11, color: "var(--fg-2)", textAlign: "center" }}>
                  Add at least one recipient before launching.
                </div>
              ) : null}
              {launchResult?.ok ? (
                <div style={{ fontSize: 12, color: "#186e44", textAlign: "center" }}>
                  ✓ Launched — first-touch is going out.
                </div>
              ) : null}
              {launchResult && !launchResult.ok ? (
                <div style={{ fontSize: 12, color: "#9b1d1d", textAlign: "center" }}>
                  Launch failed: {launchResult.error}
                </div>
              ) : null}
            </div>
          ) : campaignStatus && campaignStatus !== "draft" ? (
            <div style={{ fontSize: 12, color: "#186e44", textAlign: "center", padding: "6px 0" }}>
              Campaign is {campaignStatus}.
            </div>
          ) : null}
        </div>
      ) : null}
    </AgentSection>
  );
}

// Campaign picker — header dropdown that lists all existing campaigns and
// lets the CSM switch between them or start fresh. Selecting an item
// updates the ?campaign= URL param and reloads, which is simpler and more
// robust than mid-flight rebinding the live agent context.
// Inline downloads block — rendered when the operator agent calls
// `offer_exports`. Each item is a button styled like the choice buttons,
// but it triggers a file download instead of sending a chat message.
function ExportsBlock({ apiBase, exports }) {
  if (!exports?.length) return null;
  return (
    <div className="agent-choices" style={{ flexDirection: "column", alignItems: "flex-start" }}>
      <div className="agent-choices-q">Pick what you want to download:</div>
      <div className="agent-choices-row" style={{ flexWrap: "wrap", gap: 8 }}>
        {exports.map((ex) => (
          <button
            key={ex.id}
            className="agent-choice-btn"
            onClick={(e) => { e.preventDefault(); _authedDownload(`${apiBase}${ex.url}`, ex.label); }}
            title={ex.description || ""}
            style={{ textDecoration: "none", border: 0, font: "inherit", cursor: "pointer" }}
          >
            ↓ {ex.label}
          </button>
        ))}
      </div>
    </div>
  );
}


// Always-on attach button for the chat composer. Posts to
// /agent/conversations/{cid}/upload, which extracts text + tries to detect
// recipient lists. The agent receives a structured user message describing
// the upload and decides what to do based on the current step.
function AgentAttachButton({ apiBase, convId, busy, sendMessage }) {
  const ref = useRef(null);
  const [uploading, setUploading] = useState(false);

  async function handleFile(e) {
    const file = e.target.files?.[0];
    if (!file || !convId) return;
    e.target.value = "";  // allow uploading the same file twice
    setUploading(true);
    try {
      const fd = new FormData();
      fd.append("file", file);
      const r = await fetch(`${apiBase}/agent/conversations/${convId}/upload`, {
        method: "POST", body: fd,
      });
      if (!r.ok) throw new Error(`upload ${r.status}: ${(await r.text()).slice(0, 200)}`);
      const data = await r.json();

      // Build the agent-facing message. Branch on detected shape so the
      // agent gets a clear signal about what kind of file it is.
      let agentMsg = `[user uploaded "${data.filename}", ${data.size} bytes, ${data.char_count} chars extracted]`;
      const recips = data.parsed_recipients;
      if (recips && recips.length > 0) {
        agentMsg += `\n\nDetected ${recips.length} recipient row(s):\n`;
        const sample = recips.slice(0, 30).map((r) => {
          const fn = r.first_name ? ` (${r.first_name})` : "";
          return `- ${r.supplier_name}${fn} <${r.contact_email}>`;
        }).join("\n");
        agentMsg += sample;
        if (recips.length > 30) agentMsg += `\n... and ${recips.length - 30} more`;
        agentMsg += `\n\nIf we're at the recipients step, apply via add_recipients with the parsed list. If earlier, acknowledge and continue the current step — you'll come back to it.`;
      } else if (data.char_count > 0) {
        agentMsg += `\n\nContent excerpt (first ${Math.min(8000, data.char_count)} chars):\n${data.text_excerpt}\n\n`;
        agentMsg += `Use this as context for the current step. If we're on brief: distill into a 2–4 sentence brief and call set_brief. If we're on schema: use it to inform field choices (add_field for new ones). If we're on recipients but the file isn't tabular: ask the user what they want.`;
      } else {
        agentMsg += `\n\nThe file was unreadable as text (perhaps an image without OCR enabled). Acknowledge and ask the user what they'd like done.`;
      }

      sendMessage(agentMsg, undefined, {
        suppressUserInUI: true,
        displayContent: `📎 Uploaded "${data.filename}"`,
      });
    } catch (err) {
      sendMessage(`[upload failed: ${err.message}]`, undefined, {
        suppressUserInUI: true,
        displayContent: `📎 Upload failed: ${err.message}`,
      });
    } finally {
      setUploading(false);
    }
  }

  return (
    <>
      <input
        ref={ref}
        type="file"
        style={{ display: "none" }}
        accept=".pdf,.docx,.txt,.csv,.xlsx,.xls,.png,.jpg,.jpeg,.webp,.gif,application/pdf,text/plain,text/csv,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,image/png,image/jpeg,image/webp,image/gif"
        onChange={handleFile}
        disabled={busy || uploading || !convId}
      />
      <button
        type="button"
        onClick={() => ref.current?.click()}
        disabled={busy || uploading || !convId}
        title="Attach a file (CSV / xlsx / PDF / Word / image)"
        style={{
          padding: "8px 10px", fontSize: 16, lineHeight: 1,
          background: "transparent", border: "1px solid var(--border-2)",
          borderRadius: 8, cursor: "pointer", color: "var(--fg-2)",
          flexShrink: 0,
        }}
      >
        {uploading ? "…" : "📎"}
      </button>
    </>
  );
}

function AgentCampaignPicker({ apiBase, currentId }) {
  const [open, setOpen] = useState(false);
  const [campaigns, setCampaigns] = useState(null);
  const [error, setError] = useState(null);
  const ref = useRef(null);

  useEffect(() => {
    if (!open || campaigns) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch(`${apiBase}/campaigns`);
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        const data = await r.json();
        if (!cancelled) setCampaigns(data);
      } catch (e) {
        if (!cancelled) setError(e.message);
      }
    })();
    return () => { cancelled = true; };
  }, [open, apiBase, campaigns]);

  // Close on outside click.
  useEffect(() => {
    if (!open) return;
    function onDoc(e) {
      if (ref.current && !ref.current.contains(e.target)) setOpen(false);
    }
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open]);

  function pick(id) {
    const u = new URL(window.location.href);
    if (id) u.searchParams.set("campaign", id);
    else u.searchParams.delete("campaign");
    window.location.href = u.toString();
  }

  const current = campaigns?.find((c) => c.id === currentId);

  return (
    <div className="agent-picker" ref={ref}>
      <button
        type="button"
        className="agent-picker-btn"
        onClick={() => setOpen((o) => !o)}
        title="Switch or start a campaign"
      >
        <span className="agent-picker-name">
          {current ? current.name : currentId ? "Loading…" : "New campaign"}
        </span>
        {current ? (
          <span className={cx("agent-status", current.status)}>{current.status}</span>
        ) : null}
        <span className="agent-picker-caret">▾</span>
      </button>
      {open ? (
        <div className="agent-picker-menu">
          <button
            className="agent-picker-item agent-picker-new"
            onClick={() => pick(null)}
          >
            <span className="plus">＋</span>
            <span>Start a new campaign</span>
          </button>
          <div className="agent-picker-sep">All campaigns</div>
          {error ? (
            <div className="agent-picker-empty">Couldn't load campaigns: {error}</div>
          ) : !campaigns ? (
            <div className="agent-picker-empty">Loading…</div>
          ) : campaigns.length === 0 ? (
            <div className="agent-picker-empty">No campaigns yet.</div>
          ) : (
            campaigns.map((c) => (
              <button
                key={c.id}
                className={cx(
                  "agent-picker-item",
                  c.id === currentId && "current"
                )}
                onClick={() => pick(c.id)}
                disabled={c.id === currentId}
              >
                <div className="agent-picker-item-main">
                  <span className="agent-picker-item-name">{c.name}</span>
                  <span className={cx("agent-status", c.status)}>{c.status}</span>
                </div>
                <div className="agent-picker-item-sub">
                  {(c.fields || []).length} field{(c.fields || []).length === 1 ? "" : "s"}
                </div>
              </button>
            ))
          )}
        </div>
      ) : null}
    </div>
  );
}

function ScreenAgent({ live, seedMessage, onSeedConsumed, onLaunched }) {
  const apiBase = live?.apiBase || _DEFAULT_API_ORIGIN;
  const [convId, setConvId] = useState(null);
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState("");
  const [busy, setBusy] = useState(false);
  const [snapshot, setSnapshot] = useState({ campaign: null, fields: [], recipients: [] });
  const [emailPreview, setEmailPreview] = useState(null);
  const msgsRef = useRef(null);

  // Launch takeover. null | {phase:"sending"} | {phase:"done", sent} —
  // covers both launch paths (the panel's 🚀 button and the agent's
  // launch_campaign tool). "done" auto-navigates to the collection
  // command center after a beat.
  const [launchOverlay, setLaunchOverlay] = useState(null);
  useEffect(() => {
    if (launchOverlay?.phase !== "done") return;
    const cid = snapshot.campaign?.id;
    const t = setTimeout(() => {
      if (cid && onLaunched) onLaunched(cid);
      else setLaunchOverlay(null);
    }, 1400);
    return () => clearTimeout(t);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [launchOverlay?.phase]);

  // Re-fetch the snapshot from /agent/conversations/{cid}/state. Called
  // by the editable side-panels after a direct mutation (Brief save,
  // field add/delete, recipient add/delete) so the UI reflects the new
  // state without going through the agent. The agent's chat history
  // doesn't know about these edits — it'll see the new state on the
  // next turn via the snapshot.
  const refreshSnapshot = useCallback(async () => {
    if (!convId) return;
    try {
      const r = await fetch(`${apiBase}/agent/conversations/${convId}/state`);
      if (r.ok) setSnapshot(await r.json());
    } catch {}
  }, [apiBase, convId]);

  // Multi-recipient preview list. Populated when the campaign has ≥2
  // recipients — each entry is one supplier's first-touch with that
  // supplier's auto-assigned parts. Single-recipient campaigns leave
  // this null and use `emailPreview` (the legacy single-shot endpoint).
  const [previewSet, setPreviewSet] = useState(null);
  // True while a preview fetch is in flight. The panel uses this to show
  // a "generating" spinner instead of a stale preview (e.g. the cached
  // [Sample Supplier] render while the per-recipient set regenerates).
  const [previewLoading, setPreviewLoading] = useState(false);

  // Ref to the latest refreshPreview. The SSE handler lives inside
  // `sendMessage` (useCallback with stable deps), so a direct closure
  // would capture a stale version of refreshPreview when
  // recipients.length changes. The ref dodges that — its `.current`
  // is repointed below every render.
  const refreshPreviewRef = useRef(null);

  // Refresh just the email preview — used by the editable email-preview
  // panel after a Save / Revert + auto-fetched once on campaign load
  // (the backend caches the rendered preview on
  // runtime_config.last_preview, so this is cheap on subsequent loads).
  const refreshPreview = useCallback(async () => {
    const cid = live?.wizardCampaignId || snapshot.campaign?.id;
    if (!cid || !live?.previewEmail) return;
    const recipientCount = (snapshot.recipients || []).length;
    setPreviewLoading(true);
    try {
      if (recipientCount >= 2 && live?.previewEmails) {
        // Multi-recipient mode — get the per-supplier draft set
        // (and trigger lazy parts-to-recipient assignment on the way).
        const data = await live.previewEmails(cid);
        setPreviewSet(data);
        const first = data?.previews?.[0];
        if (first) {
          setEmailPreview({
            subject: first.subject,
            body_text: first.body_text,
            body_html: first.body_html,
            is_override: !!first.is_override,
            sample_supplier_name: first.recipient_name,
            sample_email: first.recipient_email,
            updated_at: first.updated_at ? new Date(first.updated_at).getTime() : Date.now(),
          });
        } else {
          setEmailPreview(null);
        }
      } else {
        const data = await live.previewEmail(cid);
        const ts = data.updated_at ? new Date(data.updated_at).getTime() : Date.now();
        setEmailPreview({ ...data, updated_at: ts });
        setPreviewSet(null);
      }
    } catch {} finally {
      setPreviewLoading(false);
    }
  }, [live, snapshot.campaign?.id, (snapshot.recipients || []).length]);

  // Keep the ref pointing at the latest refreshPreview. Done in an
  // effect rather than inline so React's render is pure.
  useEffect(() => {
    refreshPreviewRef.current = refreshPreview;
  }, [refreshPreview]);

  // On first campaign load (or campaign switch), auto-fetch the preview
  // so it's already populated when the user opens the side panel. Cheap
  // when the cache exists; one LLM call on a fresh campaign.
  useEffect(() => {
    const cid = snapshot.campaign?.id;
    if (cid && !emailPreview) refreshPreview();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [snapshot.campaign?.id]);

  // Re-fetch when the recipient count crosses 2 — switches between the
  // singular and plural preview endpoints. Without this, a campaign
  // that gets an early singular preview (e.g. agent ran preview_email
  // before recipients were added) would stay stuck on the singular
  // path even after the user pastes in 5 suppliers.
  const _recipientCount = (snapshot.recipients || []).length;
  useEffect(() => {
    const cid = snapshot.campaign?.id;
    if (!cid) return;
    if (_recipientCount >= 2 && !previewSet) {
      refreshPreview();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [_recipientCount, snapshot.campaign?.id]);

  // Per-panel open state. `undefined` = follow the auto rule (collapse
  // when stage moves past). `true`/`false` = user explicitly opened or
  // closed it; respect that.
  const [userOpen, setUserOpen] = useState({});

  // Stage of the setup flow, derived from snapshot + preview.
  //   0 no campaign · 1 campaign + brief, no fields · 2 has fields ·
  //   3 has recipients · 4 has preview · 5 launched
  const stage = (() => {
    const c = snapshot.campaign;
    if (!c) return 0;
    if (c.status && c.status !== "draft") return 5;
    if (emailPreview) return 4;
    if ((snapshot.recipients || []).length > 0) return 3;
    if ((snapshot.fields || []).length > 0) return 2;
    return 1;
  })();

  // A panel is "done" only when the user has explicitly confirmed it
  // (`mark_step_complete` from the agent → runtime_config.confirmed_steps).
  // The earlier "OR stage >= N" shortcut fired too eagerly — picking a
  // template (stage 1) made the brief / data / recipients all go green
  // even though the user hadn't reviewed them yet.
  // Campaign and email remain implicit: campaign exists the moment we
  // have an id; email is "done" only when the campaign launches.
  const confirmed = new Set(snapshot.campaign?.confirmed_steps || []);
  const doneFor = {
    campaign: stage >= 1,
    brief: confirmed.has("brief"),
    fields: confirmed.has("schema"),
    parts: confirmed.has("parts"),
    recipients: confirmed.has("recipients"),
    email: stage >= 5,
  };
  // Default-open rule: open while still relevant, collapse once done.
  // User overrides win.
  const openFor = (id) =>
    userOpen[id] !== undefined ? userOpen[id] : !doneFor[id];
  const togglePanel = (id) => {
    setUserOpen((p) => ({ ...p, [id]: !openFor(id) }));
  };

  const sendMessage = useCallback(async (content, cid, opts = {}) => {
    // Backwards-compat: if opts is a boolean, treat as { suppressUserInUI: bool }.
    if (typeof opts === "boolean") opts = { suppressUserInUI: opts };
    const useCid = cid || convId;
    if (!useCid || !content.trim()) return;
    setBusy(true);
    if (!opts.suppressUserInUI) {
      setMessages((m) => [...m, { role: "user", content }]);
    }
    try {
      await _streamAgentMessage(apiBase, useCid, content, (event, data) => {
        if (event === "text") {
          setMessages((m) => _appendBeforeTrailingChoice(m, { role: "assistant", content: data.text }));
        } else if (event === "tool_call") {
          // The agent is launching — take over the screen with the
          // sending state until its tool_result lands.
          if (data.name === "launch_campaign") setLaunchOverlay({ phase: "sending" });
          // UI-primitive tools render their own widgets via dedicated SSE
          // events — skip the tool-trace line for those.
          if (data.name === "present_choices" || data.name === "request_document_upload") return;
          setMessages((m) => _appendBeforeTrailingChoice(m, { role: "tool", content: { name: data.name, args: data.args } }));
        } else if (event === "tool_result") {
          // tool_result events carry no tool name — but launch_campaign is
          // synchronous, so the first result while the overlay is in
          // "sending" is the launch outcome.
          setLaunchOverlay((prev) => {
            if (prev?.phase !== "sending") return prev;
            const r = data.result || {};
            if (r.ok === false) return null; // launch failed — agent explains in chat
            const sent = Array.isArray(r.sent) ? r.sent.length : (r.sent ?? null);
            return { phase: "done", sent };
          });
        } else if (event === "ui_choice") {
          setMessages((m) => [..._dismissOlderPendingInteractives(m), {
            role: "choices",
            content: { question: data.question, options: data.options || [], answered: false },
          }]);
        } else if (event === "ui_upload") {
          setMessages((m) => [..._dismissOlderPendingInteractives(m), {
            role: "upload",
            content: { prompt: data.prompt || "Upload a document", status: "idle", filename: null, error: null },
          }]);
        } else if (event === "state") {
          setSnapshot(data);
        } else if (event === "email_preview") {
          // SSE payload is always the SINGULAR-recipient render (the
          // agent's preview_email tool doesn't know about the
          // per-supplier split). Mirror it into emailPreview for
          // immediate paint, then trigger refreshPreview so the
          // multi-recipient endpoint runs whenever recipients.length
          // is ≥ 2 — without this, previewSet never populates and
          // the cycle nav stays hidden.
          setEmailPreview({ ...data, updated_at: Date.now() });
          if (refreshPreviewRef.current) refreshPreviewRef.current();
        } else if (event === "error") {
          setMessages((m) => [...m, { role: "assistant", content: `[error] ${data.message}` }]);
        }
      }, {
        displayContent: opts.displayContent,
        suppressUserInLog: !!opts.suppressUserInLog,
      });
    } catch (e) {
      setMessages((m) => [...m, { role: "assistant", content: `[network error] ${e.message}` }]);
    } finally {
      setBusy(false);
    }
  }, [apiBase, convId]);

  // Bootstrap: read URL for an existing campaign, create conversation,
  // and either kick off fresh OR resume with state pre-loaded.
  useEffect(() => {
    let cancelled = false;
    (async () => {
      const params = new URLSearchParams(window.location.search);
      const resumeId = params.get("campaign");
      const body = resumeId ? { campaign_id: resumeId } : (seedMessage ? { seeded: true } : {});
      let resp;
      try {
        resp = await fetch(`${apiBase}/agent/conversations`, {
          method: "POST", headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
        });
      } catch (e) {
        if (!cancelled) setMessages([{ role: "assistant", content: `[network error] ${e.message}` }]);
        return;
      }
      if (resp.status === 404 && resumeId) {
        const u = new URL(window.location.href);
        u.searchParams.delete("campaign");
        window.history.replaceState({}, "", u.toString());
        const r2 = await fetch(`${apiBase}/agent/conversations`, {
          method: "POST", headers: { "Content-Type": "application/json" },
          body: JSON.stringify({}),
        });
        const d2 = await r2.json();
        if (cancelled) return;
        setConvId(d2.conversation_id);
        setMessages([
          { role: "assistant", content: "That campaign link is no longer valid. Starting a fresh setup." },
          ..._replayDisplayLog(d2.display_log || []),
        ]);
        return;
      }
      const data = await resp.json();
      if (cancelled) return;
      setConvId(data.conversation_id);
      if (resumeId) {
        // Resume: paint the saved chat log first, then load current state
        // into the side panels. The agent's history is already restored
        // server-side from the same display log's twin (agent_history).
        if (Array.isArray(data.display_log) && data.display_log.length > 0) {
          // Replay persisted log; the last choice/upload block stays
          // interactive if the user never replied to it. Older blocks
          // are disabled (history).
          setMessages(_replayDisplayLog(data.display_log));
        }
        try {
          const sResp = await fetch(`${apiBase}/agent/conversations/${data.conversation_id}/state`);
          const snap = await sResp.json();
          if (!cancelled) {
            setSnapshot(snap);
            // Only show a "Resumed" line when there's no saved log to
            // avoid duplicating the welcome assistant-message.
            if (!data.display_log || data.display_log.length === 0) {
              setMessages([{
                role: "assistant",
                content: snap.campaign
                  ? `Resumed “${snap.campaign.name}”. ${snap.fields.length} field${snap.fields.length === 1 ? "" : "s"}, ${snap.recipients.length} recipient${snap.recipients.length === 1 ? "" : "s"} loaded. What would you like to do next?`
                  : "Resumed your campaign. What would you like to do next?",
              }]);
            }
          }
        } catch {}
      } else {
        // Fresh setup conversation. If we were handed a seed message
        // (e.g. from the Inbox "convert to new campaign" flow), use
        // that as the first user turn so the agent has the email's
        // content as immediate context. Otherwise the backend already
        // served the canned greeting + template picker in display_log —
        // render it directly, no LLM turn needed.
        if (seedMessage) {
          sendMessage(seedMessage, data.conversation_id, {
            suppressUserInUI: true,
            displayContent: "📥 Imported from inbox — starting setup with the forwarded thread as context.",
          });
          if (onSeedConsumed) onSeedConsumed();
        } else {
          setMessages(_replayDisplayLog(data.display_log || []));
        }
      }
    })();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Sync URL ↔ campaign id. When live hook is available, route through it
  // so switching to Dashboard reflects the new campaign without a refresh.
  useEffect(() => {
    const id = snapshot.campaign?.id;
    if (!id) return;
    if (live?.setWizardCampaignId) {
      if (live.wizardCampaignId !== id) live.setWizardCampaignId(id);
      return;
    }
    const url = new URL(window.location.href);
    if (url.searchParams.get("campaign") === id) return;
    url.searchParams.set("campaign", id);
    window.history.replaceState({}, "", url.toString());
  }, [snapshot.campaign?.id]);

  useEffect(() => {
    const el = msgsRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [messages]);

  // Send a doc to /campaigns/{id}/seed-from-document, then forward the
  // suggestions to the agent as a structured user message so it can apply
  // them via set_brief / add_field / etc.
  async function uploadDoc(idx, file) {
    const campaignId = snapshot.campaign?.id;
    if (!campaignId) {
      setMessages((m) => {
        const copy = [...m];
        if (copy[idx] && copy[idx].role === "upload") {
          copy[idx] = { ...copy[idx], content: { ...copy[idx].content, status: "error", error: "no campaign yet" } };
        }
        return copy;
      });
      return;
    }
    setMessages((m) => {
      const copy = [...m];
      if (copy[idx] && copy[idx].role === "upload") {
        copy[idx] = { ...copy[idx], content: { ...copy[idx].content, status: "uploading", filename: file.name } };
      }
      return copy;
    });
    let payload;
    try {
      const fd = new FormData();
      fd.append("file", file);
      const r = await fetch(`${apiBase}/campaigns/${campaignId}/seed-from-document`, {
        method: "POST",
        body: fd,
      });
      if (!r.ok) throw new Error(`HTTP ${r.status} — ${(await r.text()).slice(0, 160)}`);
      payload = await r.json();
    } catch (e) {
      setMessages((m) => {
        const copy = [...m];
        if (copy[idx] && copy[idx].role === "upload") {
          copy[idx] = { ...copy[idx], content: { ...copy[idx].content, status: "error", error: e.message } };
        }
        return copy;
      });
      return;
    }
    setMessages((m) => {
      const copy = [...m];
      if (copy[idx] && copy[idx].role === "upload") {
        copy[idx] = { ...copy[idx], content: { ...copy[idx].content, status: "done" } };
      }
      return copy;
    });
    // Hand off to the agent. Display = short confirmation; what the agent
    // sees = the full structured suggestions so it can act.
    const agentMsg =
      `I uploaded "${payload.filename}". The seeder suggests:\n\n` +
      `Brief:\n${payload.suggested_brief}\n\n` +
      `Fields:\n${(payload.suggested_fields || []).map((f, i) =>
        `${i + 1}. ${f.name} (${f.type}${f.required ? ", required" : ""}) — ${f.label || ""}: ${f.description || ""}`
      ).join("\n")}\n\n` +
      `Please apply: call set_brief with the suggested brief, then reconcile the schema (add_field for new ones, edit_field for tweaks, leave existing fields unless they clearly conflict).`;
    sendMessage(agentMsg, undefined, {
      suppressUserInUI: true,
      displayContent: `Uploaded "${payload.filename}" — applying suggestions.`,
    });
  }

  function pickChoice(idx, option) {
    setMessages((m) => {
      const copy = [...m];
      const blk = copy[idx];
      if (blk && blk.role === "choices") {
        copy[idx] = { ...blk, content: { ...blk.content, answered: true, picked: option.id } };
      }
      return copy;
    });
    setMessages((m) => [...m, { role: "user", content: option.label }]);
    // suppressUserInUI: we already added the label as a user message
    // above (so the user sees their own click immediately).
    // displayContent: the persisted log should also show the label, not
    // the canonical send_message.
    sendMessage(option.send_message, undefined, {
      suppressUserInUI: true,
      displayContent: option.label,
    });
  }

  function onKey(e) {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      const v = input.trim();
      if (!v) return;
      setInput("");
      sendMessage(v);
    }
  }

  const launchRecipients = (snapshot.recipients || []).length;
  return (
    <div className="agent-shell">
      {launchOverlay ? (
        <div style={{
          position: "fixed", inset: 0, zIndex: 1000,
          background: "rgba(23,0,59,0.45)", backdropFilter: "blur(2px)",
          display: "flex", alignItems: "center", justifyContent: "center",
        }}>
          <div style={{
            background: "white", borderRadius: 14, padding: "28px 36px",
            boxShadow: "0 12px 48px rgba(20,18,40,.25)", textAlign: "center",
            minWidth: 320,
          }}>
            {launchOverlay.phase === "sending" ? (
              <>
                <div className="agent-thinking" style={{ justifyContent: "center" }}>
                  <span className="agent-thinking-spinner" />
                  <div className="agent-thinking-label" style={{ fontSize: 14 }}>
                    Launching — sending first-touch email{launchRecipients === 1 ? "" : "s"}
                    {launchRecipients ? ` to ${launchRecipients} supplier${launchRecipients === 1 ? "" : "s"}` : ""}…
                  </div>
                </div>
                <div style={{ fontSize: 12, color: "var(--fg-2)", marginTop: 10 }}>
                  This can take a moment — each email is rendered per supplier.
                </div>
              </>
            ) : (
              <>
                <div style={{ fontSize: 28 }}>🚀</div>
                <div style={{ fontWeight: 700, fontSize: 15, color: "var(--fg-0)", marginTop: 6 }}>
                  Campaign launched{launchOverlay.sent != null ? ` — ${launchOverlay.sent} email${launchOverlay.sent === 1 ? "" : "s"} sent` : ""}
                </div>
                <div style={{ fontSize: 12.5, color: "var(--fg-2)", marginTop: 6 }}>
                  Opening the collection command center…
                </div>
              </>
            )}
          </div>
        </div>
      ) : null}
      <div className="agent-chat-col">
        <div className="agent-chat-head">
          <span className={cx("agent-dot", busy && "busy")} />
          <h1>Setup Agent</h1>
          <AgentCampaignPicker apiBase={apiBase} currentId={snapshot.campaign?.id || null} />
          <span className="agent-sub">{busy ? "thinking…" : "ready"}</span>
        </div>
        <div className="agent-msgs" ref={msgsRef}>
          {messages.map((m, i) => {
            if (m.role === "tool") {
              const friendly = describeToolCall(m.content.name, m.content.args);
              const rawTitle = `${m.content.name}(${JSON.stringify(m.content.args || {})})`;
              return (
                <div key={i} className="agent-msg-tool" title={rawTitle}>
                  <span className="tname">▸ {friendly.label}</span>
                  {friendly.detail ? (
                    <span className="tdetail"> · {friendly.detail}</span>
                  ) : null}
                </div>
              );
            }
            if (m.role === "choices") {
              const { question, options, answered, picked } = m.content;
              // Once answered: keep the question as a transcript line;
              // drop the buttons. (User-message bubble shows the pick.)
              if (answered) {
                return question ? (
                  <div key={i} className="agent-msg assistant">{question}</div>
                ) : null;
              }
              return (
                <div key={i} className="agent-choices" style={_PENDING_CHOICE_STYLE}>
                  {question ? <div className="agent-choices-q">{question}</div> : null}
                  <div className="agent-choices-row">
                    {options.map((o) => (
                      <button
                        key={o.id}
                        className={cx("agent-choice-btn", answered && picked === o.id && "picked")}
                        disabled={answered || busy}
                        onClick={() => pickChoice(i, o)}
                      >
                        {o.label}
                      </button>
                    ))}
                  </div>
                </div>
              );
            }
            if (m.role === "upload") {
              const { prompt, status, filename, error } = m.content;
              const inputId = `agent-upload-${i}`;
              const isPending = (status || "idle") === "idle";
              return (
                <div key={i} className={cx("agent-upload", status)} style={isPending ? _PENDING_CHOICE_STYLE : undefined}>
                  <div className="agent-upload-prompt">{prompt}</div>
                  {status === "idle" ? (
                    <label htmlFor={inputId} className="agent-upload-btn">
                      <span className="agent-upload-icon" aria-hidden="true">↑</span>
                      Choose a file…
                      <input
                        id={inputId}
                        type="file"
                        accept=".pdf,.docx,.xlsx,.csv,.txt,.md"
                        style={{ display: "none" }}
                        onChange={(e) => {
                          const f = e.target.files?.[0];
                          if (f) uploadDoc(i, f);
                        }}
                      />
                    </label>
                  ) : status === "uploading" ? (
                    <div className="agent-upload-status">
                      Uploading {filename}…
                    </div>
                  ) : status === "done" ? (
                    <div className="agent-upload-status done">
                      ✓ Uploaded {filename} — agent is applying suggestions.
                    </div>
                  ) : (
                    <div className="agent-upload-status err">
                      Upload failed: {error || "unknown error"}
                    </div>
                  )}
                  <div className="agent-upload-hint">PDF, Word, Excel, CSV, or plain text.</div>
                </div>
              );
            }
            return <div key={i} className={cx("agent-msg", m.role)}>{m.content}</div>;
          })}
          {busy ? (
            <div className="agent-thinking" aria-live="polite">
              <span className="agent-thinking-spinner" />
              <div className="agent-thinking-label">Agent is thinking…</div>
            </div>
          ) : null}
        </div>
        <div className={cx("agent-composer", busy && "busy")}>
          <AgentAttachButton
            apiBase={apiBase}
            convId={convId}
            busy={busy}
            sendMessage={sendMessage}
          />
          <textarea
            placeholder={busy
              ? "Wait for the agent to finish before sending…"
              : "Tell the agent what you want to collect…"}
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={onKey}
            disabled={busy || !convId}
            rows={1}
          />
          <button
            onClick={() => {
              const v = input.trim();
              if (!v) return;
              setInput("");
              sendMessage(v);
            }}
            disabled={busy || !input.trim() || !convId}
          >
            {busy ? "Working…" : "Send"}
          </button>
        </div>
      </div>

      <div className="agent-panels-col">
        <AgentCampaignPanel snapshot={snapshot}
          open={openFor("campaign")} done={doneFor.campaign}
          onToggle={() => togglePanel("campaign")} />
        <AgentBriefPanel snapshot={snapshot}
          open={openFor("brief")} done={doneFor.brief}
          onToggle={() => togglePanel("brief")}
          live={live} onMutated={refreshSnapshot} />
        <AgentFieldsPanel fields={snapshot.fields}
          open={openFor("fields")} done={doneFor.fields}
          onToggle={() => togglePanel("fields")}
          live={live} campaignId={snapshot.campaign?.id} onMutated={refreshSnapshot} />
        <AgentPartsPanel parts={snapshot.campaign?.parts}
          open={openFor("parts")} done={doneFor.parts}
          onToggle={() => togglePanel("parts")}
          campaignExists={!!snapshot.campaign?.id} />
        <AgentRecipientsPanel recipients={snapshot.recipients}
          open={openFor("recipients")} done={doneFor.recipients}
          onToggle={() => togglePanel("recipients")}
          live={live} campaignId={snapshot.campaign?.id} onMutated={refreshSnapshot} />
        <AgentDirectivesPanel directives={snapshot.campaign?.email_directives}
          open={userOpen.directives !== undefined ? userOpen.directives : true}
          onToggle={() => setUserOpen((p) => ({ ...p, directives: !(userOpen.directives !== undefined ? userOpen.directives : true) }))} />
        {/* Step 5. Empty shell visible from the start so the user can see
            the full 1→5 flow; the draft body only populates once
            recipients are filled. */}
        <AgentEmailPreviewPanel
          preview={doneFor.recipients || doneFor.email ? emailPreview : null}
          previewSet={doneFor.recipients || doneFor.email ? previewSet : null}
          open={openFor("email")} done={doneFor.email}
          onToggle={() => togglePanel("email")}
          live={live} campaignId={snapshot.campaign?.id}
          onMutated={() => { refreshPreview && refreshPreview(); refreshSnapshot && refreshSnapshot(); }}
          gated={!(doneFor.recipients || doneFor.email)}
          recipientCount={snapshot.recipients?.length || 0}
          campaignStatus={snapshot.campaign?.status}
          loading={previewLoading}
          onLaunchState={setLaunchOverlay} />
      </div>
    </div>
  );
}

// =========================================================================
// Operator chat — SSE helper, mirrors _streamAgentMessage but talks to
// the operator-agent endpoints. Same event vocabulary so the renderers
// can be reused (text / tool_call / tool_result / state / ui_choice /
// done / error).
// =========================================================================
// Inline CSS for a pending interactive block — flex `order: 1` floats
// it after all default-order siblings, so any narration the agent
// emits after present_choices renders above the buttons. We do this
// visually (not by reordering state) so the index passed to pickChoice
// stays correct against state.
const _PENDING_CHOICE_STYLE = { order: 1 };

// When a NEW pending choice (or upload) comes in, mark any earlier
// unanswered choice/upload as answered so it falls back to a plain
// transcript line. Only one pending interactive block at a time —
// otherwise the user sees multiple stacked button rows.
function _dismissOlderPendingInteractives(messages) {
  return messages.map((m) => {
    const role = m.role || m.kind;
    if (role === "choices" && !m.content?.answered) {
      return { ...m, content: { ...m.content, answered: true, picked: null } };
    }
    if (role === "upload" && (m.content?.status || "idle") === "idle") {
      return { ...m, content: { ...m.content, status: "done", filename: m.content?.filename || "(skipped)" } };
    }
    return m;
  });
}

// Insert a new chat item, keeping any unanswered trailing choices/upload
// block at the END of state. The reasoning model sometimes emits a
// present_choices call BEFORE the assistant text that goes with it; if
// we just appended the text we'd get state [..., choices, text] and
// once the user answers, the question would render ABOVE the
// narration. Inserting before the trailing pending-interactive keeps
// the natural transcript order: narration → question → user reply.
function _appendBeforeTrailingChoice(messages, newItem) {
  if (messages.length === 0) return [newItem];
  const last = messages[messages.length - 1];
  const lastRole = last.role || last.kind;
  const isPending =
    (lastRole === "choices" && !last.content?.answered) ||
    (lastRole === "upload" && (last.content?.status || "idle") === "idle");
  if (isPending) {
    return [...messages.slice(0, -1), newItem, last];
  }
  return [...messages, newItem];
}

// Replay a persisted display_log into local message state, restoring
// interactivity to the LAST unanswered choice / upload block. A choices
// block is considered answered if any `user` message appears after it
// in the log (the user already clicked an option or typed a reply). An
// upload block is "done" if the same condition holds. Older interactive
// blocks (those that have a user message after them) are forcibly
// disabled — they're history.
function _replayDisplayLog(displayLog) {
  const msgs = (displayLog || []).map((m) => ({ role: m.role, content: m.content }));
  // First pass: anything before the last user message is in the past.
  let lastUserIdx = -1;
  for (let i = msgs.length - 1; i >= 0; i--) {
    if (msgs[i].role === "user") { lastUserIdx = i; break; }
  }
  for (let i = 0; i < msgs.length; i++) {
    const m = msgs[i];
    if (m.role === "choices") {
      const answered = i < lastUserIdx;
      msgs[i] = { ...m, content: { ...m.content, answered } };
    } else if (m.role === "upload") {
      const done = i < lastUserIdx;
      msgs[i] = {
        ...m,
        content: done
          ? { ...m.content, status: "done", filename: m.content?.filename || "(history)" }
          : { ...m.content, status: "idle", filename: null, error: null },
      };
    }
  }
  // Second pass: only the LAST pending interactive stays interactive.
  // Older pendings (e.g. two ui_choice events in a row, no user reply
  // between) collapse to plain transcript lines.
  let lastPendingIdx = -1;
  for (let i = msgs.length - 1; i >= 0; i--) {
    const m = msgs[i];
    if (m.role === "choices" && !m.content?.answered) { lastPendingIdx = i; break; }
    if (m.role === "upload" && (m.content?.status || "idle") === "idle") { lastPendingIdx = i; break; }
  }
  if (lastPendingIdx > 0) {
    for (let i = 0; i < lastPendingIdx; i++) {
      const m = msgs[i];
      if (m.role === "choices" && !m.content?.answered) {
        msgs[i] = { ...m, content: { ...m.content, answered: true, picked: null } };
      }
      if (m.role === "upload" && (m.content?.status || "idle") === "idle") {
        msgs[i] = { ...m, content: { ...m.content, status: "done", filename: m.content?.filename || "(skipped)" } };
      }
    }
  }
  return msgs;
}

async function _streamOperatorMessage(apiBase, scope, scopeId, conversationId, content, handlers, opts = {}) {
  const path = scope === "recipient"
    ? `/recipients/${scopeId}/messages`
    : scope === "global"
    ? `/global/messages`
    : `/collections/${scopeId}/messages`;
  const body = {
    content,
    conversation_id: conversationId,
  };
  if (opts.displayContent) body.display_content = opts.displayContent;
  if (opts.suppressUserInLog) body.suppress_user_in_log = true;
  const resp = await fetch(`${apiBase}${path}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!resp.body) throw new Error("no response body");
  const reader = resp.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const blocks = buffer.split("\n\n");
    buffer = blocks.pop() || "";
    for (const block of blocks) {
      if (!block.trim()) continue;
      const lines = block.split("\n");
      let event = "message";
      const dataLines = [];
      for (const line of lines) {
        if (line.startsWith("event: ")) event = line.slice(7);
        else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
      }
      if (!dataLines.length) continue;
      let parsed;
      try { parsed = JSON.parse(dataLines.join("\n")); } catch { continue; }
      handlers(event, parsed);
    }
  }
}

// =========================================================================
// Screen — Collections list (the new landing page, replaces the old
// Configure / Operate split).
// =========================================================================
// Inbox view — pending intake emails (forwards / CCs that didn't auto-route)
// plus a tab to see auto-routed history. Each row expands to a detail
// panel with three actions: dismiss, attach to existing campaign, or
// hand off to the setup agent for a new campaign.
function ScreenInbox({ apiBase, live, onConvertToNewCampaign, onAttachedToCampaign, onOpenRecipient, registerRefresh }) {
  const [view, setView] = useState("triage"); // triage | log
  const [tab, setTab] = useState("all");
  const [items, setItems] = useState([]);
  const [counts, setCounts] = useState({ pending: 0, auto_routed: 0, converted: 0, dismissed: 0 });
  const [logFailed, setLogFailed] = useState(0);
  const [picked, setPicked] = useState(null);
  const [busy, setBusy] = useState(null);
  const [error, setError] = useState(null);
  const [q, setQ] = useState("");
  const [selected, setSelected] = useState(new Set());
  const [bulkBusy, setBulkBusy] = useState(false);

  const refresh = useCallback(async () => {
    try {
      const [r1, r2, r3] = await Promise.all([
        fetch(`${apiBase}/intake?status=${tab}&limit=100`),
        fetch(`${apiBase}/intake/counts`),
        fetch(`${apiBase}/inbound/counts`),
      ]);
      if (r1.ok) setItems(await r1.json());
      if (r2.ok) setCounts(await r2.json());
      if (r3.ok) setLogFailed((await r3.json()).failed || 0);
    } catch (e) {
      setError(String(e));
    }
  }, [apiBase, tab]);

  function toggleSel(id) {
    setSelected((s) => {
      const n = new Set(s);
      if (n.has(id)) n.delete(id); else n.add(id);
      return n;
    });
  }
  async function bulkDismiss() {
    if (selected.size === 0) return;
    if (!confirm(`Delete ${selected.size} inbox item${selected.size === 1 ? "" : "s"}?\n\nRemoves the rows entirely.`)) return;
    setBulkBusy(true);
    try {
      const results = await Promise.allSettled(
        Array.from(selected).map(async (id) => {
          const r = await fetch(`${apiBase}/intake/${id}`, { method: "DELETE" });
          if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`);
        })
      );
      const failed = results.filter((r) => r.status === "rejected");
      if (failed.length > 0) {
        alert(`Failed to delete ${failed.length}: ${failed.map((f) => f.reason?.message || f.reason).slice(0, 3).join(" · ")}`);
      }
      setSelected(new Set());
      setPicked(null);
      refresh();
    } finally {
      setBulkBusy(false);
    }
  }
  useEffect(() => { setSelected(new Set()); }, [tab]);

  useEffect(() => { refresh(); }, [refresh]);
  useEffect(() => {
    const t = setInterval(refresh, 10000);
    return () => clearInterval(t);
  }, [refresh]);
  // Let the workspace chat panel poke us after a tool result so dismiss /
  // attach actions land instantly instead of waiting for the next tick.
  useEffect(() => {
    if (registerRefresh) registerRefresh(refresh);
    return () => { if (registerRefresh) registerRefresh(null); };
  }, [registerRefresh, refresh]);

  async function dismiss(id) {
    setBusy(id);
    try {
      await fetch(`${apiBase}/intake/${id}/dismiss`, { method: "POST" });
      setPicked(null);
      refresh();
    } finally { setBusy(null); }
  }

  async function acceptSuggestion(item) {
    if (!item.ai_suggested_campaign_id || !item.ai_suggested_recipient_email) return;
    setBusy(item.id);
    try {
      const r = await fetch(`${apiBase}/intake/${item.id}/attach`, {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          campaign_id: item.ai_suggested_campaign_id,
          recipient_email: item.ai_suggested_recipient_email,
        }),
      });
      if (r.ok) {
        const data = await r.json();
        setPicked(null);
        refresh();
        if (onAttachedToCampaign) onAttachedToCampaign(data.campaign_id);
      } else {
        setError(`attach: ${r.status} ${(await r.text()).slice(0, 200)}`);
      }
    } finally { setBusy(null); }
  }

  async function convertToNew(item) {
    setBusy(item.id);
    try {
      const r = await fetch(`${apiBase}/intake/${item.id}/convert`, {
        method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
      });
      if (r.ok) {
        const data = await r.json();
        setPicked(null);
        refresh();
        if (onConvertToNewCampaign) onConvertToNewCampaign(data.seed_message);
      }
    } finally { setBusy(null); }
  }

  const tabs = [
    ["all", "All", counts.all],
    ["pending", "Pending", counts.pending],
    ["auto_routed", "Auto-routed", counts.auto_routed],
    ["converted", "Converted", counts.converted],
    ["dismissed", "Dismissed", counts.dismissed],
  ];

  // Multi-token AND match across every searchable field per row.
  const filtered = q.trim()
    ? items.filter((it) => _matchesQuery([
        it.subject, it.from_addr, it.body_excerpt, it.ai_summary,
        (it.ai_suppliers_mentioned || []).join(" "),
        (it.to_addrs || []).join(" "), (it.cc_addrs || []).join(" "),
      ].join(" "), q))
    : items;

  return (
    <div>
      <div style={{ display: "flex", gap: 8, marginBottom: 14 }}>
        <button className={cx("filter-chip", view === "triage" && "active")} onClick={() => setView("triage")}>
          Triage
        </button>
        <button className={cx("filter-chip", view === "log" && "active")} onClick={() => setView("log")}>
          Mail log
          {logFailed > 0 ? (
            <span style={{ marginLeft: 7, padding: "1px 7px", borderRadius: 999, background: "#fdecea", color: "#b3261e", fontSize: 11, fontWeight: 600 }}>
              {logFailed} failed
            </span>
          ) : null}
        </button>
      </div>

      {view === "log" ? <MailLogView apiBase={apiBase} onOpenRecipient={onOpenRecipient} /> : <>

      <p className="text" style={{ marginTop: 0, marginBottom: 14, color: "var(--fg-2)", fontSize: 13 }}>
        Forward or CC <code>collection@app.sluicebox.ai</code> on any email
        to bring it into the system.
      </p>

      <div style={{ display: "flex", gap: 8, marginBottom: 18, alignItems: "center", flexWrap: "wrap" }}>
        <input
          className="search"
          placeholder="Search subject, sender, body, AI summary…"
          value={q}
          onChange={(e) => setQ(e.target.value)}
          style={{ flex: 1, minWidth: 220, maxWidth: 360, padding: "10px 14px", border: "1px solid var(--border-1)", borderRadius: 10, fontSize: 13.5, background: "white", outline: "none", fontFamily: "inherit" }}
        />
        {tabs.map(([id, label, count]) => (
          <button
            key={id}
            className={cx("filter-chip", tab === id && "active")}
            onClick={() => { setTab(id); setPicked(null); }}
          >
            {label}
            {count > 0 ? (
              <span style={{ marginLeft: 7, padding: "1px 7px", borderRadius: 999, background: tab === id ? "rgba(255,255,255,.22)" : "var(--bg-1)", fontSize: 11, fontWeight: 600 }}>
                {count}
              </span>
            ) : null}
          </button>
        ))}
      </div>

      {error && <div className="card" style={{ marginBottom: 16, color: "var(--warm)" }}>{error}</div>}

      {selected.size > 0 && (
        <BulkBar count={selected.size} busy={bulkBusy} onDelete={bulkDismiss} onClear={() => setSelected(new Set())} />
      )}

      {filtered.length === 0 ? (
        <div className="card" style={{ textAlign: "center", padding: 48, color: "var(--fg-3)" }}>
          {q.trim()
            ? <>No matches for <code>{q}</code>{tab !== "all" && <> in <strong>{tab.replace("_", "-")}</strong></>}.</>
            : tab === "pending"
            ? "Nothing pending. New forwards / CCs to collection@ will appear here within ~10 seconds."
            : tab === "all"
            ? "No intake activity yet."
            : `No ${tab.replace("_", "-")} items.`}
        </div>
      ) : (
        <div
          style={{
            display: "grid",
            // Two columns when a row is picked: list narrow, detail wide.
            // The chat panel lives in the OUTER workspace column, never
            // inside the tab content.
            gridTemplateColumns: picked
              ? "minmax(260px, 320px) minmax(0, 1fr)"
              : "minmax(0, 1fr)",
            gap: 16,
            alignItems: "start",
          }}
        >
          <div style={{ minWidth: 0 }}>
            {q.trim() && filtered.length < items.length && (
              <div className="meta" style={{ fontSize: 12, marginBottom: 6 }}>
                {filtered.length} of {items.length} match <code>{q}</code>
              </div>
            )}
            <SelectAllHeader
              visibleIds={filtered.map((it) => it.id)}
              selected={selected}
              setSelected={setSelected}
            />
            <div className="list-card">
              {filtered.map((item) => (
                <IntakeRow
                  key={item.id}
                  item={item}
                  active={picked?.id === item.id}
                  onClick={() => setPicked(item)}
                  selected={selected.has(item.id)}
                  onToggleSelect={() => toggleSel(item.id)}
                />
              ))}
            </div>
          </div>
          {picked && (
            <div style={{ minWidth: 0 }}>
              <IntakeDetail
                item={picked}
                busy={busy === picked.id}
                onClose={() => setPicked(null)}
                onDismiss={() => dismiss(picked.id)}
                onAccept={() => acceptSuggestion(picked)}
                onConvert={() => convertToNew(picked)}
              />
            </div>
          )}
        </div>
      )}
      </>}
    </div>
  );
}

// =========================================================================
// Mail log — the durable inbound-email buffer (SLU-935). Every email
// consumed from SQS lands in InboundEmail before processing; this view
// shows what arrived, what happened to it, and lets the user retry
// failures.
// =========================================================================
const MAIL_LOG_PILL = {
  failed: { background: "#fdecea", color: "#b3261e" },
  processed: { background: "#e6f4ea", color: "#1e7e34" },
  processing: { background: "#fff4e5", color: "#9a6700" },
  received: { background: "var(--bg-1)", color: "var(--fg-2)" },
};

function MailLogView({ apiBase, onOpenRecipient }) {
  const [tab, setTab] = useState("all");
  const [items, setItems] = useState([]);
  const [counts, setCounts] = useState({ all: 0, failed: 0, processed: 0, received: 0, processing: 0 });
  const [busy, setBusy] = useState(null);
  const [expanded, setExpanded] = useState(null);
  const [error, setError] = useState(null);
  const [retryAllBusy, setRetryAllBusy] = useState(false);

  const refresh = useCallback(async () => {
    try {
      const [r1, r2] = await Promise.all([
        fetch(`${apiBase}/inbound?status=${tab}&limit=100`),
        fetch(`${apiBase}/inbound/counts`),
      ]);
      if (r1.ok) setItems(await r1.json());
      if (r2.ok) setCounts(await r2.json());
    } catch (e) {
      setError(String(e));
    }
  }, [apiBase, tab]);
  useEffect(() => { refresh(); }, [refresh]);
  useEffect(() => {
    const t = setInterval(refresh, 10000);
    return () => clearInterval(t);
  }, [refresh]);

  async function retry(id) {
    setBusy(id);
    setError(null);
    try {
      const r = await fetch(`${apiBase}/inbound/${id}/retry`, { method: "POST" });
      if (!r.ok) setError(`retry: ${r.status} ${(await r.text()).slice(0, 200)}`);
      await refresh();
    } finally { setBusy(null); }
  }

  async function retryAllFailed() {
    const failed = items.filter((it) => it.status === "failed");
    if (failed.length === 0) return;
    setRetryAllBusy(true);
    setError(null);
    try {
      await Promise.allSettled(
        failed.map((it) => fetch(`${apiBase}/inbound/${it.id}/retry`, { method: "POST" }))
      );
      await refresh();
    } finally { setRetryAllBusy(false); }
  }

  function outcome(it) {
    if (it.result_message_id && it.recipient_name) {
      return (
        <span
          style={{ color: "var(--sb-ultra-violet)", cursor: onOpenRecipient ? "pointer" : "default", fontWeight: 600 }}
          onClick={() => onOpenRecipient && it.recipient_id && onOpenRecipient(it.recipient_id)}
        >
          → {it.recipient_name}{it.campaign_name ? ` · ${it.campaign_name}` : ""}
        </span>
      );
    }
    if (it.result_intake_id) {
      return <span>→ Inbox triage{it.intake_status ? ` (${it.intake_status.replace(/_/g, "-")})` : ""}</span>;
    }
    return null;
  }

  const tabs = [
    ["all", "All", counts.all],
    ["failed", "Failed", counts.failed],
    ["processed", "Processed", counts.processed],
  ];

  return (
    <div>
      <p className="text" style={{ marginTop: 0, marginBottom: 14, color: "var(--fg-2)", fontSize: 13 }}>
        Every inbound email consumed from the mail queue, with its processing
        outcome. Failed items keep their raw content and can be retried.
      </p>

      <div style={{ display: "flex", gap: 8, marginBottom: 18, alignItems: "center", flexWrap: "wrap" }}>
        {tabs.map(([id, label, count]) => (
          <button
            key={id}
            className={cx("filter-chip", tab === id && "active")}
            onClick={() => setTab(id)}
          >
            {label}
            {count > 0 ? (
              <span style={{ marginLeft: 7, padding: "1px 7px", borderRadius: 999, background: tab === id ? "rgba(255,255,255,.22)" : "var(--bg-1)", fontSize: 11, fontWeight: 600 }}>
                {count}
              </span>
            ) : null}
          </button>
        ))}
        <div style={{ flex: 1 }} />
        {counts.failed > 0 && (
          <button className="btn ghost sm" disabled={retryAllBusy} onClick={retryAllFailed}>
            {retryAllBusy ? "Retrying…" : `Retry all failed (${counts.failed})`}
          </button>
        )}
      </div>

      {error && <div className="card" style={{ marginBottom: 16, color: "var(--warm)" }}>{error}</div>}

      {items.length === 0 ? (
        <div className="card" style={{ textAlign: "center", padding: 48, color: "var(--fg-3)" }}>
          {tab === "all" ? "No inbound email consumed yet." : `No ${tab} items.`}
        </div>
      ) : (
        <div className="list-card">
          {items.map((it) => {
            const pill = MAIL_LOG_PILL[it.status] || MAIL_LOG_PILL.received;
            const isFailed = it.status === "failed";
            const isOpen = expanded === it.id;
            return (
              <div
                key={it.id}
                className="list-row"
                onClick={() => isFailed && setExpanded(isOpen ? null : it.id)}
                style={{
                  padding: "10px 12px",
                  borderBottom: "1px solid var(--border-1)",
                  background: "white",
                  cursor: isFailed ? "pointer" : "default",
                }}
              >
                <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
                  <span style={{ ...pill, padding: "2px 9px", borderRadius: 999, fontSize: 11, fontWeight: 600, whiteSpace: "nowrap" }}>
                    {it.status}
                  </span>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontWeight: 600, fontSize: 14, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                      {it.subject || "(no subject)"}
                    </div>
                    <div className="meta" style={{ fontSize: 12, marginTop: 2 }}>
                      from <strong>{it.from_addr || "(unknown)"}</strong> · {_ageLabel(it.received_at || it.created_at)}
                      {it.attempts > 1 ? <> · {it.attempts} attempts</> : null}
                    </div>
                  </div>
                  <div className="meta" style={{ fontSize: 12, whiteSpace: "nowrap" }}>{outcome(it)}</div>
                  {isFailed && (
                    <button
                      className="btn primary sm"
                      disabled={busy === it.id}
                      onClick={(e) => { e.stopPropagation(); retry(it.id); }}
                    >
                      {busy === it.id ? "Retrying…" : "Retry"}
                    </button>
                  )}
                </div>
                {isOpen && it.last_error && (
                  <pre style={{
                    marginTop: 10, marginBottom: 2, padding: 12, borderRadius: 8,
                    background: "#fdecea", color: "#7a1c15", fontSize: 11.5,
                    whiteSpace: "pre-wrap", wordBreak: "break-word",
                    maxHeight: 280, overflow: "auto",
                  }}>
                    {it.last_error}
                  </pre>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// BriefCard — read-at-a-glance for the campaign brief, rendered as a
// section header + body text (no card chrome). Long briefs collapse to
// ~240 chars + "Show full brief" toggle so they don't crowd the tiles.
function BriefCard({ text }) {
  const [expanded, setExpanded] = useState(false);
  const trimmed = (text || "").trim();
  const long = trimmed.length > 240;
  const visible = expanded || !long ? trimmed : trimmed.slice(0, 240).trimEnd() + "…";
  return (
    <div style={{
      fontSize: 13.5,
      lineHeight: 1.55,
      color: "var(--fg-1)",
      whiteSpace: "pre-wrap",
    }}>
      <div className="meta" style={{ fontSize: 11, marginBottom: 6, letterSpacing: ".06em" }}>
        COLLECTION BRIEF
      </div>
      <div>{visible}</div>
      {long && (
        <button
          onClick={() => setExpanded((v) => !v)}
          style={{
            marginTop: 8,
            background: "transparent",
            border: "none",
            color: "var(--sb-ultra-violet)",
            cursor: "pointer",
            fontSize: 12,
            fontFamily: "inherit",
            padding: 0,
            fontWeight: 600,
          }}
        >
          {expanded ? "Show less" : "Show full brief"}
        </button>
      )}
    </div>
  );
}

// =========================================================================
// PageHeader — single header chrome used by every workspace screen.
// Three slots so the visual rhythm stays the same as you drill in:
//   crumbs[]  — last item renders bold + big = current page title.
//               Earlier items render small/grey/clickable.
//   subtitle  — one-line context (description / brief / email).
//   meta      — right-aligned content (KPIs / status pill / +New button).
//   toolbar   — row beneath subtitle (tabs / filter chips / nothing).
// Going list → campaign → recipient extends the crumb chain and shifts
// the bolded title to the new context, instead of swapping headers.
// =========================================================================
function PageHeader({ crumbs = [], subtitle, meta, toolbar }) {
  return (
    <div className="page-header">
      <div className="crumb-row">
        <div className="crumb-chain">
          {crumbs.map((c, i) => {
            const isLast = i === crumbs.length - 1;
            return (
              <Fragment key={i}>
                {i > 0 && <span className="crumb-sep">›</span>}
                {isLast ? (
                  <span className="crumb-current">{c.label}</span>
                ) : (
                  <span className="crumb-link" onClick={c.onClick}>{c.label}</span>
                )}
              </Fragment>
            );
          })}
        </div>
        {meta != null ? <div className="crumb-meta">{meta}</div> : null}
      </div>
      {subtitle ? <div className="page-header-subtitle">{subtitle}</div> : null}
      {toolbar ? <div className="page-header-toolbar">{toolbar}</div> : null}
    </div>
  );
}

// =========================================================================
// Workspace — outer wrapper that holds Collections / Inbox / Sent as
// internal tabs + a persistent global-scope agent chat in a fixed right
// column. Replaces three sibling sidebar entries with one workspace.
// Mirrors ScreenCollection's grid (1fr | 380px) so navigation feels
// visually continuous when entering a per-campaign view.
// =========================================================================
function ScreenWorkspace({ apiBase, live, screen, setScreen, onPickDraft, onPickLive, onNew, onConvertToNewCampaign, onAttachedToCampaign, onOpenRecipient }) {
  // Active tab is just the screen id — list / inbox / sent / contacts.
  const tab = (screen === "inbox" || screen === "sent" || screen === "contacts") ? screen : "list";
  const setTab = (t) => setScreen(t);
  // Pending-intake count for the Inbox tab badge. Polled on a slow
  // cadence; the tab chrome doesn't need second-precision freshness.
  const [inboxPending, setInboxPending] = useState(0);
  useEffect(() => {
    let stop = false;
    const tick = async () => {
      try {
        const r = await fetch(`${apiBase}/intake/counts`);
        if (r.ok && !stop) {
          const c = await r.json();
          setInboxPending(c.pending || 0);
        }
      } catch {}
    };
    tick();
    const t = setInterval(tick, 15000);
    return () => { stop = true; clearInterval(t); };
  }, [apiBase]);

  // The active tab can register a refresh callback so the chat panel
  // can poke it after a tool result (e.g. the agent dismisses an intake
  // and the Inbox list should re-fetch). Single shared hook so the chat
  // panel doesn't need to know which tab is active.
  const refreshRef = useRef(() => {});
  const registerRefresh = useCallback((fn) => { refreshRef.current = fn || (() => {}); }, []);

  // Workspace chat collapse — toggleable, persisted in localStorage so
  // the choice survives reloads.
  const [chatCollapsed, setChatCollapsed] = useState(() => {
    try { return localStorage.getItem("sb-ws-chat-collapsed") === "1"; } catch { return false; }
  });
  useEffect(() => {
    try { localStorage.setItem("sb-ws-chat-collapsed", chatCollapsed ? "1" : "0"); } catch {}
  }, [chatCollapsed]);

  const tabs = [
    ["list", "Collections", null],
    ["inbox", "Inbox", inboxPending],
    ["sent", "Sent", null],
    ["contacts", "Recipients", null],
  ];

  return (
    <div className="workspace">
      <PageHeader
        crumbs={[{ label: "Data collections" }]}
        meta={(
          <button className="new-btn" onClick={onNew}>＋ New collection</button>
        )}
        toolbar={(
          <div className="workspace-tabs">
            {tabs.map(([id, label, count]) => (
              <button
                key={id}
                className={cx("tab", tab === id && "active")}
                onClick={() => setTab(id)}
              >
                {label}
                {count > 0 ? <span className="count">{count}</span> : null}
              </button>
            ))}
          </div>
        )}
      />

      <div className={cx("workspace-body", chatCollapsed && "chat-collapsed")}>
        <div className="workspace-content">
          {tab === "list" && (
            <ScreenCollectionsList
              apiBase={apiBase}
              onPickDraft={onPickDraft}
              onPickLive={onPickLive}
              onNew={onNew}
            />
          )}
          {tab === "inbox" && (
            <ScreenInbox
              apiBase={apiBase}
              live={live}
              onConvertToNewCampaign={onConvertToNewCampaign}
              onAttachedToCampaign={onAttachedToCampaign}
              onOpenRecipient={onOpenRecipient}
              registerRefresh={registerRefresh}
            />
          )}
          {tab === "sent" && (
            <ScreenSent
              apiBase={apiBase}
              onOpenRecipient={onOpenRecipient}
              registerRefresh={registerRefresh}
            />
          )}
          {tab === "contacts" && (
            <ScreenContacts
              apiBase={apiBase}
              live={live}
              onOpenRecipient={onOpenRecipient}
            />
          )}
        </div>
        {chatCollapsed ? (
          <button
            className="workspace-chat-toggle collapsed"
            onClick={() => setChatCollapsed(false)}
            title="Show workspace agent"
          >
            🤖
          </button>
        ) : (
          <WorkspaceChatPanel
            apiBase={apiBase}
            onActed={() => refreshRef.current()}
            onCollapse={() => setChatCollapsed(true)}
          />
        )}
      </div>
    </div>
  );
}

function WorkspaceChatPanel({ apiBase, onActed, onCollapse }) {
  const [convId, setConvId] = useState(null);
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState("");
  const [busy, setBusy] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch(`${apiBase}/global/conversations`, {
          method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
        });
        const d = await r.json();
        if (cancelled) return;
        setConvId(d.conversation_id);
        setMessages([{
          role: "assistant",
          content: "I'm your workspace agent. Ask me what's pending in the inbox, what's stalled in sent, or to take action on a thread.",
        }]);
      } catch (e) {
        if (!cancelled) setMessages([{ role: "assistant", content: `[error] ${e.message}` }]);
      }
    })();
    return () => { cancelled = true; };
  }, [apiBase]);

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

  const send = async (text) => {
    if (!convId || !text.trim() || busy) return;
    setBusy(true);
    setMessages((m) => [...m, { role: "user", content: text }]);
    try {
      await _streamOperatorMessage(apiBase, "global", null, convId, text, (event, payload) => {
        if (event === "text") {
          setMessages((m) => [...m, { role: "assistant", content: payload.text }]);
        } else if (event === "tool_call") {
          if (payload.name === "present_choices" || payload.name === "offer_exports") return;
          setMessages((m) => [...m, { role: "tool", content: { name: payload.name, args: payload.args } }]);
        } else if (event === "tool_result") {
          // After any tool result, ping the active tab's refresh hook so
          // intake actions / status changes / etc. are reflected without
          // waiting for the polling tick.
          if (onActed) onActed();
        } else if (event === "error") {
          setMessages((m) => [...m, { role: "assistant", content: `[error] ${payload.message}` }]);
        }
      }, {});
    } catch (e) {
      setMessages((m) => [...m, { role: "assistant", content: `[network error] ${e.message}` }]);
    } finally {
      setBusy(false);
    }
  };

  return (
    <aside className="workspace-chat">
      <div className="head" style={{ position: "relative" }}>
        {onCollapse ? (
          <button
            onClick={onCollapse}
            title="Hide workspace agent"
            style={{ position: "absolute", top: 10, right: 12, background: "transparent", border: 0, fontSize: 18, color: "var(--fg-2)", cursor: "pointer", padding: 4 }}
          >›</button>
        ) : null}
        <h3>🤖 Workspace agent</h3>
        <div className="sub">Cross-tab triage — inbox, sent, and recipient actions.</div>
      </div>
      <div ref={ref} className="msgs">
        {messages.map((m, i) => {
          if (m.role === "user") {
            return <div key={i} className="mock-event-user" style={{ alignSelf: "flex-end", maxWidth: "100%" }}>{m.content}</div>;
          }
          if (m.role === "assistant") {
            return <div key={i} className="mock-event-agent" style={{ alignSelf: "flex-start", maxWidth: "100%" }}>{m.content}</div>;
          }
          if (m.role === "tool") {
            const f = describeToolCall(m.content.name, m.content.args);
            return <div key={i} className="agent-msg-tool"><span className="tname">▸ {f.label}</span>{f.detail ? <span className="tdetail"> · {f.detail}</span> : null}</div>;
          }
          return null;
        })}
        {busy ? <div className="agent-thinking"><span className="agent-thinking-spinner" /><span className="agent-thinking-label">Agent is thinking…</span></div> : null}
      </div>
      <div className={cx("agent-composer", busy && "busy")}>
        <textarea
          rows={1}
          placeholder={busy ? "Wait for the agent to finish…" : "e.g. 'what's pending?' or 'dismiss the autoresponder'"}
          value={input}
          onChange={(e) => setInput(e.target.value)}
          disabled={busy || !convId}
          onKeyDown={(e) => {
            if (e.key === "Enter" && !e.shiftKey) {
              e.preventDefault();
              const v = input.trim(); if (!v) return;
              setInput(""); send(v);
            }
          }}
        />
        <button onClick={() => { const v = input.trim(); if (!v) return; setInput(""); send(v); }} disabled={busy || !input.trim() || !convId}>
          {busy ? "Working…" : "Send"}
        </button>
      </div>
    </aside>
  );
}

function IntakeRow({ item, active, onClick, selected, onToggleSelect }) {
  const age = _ageLabel(item.received_at);
  return (
    <div
      onClick={onClick}
      className="list-row"
      style={{
        padding: "10px 12px", cursor: "pointer",
        borderBottom: "1px solid var(--border-1)",
        background: active ? "var(--sb-pale-amethyst)" : selected ? "var(--sb-pale-amethyst)" : "white",
      }}
    >
      <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
        {onToggleSelect && (
          <div onClick={(e) => { e.stopPropagation(); onToggleSelect(); }} style={{ display: "flex", alignItems: "center" }}>
            <input type="checkbox" checked={!!selected} onChange={onToggleSelect} onClick={(e) => e.stopPropagation()} />
          </div>
        )}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontWeight: 600, fontSize: 14, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
            {item.subject || "(no subject)"}
          </div>
          <div className="meta" style={{ fontSize: 12, marginTop: 2 }}>
            from <strong>{item.from_addr}</strong> · {age}
          </div>
        </div>
      </div>
    </div>
  );
}

function IntakeDetail({ item, busy, onClose, onDismiss, onAccept, onConvert }) {
  const canAccept = !!(item.ai_suggested_campaign_id && item.ai_suggested_recipient_email && item.ai_suggested_action === "attach_existing");
  const isPending = item.status === "pending";
  return (
    <div className="card" style={{ padding: 20, position: "sticky", top: 16, alignSelf: "flex-start" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 12 }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="meta" style={{ fontSize: 11 }}>{item.from_addr}</div>
          <div style={{ fontWeight: 600, fontSize: 16, marginTop: 2 }}>{item.subject}</div>
        </div>
        <button onClick={onClose} className="btn quiet sm">✕</button>
      </div>

      {item.ai_summary && (
        <div style={{
          padding: 12, marginBottom: 12, borderRadius: 8,
          background: "var(--bg-soft)", border: "1px solid var(--border-1)",
        }}>
          <div className="meta" style={{ fontSize: 11, marginBottom: 4 }}>
            AI · {item.ai_kind || "?"} · confidence {item.ai_confidence?.toFixed(2) ?? "0"}
          </div>
          <div className="text" style={{ fontSize: 13 }}>{item.ai_summary}</div>
          {item.ai_suggested_action && (
            <div className="meta" style={{ fontSize: 11, marginTop: 6 }}>
              <strong>Suggests:</strong> {item.ai_suggested_action.replace("_", " ")}
              {item.ai_suggested_campaign_name && <> → <em>{item.ai_suggested_campaign_name}</em></>}
              {item.ai_suggested_recipient_email && <> · {item.ai_suggested_recipient_email}</>}
            </div>
          )}
          {item.ai_suppliers_mentioned?.length > 0 && (
            <div className="meta" style={{ fontSize: 11, marginTop: 6 }}>
              Suppliers: {item.ai_suppliers_mentioned.join(", ")}
            </div>
          )}
        </div>
      )}

      <div className="text" style={{
        fontSize: 13, lineHeight: 1.55, padding: 12, background: "white",
        border: "1px solid var(--border-1)", borderRadius: 8,
        whiteSpace: "pre-wrap", maxHeight: 400, overflow: "auto",
        fontFamily: "inherit",
      }}>
        {item.body_text || "(empty body)"}
      </div>

      {isPending && (
        <div style={{ display: "flex", gap: 8, marginTop: 14, flexWrap: "wrap" }}>
          {canAccept && (
            <button
              className="btn primary"
              disabled={busy}
              onClick={onAccept}
            >
              {busy ? "Working…" : `✓ Accept: attach to ${item.ai_suggested_campaign_name || "suggested"}`}
            </button>
          )}
          <button
            className="btn ghost"
            disabled={busy}
            onClick={onConvert}
          >
            {busy ? "…" : "Start a new campaign from this"}
          </button>
          <button
            className="btn quiet"
            disabled={busy}
            onClick={onDismiss}
          >
            Dismiss
          </button>
        </div>
      )}

      {!isPending && (
        <div className="meta" style={{ marginTop: 14, fontSize: 12 }}>
          Status: <strong>{item.status}</strong>
          {item.converted_to_campaign_id && (
            <> · routed to campaign <code>{item.converted_to_campaign_id.slice(0, 8)}</code></>
          )}
        </div>
      )}
    </div>
  );
}

// =========================================================================
// Sent / Outbox — every outbound email the agent shipped.
// Mirrors ScreenInbox; backed by GET /sent + /sent/counts.
// =========================================================================
function ScreenSent({ apiBase, onOpenRecipient, registerRefresh }) {
  const [tab, setTab] = useState("all");
  const [items, setItems] = useState([]);
  const [counts, setCounts] = useState({ awaiting: 0, replied: 0, all: 0 });
  const [picked, setPicked] = useState(null);
  const [pickedFull, setPickedFull] = useState(null);
  const [error, setError] = useState(null);
  const [q, setQ] = useState("");
  const [selected, setSelected] = useState(new Set());
  const [bulkBusy, setBulkBusy] = useState(false);
  function toggleSel(id) {
    setSelected((s) => {
      const n = new Set(s);
      if (n.has(id)) n.delete(id); else n.add(id);
      return n;
    });
  }

  const refresh = useCallback(async () => {
    try {
      const [r1, r2] = await Promise.all([
        fetch(`${apiBase}/sent?status=${tab}&limit=200`),
        fetch(`${apiBase}/sent/counts`),
      ]);
      if (r1.ok) setItems(await r1.json());
      if (r2.ok) setCounts(await r2.json());
    } catch (e) {
      setError(String(e));
    }
  }, [apiBase, tab]);

  useEffect(() => { refresh(); }, [refresh]);
  useEffect(() => {
    const t = setInterval(refresh, 10000);
    return () => clearInterval(t);
  }, [refresh]);
  useEffect(() => {
    if (registerRefresh) registerRefresh(refresh);
    return () => { if (registerRefresh) registerRefresh(null); };
  }, [registerRefresh, refresh]);
  useEffect(() => { setSelected(new Set()); }, [tab]);
  async function bulkDelete() {
    if (selected.size === 0) return;
    if (!confirm(`Delete ${selected.size} sent message${selected.size === 1 ? "" : "s"}?`)) return;
    setBulkBusy(true);
    try {
      const results = await Promise.allSettled(
        Array.from(selected).map(async (id) => {
          const r = await fetch(`${apiBase}/sent/${id}`, { method: "DELETE" });
          if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`);
        })
      );
      const failed = results.filter((r) => r.status === "rejected");
      if (failed.length > 0) {
        alert(`Failed to delete ${failed.length}: ${failed.map((f) => f.reason?.message || f.reason).slice(0, 3).join(" · ")}`);
      }
      setSelected(new Set());
      setPicked(null);
      refresh();
    } finally {
      setBulkBusy(false);
    }
  }

  // When the user picks a row, fetch the full body. The list payload
  // only includes a 400-char excerpt to keep the grid lightweight.
  useEffect(() => {
    if (!picked) { setPickedFull(null); return; }
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch(`${apiBase}/sent/${picked.id}/full`);
        if (r.ok && !cancelled) setPickedFull(await r.json());
      } catch {}
    })();
    return () => { cancelled = true; };
  }, [apiBase, picked]);

  const tabs = [
    ["all", "All", counts.all],
    ["awaiting", "Awaiting reply", counts.awaiting],
    ["replied", "Replied", counts.replied],
  ];

  const filtered = q.trim()
    ? items.filter((it) => _matchesQuery([
        it.subject, it.recipient_name, it.recipient_email,
        it.campaign_name, it.body_excerpt, it.kind,
      ].join(" "), q))
    : items;

  return (
    <div>
      <p className="text" style={{ marginTop: 0, marginBottom: 14, color: "var(--fg-2)", fontSize: 13 }}>
        Every email the agent has sent on your behalf.
      </p>

      <div style={{ display: "flex", gap: 8, marginBottom: 18, alignItems: "center", flexWrap: "wrap" }}>
        <input
          className="search"
          placeholder="Search subject, recipient, body, campaign…"
          value={q}
          onChange={(e) => setQ(e.target.value)}
          style={{ flex: 1, minWidth: 220, maxWidth: 360, padding: "10px 14px", border: "1px solid var(--border-1)", borderRadius: 10, fontSize: 13.5, background: "white", outline: "none", fontFamily: "inherit" }}
        />
        {tabs.map(([id, label, count]) => (
          <button
            key={id}
            className={cx("filter-chip", tab === id && "active")}
            onClick={() => { setTab(id); setPicked(null); }}
          >
            {label}
            {count > 0 ? (
              <span style={{ marginLeft: 7, padding: "1px 7px", borderRadius: 999, background: tab === id ? "rgba(255,255,255,.22)" : "var(--bg-1)", fontSize: 11, fontWeight: 600 }}>
                {count}
              </span>
            ) : null}
          </button>
        ))}
      </div>

      {error && <div className="card" style={{ marginBottom: 16, color: "var(--warm)" }}>{error}</div>}

      {selected.size > 0 && (
        <BulkBar count={selected.size} busy={bulkBusy} onDelete={bulkDelete} onClear={() => setSelected(new Set())} />
      )}

      {filtered.length === 0 ? (
        <div className="card" style={{ textAlign: "center", padding: 48, color: "var(--fg-3)" }}>
          {q.trim()
            ? <>No matches for <code>{q}</code>{tab !== "all" && <> in <strong>{tab.replace("_", " ")}</strong></>}.</>
            : tab === "awaiting"
            ? "No outbounds awaiting reply. Either nothing's been sent yet, or every email got an answer."
            : tab === "replied"
            ? "No replied outbounds yet."
            : "No outbound messages yet."}
        </div>
      ) : (
        <div
          style={{
            display: "grid",
            gridTemplateColumns: picked
              ? "minmax(300px, 360px) minmax(0, 1fr)"
              : "minmax(0, 1fr)",
            gap: 16,
            alignItems: "start",
          }}
        >
          <div style={{ minWidth: 0 }}>
            {q.trim() && filtered.length < items.length && (
              <div className="meta" style={{ fontSize: 12, marginBottom: 6 }}>
                {filtered.length} of {items.length} match <code>{q}</code>
              </div>
            )}
            <SelectAllHeader
              visibleIds={filtered.map((it) => it.id)}
              selected={selected}
              setSelected={setSelected}
            />
            <div className="list-card">
              {filtered.map((item) => (
                <SentRow
                  key={item.id}
                  item={item}
                  active={picked?.id === item.id}
                  onClick={() => setPicked(item)}
                  selected={selected.has(item.id)}
                  onToggleSelect={() => toggleSel(item.id)}
                />
              ))}
            </div>
          </div>
          {picked && (
            <div style={{ minWidth: 0 }}>
              <SentDetail
                item={pickedFull || picked}
                onClose={() => setPicked(null)}
                onOpenRecipient={() => onOpenRecipient && onOpenRecipient(picked.recipient_id)}
              />
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// Multi-token AND match. Splits the query on whitespace, requires every
// token to appear in the haystack (case-insensitive substring). Used by
// the search inputs on Inbox + Sent.
function _matchesQuery(haystack, query) {
  if (!query) return true;
  const hay = (haystack || "").toLowerCase();
  return query.toLowerCase().split(/\s+/).filter(Boolean).every((tok) => hay.includes(tok));
}

const _KIND_LABEL = {
  first_touch: "first touch",
  followup: "follow-up",
  custom: "custom",
  attestation: "attestation",
};

// Parse a server-emitted ISO timestamp. Most backend rows omit the TZ
// suffix (datetime.utcnow().isoformat() → "2026-05-11T20:30:00"); Safari
// interprets that as local time, which surfaces as the wrong relative
// age. If there's no Z or ±HH:MM suffix, assume UTC.
function _parseServerTs(s) {
  if (!s) return null;
  if (typeof s !== "string") return new Date(s);
  const hasTz = /Z$|[+-]\d{2}:?\d{2}$/.test(s);
  return new Date(hasTz ? s : s + "Z");
}

// Friendly labels for AgentRun.kind values — used by the timeline
// in the recipient view so the user sees what the agent is actually
// doing (and not the raw enum name).
const _AGENT_RUN_LABELS = {
  inbound_extract: "Extracting data from supplier reply",
  outbound_first: "Generating first-touch email",
  outbound_followup: "Generating follow-up draft",
  operator_decision: "Reviewing thread",
  operator_chat: "Working on a response",
};

function _ageLabel(dateStr) {
  const d = _parseServerTs(dateStr);
  if (!d) return "—";
  const ms = Date.now() - d.getTime();
  if (ms < 60_000) return "just now";
  if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m ago`;
  if (ms < 86_400_000) return `${Math.round(ms / 3_600_000)}h ago`;
  return `${Math.round(ms / 86_400_000)}d ago`;
}

function SentRow({ item, active, onClick, selected, onToggleSelect }) {
  const sentAge = _ageLabel(item.sent_at);
  const replied = item.reply_status === "replied";
  // Stale threshold: awaiting > 5 days. Soft signal — colors the row's
  // age label; doesn't block anything.
  const stale = !replied && item.age_days >= 5;
  return (
    <div
      onClick={onClick}
      className="list-row"
      style={{
        padding: "10px 12px", cursor: "pointer",
        borderBottom: "1px solid var(--border-1)",
        background: active ? "var(--sb-pale-amethyst)" : selected ? "var(--sb-pale-amethyst)" : "white",
      }}
    >
      <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
        {onToggleSelect && (
          <div onClick={(e) => { e.stopPropagation(); onToggleSelect(); }} style={{ display: "flex", alignItems: "center" }}>
            <input type="checkbox" checked={!!selected} onChange={onToggleSelect} onClick={(e) => e.stopPropagation()} />
          </div>
        )}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontWeight: 600, fontSize: 14, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
            {item.subject || "(no subject)"}
          </div>
          <div className="meta" style={{ fontSize: 12, marginTop: 2 }}>
            to <strong>{item.recipient_name}</strong>
            {" · "}<span style={{ color: "var(--fg-2)" }}>{item.campaign_name}</span>
          </div>
        </div>
        <div style={{ flexShrink: 0, textAlign: "right", fontSize: 11 }}>
          <div style={{ color: stale ? "var(--warm)" : "var(--fg-3)" }}>
            sent {sentAge}
          </div>
          <div style={{
            marginTop: 2,
            color: replied ? "var(--ok)" : stale ? "var(--warm)" : "var(--fg-3)",
            fontWeight: replied || stale ? 600 : 400,
          }}>
            {replied ? `replied` : "awaiting"}
          </div>
        </div>
      </div>
    </div>
  );
}

function SentDetail({ item, onClose, onOpenRecipient }) {
  const replied = item.reply_status === "replied";
  const sentAt = new Date(item.sent_at).toLocaleString();
  const repliedAt = item.replied_at ? new Date(item.replied_at).toLocaleString() : null;
  return (
    <div className="card" style={{ padding: 20, position: "sticky", top: 16, alignSelf: "flex-start" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 12 }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="meta" style={{ fontSize: 11 }}>
            to {item.recipient_name} &lt;{item.recipient_email}&gt;
          </div>
          <div style={{ fontWeight: 600, fontSize: 16, marginTop: 2 }}>{item.subject}</div>
        </div>
        <button onClick={onClose} className="btn quiet sm">✕</button>
      </div>

      <div style={{
        padding: 12, marginBottom: 12, borderRadius: 8,
        background: "var(--bg-soft)", border: "1px solid var(--border-1)",
        fontSize: 12,
      }}>
        <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: "4px 12px", color: "var(--fg-2)" }}>
          <div>Campaign</div><div><strong>{item.campaign_name}</strong></div>
          <div>Kind</div><div>{_KIND_LABEL[item.kind] || item.kind}</div>
          <div>Sent</div><div>{sentAt} ({_ageLabel(item.sent_at)})</div>
          <div>Reply</div>
          <div style={{ color: replied ? "var(--ok)" : "var(--fg-2)" }}>
            {replied ? `received ${repliedAt}` : `none yet (${item.age_days}d)`}
          </div>
          {item.from_draft_id && (<><div>Draft</div><div><code style={{ fontSize: 11 }}>{item.from_draft_id.slice(0, 8)}…</code></div></>)}
        </div>
      </div>

      <div className="text" style={{
        fontSize: 13, lineHeight: 1.55, padding: 12, background: "white",
        border: "1px solid var(--border-1)", borderRadius: 8,
        whiteSpace: "pre-wrap", maxHeight: 400, overflow: "auto",
        fontFamily: "inherit",
      }}>
        {item.body_text || item.body_excerpt || "(empty body)"}
      </div>

      <details style={{ marginTop: 10, fontSize: 11, color: "var(--fg-3)" }}>
        <summary style={{ cursor: "pointer" }}>Threading headers</summary>
        <div style={{ marginTop: 6, fontFamily: "ui-monospace, monospace", wordBreak: "break-all" }}>
          <div>Message-ID: {item.provider_message_id}</div>
          {item.in_reply_to && <div style={{ marginTop: 2 }}>In-Reply-To: {item.in_reply_to}</div>}
          {item.references && <div style={{ marginTop: 2 }}>References: {item.references}</div>}
        </div>
      </details>

      <div style={{ display: "flex", gap: 8, marginTop: 14 }}>
        <button className="btn primary" onClick={onOpenRecipient}>
          Open recipient thread →
        </button>
      </div>
    </div>
  );
}


// =========================================================================
// Recipients tab — cross-campaign roster, deduplicated by contact_email.
// Backed by GET /recipients/contacts. Click a row to jump to that
// supplier's most-recently-active recipient view.
// =========================================================================
function ScreenContacts({ apiBase, live, onOpenRecipient }) {
  const [contacts, setContacts] = useState(null);
  const [q, setQ] = useState("");
  const [error, setError] = useState(null);
  // One contact expanded at a time. null = none expanded.
  const [expandedEmail, setExpandedEmail] = useState(null);
  const [addOpen, setAddOpen] = useState(false);
  const [campaigns, setCampaigns] = useState([]);
  useEffect(() => {
    fetch(`${apiBase}/campaigns`).then((r) => r.json()).then(setCampaigns).catch(() => {});
  }, [apiBase]);
  // Bulk-delete by email — deleting a contact removes every Recipient
  // row across all campaigns that shares that email.
  const [selected, setSelected] = useState(new Set());
  const [bulkBusy, setBulkBusy] = useState(false);
  function toggleSel(email) {
    setSelected((s) => {
      const n = new Set(s);
      if (n.has(email)) n.delete(email); else n.add(email);
      return n;
    });
  }
  async function bulkDelete() {
    if (selected.size === 0 || !contacts) return;
    const total = (contacts || [])
      .filter((c) => selected.has(c.email))
      .reduce((acc, c) => acc + (c.instances?.length || 0), 0);
    if (!confirm(`Delete ${selected.size} contact${selected.size === 1 ? "" : "s"} (${total} recipient row${total === 1 ? "" : "s"})?\n\nAll messages, drafts, and extractions are removed.`)) return;
    setBulkBusy(true);
    try {
      const recipientIds = (contacts || [])
        .filter((c) => selected.has(c.email))
        .flatMap((c) => (c.instances || []).map((i) => i.recipient_id));
      const results = await Promise.allSettled(
        recipientIds.map(async (rid) => {
          const r = await fetch(`${apiBase}/recipients/${rid}`, { method: "DELETE" });
          if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`);
        })
      );
      const failed = results.filter((r) => r.status === "rejected");
      if (failed.length > 0) {
        alert(`Failed to delete ${failed.length} of ${results.length}: ${failed.map((f) => f.reason?.message || f.reason).slice(0, 3).join(" · ")}`);
      }
      setSelected(new Set());
      if (live?.fetchContacts) {
        const data = await live.fetchContacts();
        setContacts(data);
      }
    } finally {
      setBulkBusy(false);
    }
  }

  const refresh = useCallback(async () => {
    if (!live?.fetchContacts) return;
    try {
      const data = await live.fetchContacts();
      setContacts(data);
    } catch (e) {
      setError(String(e));
    }
  }, [live]);

  useEffect(() => { refresh(); }, [refresh]);
  useEffect(() => {
    const t = setInterval(refresh, 15000);
    return () => clearInterval(t);
  }, [refresh]);

  const filtered = (contacts || []).filter((c) =>
    !q.trim() || _matchesQuery(
      [c.email, c.name, ...(c.instances || []).map((i) => i.campaign_name)].join(" "),
      q,
    )
  );

  const rel = _ageLabel;

  function statusClass(s) {
    return (s || "").replace("RecipientStatus.", "");
  }
  function statusLabel(s) {
    return (s || "").replace("RecipientStatus.", "").replace(/_/g, " ");
  }

  return (
    <div>
      <p className="text" style={{ marginTop: 0, marginBottom: 14, color: "var(--fg-2)", fontSize: 13 }}>
        Every supplier we've ever contacted, deduplicated by email. Click a row
        to see all the collections they're in.
      </p>

      <div style={{ display: "flex", gap: 8, marginBottom: 18, alignItems: "center", flexWrap: "wrap" }}>
        <input
          className="search"
          placeholder="Search name, email, collection…"
          value={q}
          onChange={(e) => setQ(e.target.value)}
          style={{ flex: 1, minWidth: 220, maxWidth: 360, padding: "10px 14px", border: "1px solid var(--border-1)", borderRadius: 10, fontSize: 13.5, background: "white", outline: "none", fontFamily: "inherit" }}
        />
        <span className="meta" style={{ fontSize: 12, color: "var(--fg-3)" }}>
          {contacts == null ? "" : `${filtered.length}${q.trim() && filtered.length !== contacts.length ? ` of ${contacts.length}` : ""}`}
        </span>
        <button className="new-btn" onClick={() => setAddOpen(true)}>＋ Add recipient</button>
      </div>

      {addOpen && (
        <AddRecipientModal
          apiBase={apiBase}
          campaigns={campaigns}
          onClose={() => setAddOpen(false)}
          onAdded={async () => {
            setAddOpen(false);
            if (live?.fetchContacts) setContacts(await live.fetchContacts());
          }}
        />
      )}

      {error ? <div style={{ color: "#9b1d1d" }}>Couldn't load: {error}</div> : null}
      {contacts === null && !error ? <div style={{ color: "var(--fg-2)" }}>Loading…</div> : null}

      {selected.size > 0 && (
        <BulkBar count={selected.size} busy={bulkBusy} onDelete={bulkDelete} onClear={() => setSelected(new Set())} />
      )}

      {contacts && filtered.length === 0 ? (
        <div className="card" style={{ textAlign: "center", padding: 48, color: "var(--fg-3)" }}>
          {q.trim()
            ? <>No contacts match <code>{q}</code>.</>
            : "No recipients yet — they'll show up here once you add suppliers to a collection."}
        </div>
      ) : null}

      {contacts && filtered.length > 0 ? (
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          <SelectAllHeader
            visibleIds={filtered.map((c) => c.email)}
            selected={selected}
            setSelected={setSelected}
            label="contacts"
          />
          {filtered.map((c) => {
            const isOpen = expandedEmail === c.email;
            return (
              <div
                key={c.email}
                className="card"
                style={{
                  padding: 0,
                  overflow: "hidden",
                  borderColor: isOpen ? "var(--sb-ultra-violet)" : undefined,
                  boxShadow: isOpen ? "0 0 0 2px var(--sb-ultra-violet-soft)" : undefined,
                  transition: "border-color 100ms ease, box-shadow 100ms ease",
                }}
              >
                {/* Header — clickable, toggles the accordion. */}
                <div
                  onClick={() => setExpandedEmail(isOpen ? null : c.email)}
                  style={{
                    display: "grid",
                    gridTemplateColumns: "28px auto 1fr auto auto",
                    alignItems: "center",
                    gap: 14,
                    padding: "14px 16px",
                    cursor: "pointer",
                    background: selected.has(c.email) ? "var(--sb-pale-amethyst)" : undefined,
                  }}
                >
                  <div onClick={(e) => { e.stopPropagation(); toggleSel(c.email); }}>
                    <input type="checkbox" checked={selected.has(c.email)} onChange={() => toggleSel(c.email)} onClick={(e) => e.stopPropagation()} />
                  </div>
                  <span style={{ fontSize: 12, color: "var(--fg-3)", width: 14, textAlign: "center" }}>
                    {isOpen ? "▾" : "▸"}
                  </span>
                  <div style={{ minWidth: 0 }}>
                    <div style={{ fontWeight: 600, fontSize: 14, color: "var(--fg-0)" }}>
                      {c.name || "(no name)"}
                    </div>
                    <div className="meta" style={{ fontSize: 12, marginTop: 2 }}>
                      <code style={{ fontSize: 11, color: "var(--fg-2)" }}>{c.email}</code>
                    </div>
                  </div>
                  <div>
                    <span className={cx("agent-status", statusClass(c.latest_status))} title="Most recent status across all collections">
                      {statusLabel(c.latest_status)}
                    </span>
                  </div>
                  <div style={{ fontSize: 12, color: "var(--fg-2)", whiteSpace: "nowrap" }}>
                    {c.campaign_count} {c.campaign_count === 1 ? "collection" : "collections"}
                  </div>
                </div>

                {/* Body — list of campaign instances. Each links to its
                    own recipient view. */}
                {isOpen ? (
                  <div style={{ borderTop: "1px solid var(--border-1)", background: "var(--bg-soft)" }}>
                    {(c.instances || []).map((ix) => (
                      <div
                        key={ix.recipient_id}
                        onClick={(e) => { e.stopPropagation(); onOpenRecipient && onOpenRecipient(ix.recipient_id); }}
                        style={{
                          display: "grid",
                          gridTemplateColumns: "1fr auto auto",
                          alignItems: "center",
                          gap: 14,
                          padding: "10px 16px 10px 44px",
                          cursor: "pointer",
                          borderBottom: "1px solid var(--border-1)",
                          background: "white",
                          transition: "background 100ms ease",
                        }}
                        onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-1)"; }}
                        onMouseLeave={(e) => { e.currentTarget.style.background = "white"; }}
                      >
                        <div style={{ fontWeight: 500, fontSize: 13, color: "var(--fg-0)", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                          {ix.campaign_name}
                        </div>
                        <span className={cx("agent-status", statusClass(ix.status))}>
                          {statusLabel(ix.status)}
                        </span>
                        <span style={{ color: "var(--fg-3)" }}>›</span>
                      </div>
                    ))}
                  </div>
                ) : null}
              </div>
            );
          })}
        </div>
      ) : null}
    </div>
  );
}


function ScreenCollectionsList({ apiBase, onPickDraft, onPickLive, onNew }) {
  const [campaigns, setCampaigns] = useState(null);
  const [filter, setFilter] = useState("all");
  const [q, setQ] = useState("");
  const [error, setError] = useState(null);
  const [selected, setSelected] = useState(new Set());
  const [deleting, setDeleting] = useState(false);

  const refresh = useCallback(() => {
    let cancelled = false;
    fetch(`${apiBase}/campaigns`)
      .then((r) => r.json())
      .then((d) => { if (!cancelled) setCampaigns(d); })
      .catch((e) => { if (!cancelled) setError(e.message); });
    return () => { cancelled = true; };
  }, [apiBase]);

  useEffect(() => { const cleanup = refresh(); return cleanup; }, [refresh]);

  function toggle(id) {
    setSelected((s) => {
      const n = new Set(s);
      if (n.has(id)) n.delete(id); else n.add(id);
      return n;
    });
  }
  async function deleteSelected() {
    if (selected.size === 0) return;
    if (!confirm(`Delete ${selected.size} collection${selected.size === 1 ? "" : "s"}?\n\nAll recipients, messages, drafts, and extractions are removed.`)) return;
    setDeleting(true);
    try {
      const results = await Promise.allSettled(
        Array.from(selected).map(async (id) => {
          const r = await fetch(`${apiBase}/campaigns/${id}`, { method: "DELETE" });
          if (!r.ok) {
            const detail = await r.text().catch(() => "");
            throw new Error(`${r.status} ${detail.slice(0, 200)}`);
          }
          return id;
        })
      );
      const failed = results.filter((r) => r.status === "rejected");
      if (failed.length > 0) {
        const msgs = failed.map((f) => f.reason?.message || String(f.reason)).slice(0, 3).join(" · ");
        alert(`Failed to delete ${failed.length} of ${results.length}: ${msgs}`);
      }
      setSelected(new Set());
      refresh();
    } catch (e) {
      alert(`Delete failed: ${e.message}`);
    } finally {
      setDeleting(false);
    }
  }

  const filtered = (campaigns || []).filter((c) =>
    (filter === "all" || c.status === filter) &&
    c.name.toLowerCase().includes(q.toLowerCase())
  );

  const rel = _ageLabel;

  return (
    <div>
      <p className="text" style={{ marginTop: 0, marginBottom: 14, color: "var(--fg-2)", fontSize: 13 }}>
        Drafts open the agent setup. Live ones open the recipient overview.
      </p>
      <div className="mock-toolbar" style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 16 }}>
        <input className="search" placeholder="Search collections…" value={q}
          onChange={(e) => setQ(e.target.value)}
          style={{ flex: 1, maxWidth: 360, padding: "10px 14px", border: "1px solid var(--border-1)", borderRadius: 10, fontSize: 13.5, background: "white", outline: "none", fontFamily: "inherit" }}
        />
        {["all", "draft", "active", "completed"].map((f) => (
          <button key={f} className={cx("filter-chip", filter === f && "active")} onClick={() => setFilter(f)}>
            {f[0].toUpperCase() + f.slice(1)}
          </button>
        ))}
      </div>

      {error ? <div style={{ color: "#9b1d1d" }}>Couldn't load: {error}</div> : null}
      {campaigns === null ? <div style={{ color: "var(--fg-2)" }}>Loading…</div> : null}

      {selected.size > 0 && (
        <BulkBar count={selected.size} busy={deleting} onDelete={deleteSelected} onClear={() => setSelected(new Set())} />
      )}

      <div className="mock-table">
        <div className="mock-row head" style={{ gridTemplateColumns: "32px 1fr 120px 160px 140px 24px" }}>
          <div>
            {(() => {
              const ids = filtered.map((c) => c.id);
              const all = ids.length > 0 && ids.every((id) => selected.has(id));
              const some = ids.some((id) => selected.has(id));
              const rf = (el) => { if (el) el.indeterminate = some && !all; };
              return (
                <input
                  ref={rf}
                  type="checkbox"
                  checked={all}
                  onChange={() => setSelected((s) => {
                    const n = new Set(s);
                    if (all) { for (const id of ids) n.delete(id); }
                    else { for (const id of ids) n.add(id); }
                    return n;
                  })}
                  onClick={(e) => e.stopPropagation()}
                  title="Select all (visible)"
                />
              );
            })()}
          </div>
          <div>Name</div><div>Status</div><div>Completed</div><div>Last activity</div><div></div>
        </div>
        {filtered.length === 0 && campaigns ? (
          <div className="mock-row" style={{ cursor: "default", color: "var(--fg-2)", fontStyle: "italic", gridTemplateColumns: "32px 1fr 120px 160px 140px 24px" }}>
            <div></div><div>No collections match.</div><div></div><div></div><div></div><div></div>
          </div>
        ) : null}
        {filtered.map((c) => {
          const isDraft = c.status === "draft";
          const onClick = () => isDraft ? onPickDraft(c) : onPickLive(c);
          const drafts = c.needs_approval_count || 0;
          const isSel = selected.has(c.id);
          return (
            <div key={c.id} className="mock-row" onClick={onClick} style={{ gridTemplateColumns: "32px 1fr 120px 160px 140px 24px", background: isSel ? "var(--sb-pale-amethyst)" : undefined }}>
              <div onClick={(e) => { e.stopPropagation(); toggle(c.id); }} style={{ display: "flex", alignItems: "center" }}>
                <input type="checkbox" checked={isSel} onChange={() => toggle(c.id)} onClick={(e) => e.stopPropagation()} />
              </div>
              <div>
                <div className="name">{c.name || "(untitled)"}</div>
                <div className="meta">
                  {(c.fields || []).length} field{(c.fields || []).length === 1 ? "" : "s"}
                  · {c.recipient_count} recipient{c.recipient_count === 1 ? "" : "s"}
                  {drafts > 0 ? <span style={{ marginLeft: 10, color: "var(--sb-ultra-violet)", fontWeight: 600 }}>· {drafts} need approval</span> : null}
                </div>
              </div>
              <div><span className={cx("agent-status", c.status)}>{c.status}</span></div>
              <div className="progress">
                {c.recipient_count > 0 ? (
                  <>
                    <span>{c.completed_count ?? 0} / {c.recipient_count}</span>
                    <span className="bar"><span style={{ width: `${((c.completed_count ?? 0) / c.recipient_count) * 100}%` }} /></span>
                  </>
                ) : <span style={{ color: "var(--fg-2)" }}>—</span>}
              </div>
              <div className="activity">{rel(c.last_activity_at)}</div>
              <div className="arrow">›</div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// Manual "Add recipient" modal — used by the Recipients tab. Lets a CSM
// drop a supplier into any existing campaign without going through CSV
// upload or the setup wizard.
function AddRecipientModal({ apiBase, campaigns, onClose, onAdded }) {
  // "" sentinel means "no collection — just save the contact." The
  // backend parks those on a hidden "Saved contacts" campaign.
  const [campaignId, setCampaignId] = useState("");
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [scopeRaw, setScopeRaw] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  async function submit() {
    setErr("");
    if (!name.trim()) { setErr("Supplier name is required."); return; }
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { setErr("Valid email is required."); return; }
    setBusy(true);
    try {
      const r = await fetch(`${apiBase}/recipients`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          campaign_id: campaignId || null,
          supplier_name: name.trim(),
          contact_email: email.trim().toLowerCase(),
          scope: scopeRaw.trim() ? { raw: scopeRaw.trim() } : {},
        }),
      });
      if (!r.ok) {
        setErr(`${r.status} ${(await r.text()).slice(0, 200)}`);
      } else {
        await onAdded();
      }
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  }

  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(23,0,59,0.45)", zIndex: 100,
      display: "flex", alignItems: "center", justifyContent: "center", padding: 20,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: "white", borderRadius: 14, padding: 22, width: "min(500px, 100%)",
        boxShadow: "0 20px 60px rgba(23,0,59,0.25)",
      }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 14 }}>
          <div style={{ fontWeight: 600, fontSize: 16 }}>Add recipient</div>
          <button className="btn quiet sm" onClick={onClose}>✕</button>
        </div>
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12, color: "var(--fg-2)" }}>
            Collection (optional)
            <select
              value={campaignId}
              onChange={(e) => setCampaignId(e.target.value)}
              style={{ padding: "8px 10px", border: "1px solid var(--border-1)", borderRadius: 8, fontSize: 13.5, background: "white", fontFamily: "inherit" }}
            >
              <option value="">— no collection (just save the contact) —</option>
              {(campaigns || []).map((c) => (
                <option key={c.id} value={c.id}>{c.name || "(untitled)"}</option>
              ))}
            </select>
          </label>
          <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12, color: "var(--fg-2)" }}>
            Supplier name
            <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Acme Corp"
              style={{ padding: "8px 10px", border: "1px solid var(--border-1)", borderRadius: 8, fontSize: 13.5, fontFamily: "inherit" }} />
          </label>
          <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12, color: "var(--fg-2)" }}>
            Contact email
            <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="contact@acme.com"
              style={{ padding: "8px 10px", border: "1px solid var(--border-1)", borderRadius: 8, fontSize: 13.5, fontFamily: "inherit" }} />
          </label>
          <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12, color: "var(--fg-2)" }}>
            Scope (optional — parts, SKUs, notes)
            <input value={scopeRaw} onChange={(e) => setScopeRaw(e.target.value)} placeholder="MPN-123, MPN-456"
              style={{ padding: "8px 10px", border: "1px solid var(--border-1)", borderRadius: 8, fontSize: 13.5, fontFamily: "inherit" }} />
          </label>
          {err && <div style={{ fontSize: 12, color: "#9b1d1d" }}>{err}</div>}
          <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 4 }}>
            <button className="btn quiet sm" onClick={onClose} disabled={busy}>Cancel</button>
            <button className="new-btn" onClick={submit} disabled={busy} style={{ marginLeft: 0 }}>
              {busy ? "Adding…" : "Add recipient"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

// Select-all header — a tiny row that lives above a list-card and toggles
// every visible id in/out of the parent's selected Set. Used by all four
// list screens.
function SelectAllHeader({ visibleIds, selected, setSelected, label }) {
  const allChecked = visibleIds.length > 0 && visibleIds.every((id) => selected.has(id));
  const someChecked = visibleIds.some((id) => selected.has(id));
  const ref = useRef(null);
  useEffect(() => {
    if (ref.current) ref.current.indeterminate = someChecked && !allChecked;
  }, [someChecked, allChecked]);
  function toggleAll() {
    setSelected((s) => {
      const n = new Set(s);
      if (allChecked) {
        for (const id of visibleIds) n.delete(id);
      } else {
        for (const id of visibleIds) n.add(id);
      }
      return n;
    });
  }
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "8px 14px", marginBottom: 6, fontSize: 12, color: "var(--fg-2)" }}>
      <input ref={ref} type="checkbox" checked={allChecked} onChange={toggleAll} />
      <span style={{ cursor: "pointer" }} onClick={toggleAll}>
        {allChecked ? "Deselect all" : `Select all${label ? ` ${label}` : ""} (${visibleIds.length})`}
      </span>
    </div>
  );
}

// Shared bulk-action bar — shows a sticky toolbar when one or more rows are
// selected. Used by all four list screens (collections, inbox, sent,
// recipients) for parallel bulk-delete.
function BulkBar({ count, busy, onDelete, onClear, deleteLabel }) {
  return (
    <div style={{
      display: "flex", alignItems: "center", gap: 12,
      padding: "10px 14px", marginBottom: 12,
      background: "var(--sb-pale-amethyst)",
      border: "1px solid var(--sb-ultra-violet)",
      borderRadius: 10, fontSize: 13,
      position: "sticky", top: 0, zIndex: 5,
    }}>
      <span style={{ fontWeight: 600 }}>{count} selected</span>
      <button className="btn quiet sm" onClick={onClear} disabled={busy}>Clear</button>
      <div style={{ flex: 1 }} />
      <button
        className="btn sm"
        onClick={onDelete}
        disabled={busy}
        style={{ background: "var(--warm)", color: "white", border: "none" }}
      >
        {busy ? "Deleting…" : (deleteLabel || `Delete selected (${count})`)}
      </button>
    </div>
  );
}

// =========================================================================
// Screen — Collection (live campaign): tile grid + collection chat
// =========================================================================
function ScreenCollection({ apiBase, campaignId, onBack, onPickRecipient }) {
  const [data, setData] = useState(null);
  const [tileFilter, setTileFilter] = useState("all");
  const [q, setQ] = useState("");
  const [convId, setConvId] = useState(null);
  const [chatMessages, setChatMessages] = useState([]);
  const [chatInput, setChatInput] = useState("");
  const [chatBusy, setChatBusy] = useState(false);
  // Suppliers (tiles, default) vs Parts (campaign-wide parts table).
  // Reflected in the URL as ?view=parts so refresh/share preserves it.
  const _initialViewMode = (new URLSearchParams(window.location.search).get("view") === "parts")
    ? "parts" : "suppliers";
  const [viewMode, _setViewMode] = useState(_initialViewMode);
  const setViewMode = useCallback((m) => {
    const next = m === "parts" ? "parts" : "suppliers";
    _setViewMode(next);
    _setUrlParam("view", next === "parts" ? "parts" : null);
  }, []);

  // Initial fetch + start a conversation
  const refreshData = useCallback(() => {
    fetch(`${apiBase}/campaigns/${campaignId}/operate`)
      .then((r) => r.json()).then(setData)
      .catch((e) => setData({ error: e.message }));
  }, [apiBase, campaignId]);

  useEffect(() => {
    refreshData();
    fetch(`${apiBase}/collections/${campaignId}/conversations`, {
      method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}),
    }).then((r) => r.json()).then((d) => {
      setConvId(d.conversation_id);
      if (Array.isArray(d.display_log) && d.display_log.length) {
        setChatMessages(_replayDisplayLog(d.display_log));
      }
    });
    // Poll for tile + KPI updates every 5s while this view is open.
    const id = setInterval(() => { refreshData(); }, 5000);
    return () => clearInterval(id);
  }, [apiBase, campaignId, refreshData]);

  async function sendChat(text, opts = {}) {
    if (!convId || !text.trim()) return;
    setChatBusy(true);
    if (!opts.suppressUserInUI) {
      setChatMessages((m) => [...m, { role: "user", content: opts.displayContent || text }]);
    }
    try {
      await _streamOperatorMessage(apiBase, "collection", campaignId, convId, text, (event, payload) => {
        if (event === "text") {
          setChatMessages((m) => _appendBeforeTrailingChoice(m, { role: "assistant", content: payload.text }));
        } else if (event === "tool_call") {
          if (payload.name === "present_choices" || payload.name === "offer_exports") return;
          setChatMessages((m) => _appendBeforeTrailingChoice(m, { role: "tool", content: { name: payload.name, args: payload.args } }));
        } else if (event === "ui_choice") {
          setChatMessages((m) => [..._dismissOlderPendingInteractives(m), { role: "choices", content: { ...payload, answered: false } }]);
        } else if (event === "ui_exports") {
          setChatMessages((m) => [...m, { role: "exports", content: payload }]);
        } else if (event === "tool_result") {
          // After every tool result, refresh the tile data
          refreshData();
        } else if (event === "error") {
          setChatMessages((m) => [...m, { role: "assistant", content: `[error] ${payload.message}` }]);
        }
      }, {
        displayContent: opts.displayContent,
        suppressUserInLog: !!opts.suppressUserInLog,
      });
    } catch (e) {
      setChatMessages((m) => [...m, { role: "assistant", content: `[network error] ${e.message}` }]);
    } finally {
      setChatBusy(false);
      refreshData();
    }
  }
  function pickChoice(idx, option) {
    setChatMessages((m) => {
      const copy = [...m];
      const blk = copy[idx];
      if (blk && blk.role === "choices") {
        copy[idx] = { ...blk, content: { ...blk.content, answered: true, picked: option.id } };
      }
      return copy;
    });
    setChatMessages((m) => [...m, { role: "user", content: option.label }]);
    sendChat(option.send_message, { suppressUserInUI: true, displayContent: option.label });
  }

  if (!data) return <div style={{ padding: 36, color: "var(--fg-2)" }}>Loading…</div>;
  if (data.error) return <div style={{ padding: 36, color: "#9b1d1d" }}>Couldn't load: {data.error}</div>;

  const tiles = data.tiles || [];
  const stats = {
    ...data.kpis,
    drafts: data.kpis?.needs_approval || 0,
  };
  const visible = tileFilter === "all" ? tiles
    : tileFilter === "needs-approval" ? tiles.filter((t) => t.needs_approval)
    : tileFilter === "completed" ? tiles.filter((t) => t.status === "completed")
    : tileFilter === "escalated" ? tiles.filter((t) => t.status === "escalated")
    : tileFilter === "awaiting" ? tiles.filter((t) => ["first_sent", "followup_sent", "awaiting_reply", "reply_received", "awaiting_approval"].includes(t.status))
    : tiles;

  // Multi-token AND match across recipient name / email / status /
  // approval reason. Same fuzzy-substring style as Inbox + Sent.
  const filteredTiles = q.trim()
    ? visible.filter((t) => _matchesQuery(
        [t.name, t.email, t.status, t.approval_reason].join(" "), q
      ))
    : visible;

  const tileFilters = [
    ["all", "All", stats.total],
    ["needs-approval", "Needs approval", stats.needs_approval],
    ["awaiting", "Awaiting reply", stats.awaiting],
    ["completed", "Completed", stats.completed],
    ["escalated", "Escalated", stats.escalated],
  ];

  return (
    <div className="mock-coll" style={{ display: "grid", gridTemplateRows: "auto 1fr", height: "100vh", minHeight: 0, background: "var(--bg-soft)" }}>
      <PageHeader
        crumbs={[
          { label: "Data collections", onClick: onBack },
          { label: data.campaign.name },
        ]}
        subtitle={(() => {
          const status = data.campaign.status
            ? data.campaign.status.charAt(0).toUpperCase() + data.campaign.status.slice(1)
            : "Active";
          const fields = data.tiles?.[0]?.fields_total ?? 0;
          const parts = [status];
          if (fields) parts.push(`${fields} ${fields === 1 ? "field" : "fields"}`);
          parts.push(`${stats.total} ${stats.total === 1 ? "recipient" : "recipients"}`);
          return parts.join(" · ");
        })()}
        meta={(
          <div className="kpis">
            <div className="kpi"><div className="v">{stats.total}</div><div className="l">Recipients</div></div>
            <div className="kpi"><div className="v" style={{ color: "#186e44" }}>{stats.completed}</div><div className="l">Completed</div></div>
            <div className="kpi"><div className="v">{stats.awaiting}</div><div className="l">Awaiting</div></div>
            <div className="kpi"><div className="v" style={{ color: "#9b1d1d" }}>{stats.escalated}</div><div className="l">Escalated</div></div>
            {stats.needs_approval > 0 && (
              <div className="kpi"><div className="v" style={{ color: "var(--sb-ultra-violet)" }}>{stats.needs_approval}</div><div className="l">Needs approval</div></div>
            )}
          </div>
        )}
      />

      <div className="mock-coll-body">
        <div className="mock-coll-tiles">
          {/* Top zone: view toggle + search + filter chips + match-narrowed hint.
              Stays fixed; doesn't scroll with the tile grid / parts table. */}
          <div className="mock-coll-tiles-top">
            <div style={{ display: "flex", gap: 6, marginBottom: 14 }}>
              <button
                className={cx("filter-chip", viewMode === "suppliers" && "active")}
                onClick={() => setViewMode("suppliers")}
              >
                Suppliers
              </button>
              <button
                className={cx("filter-chip", viewMode === "parts" && "active")}
                onClick={() => setViewMode("parts")}
              >
                Parts
              </button>
            </div>
            {viewMode === "suppliers" ? (
              <>
                <p className="text" style={{ marginTop: 0, marginBottom: 14, color: "var(--fg-2)", fontSize: 13 }}>
                  Click a recipient to open their thread and extracted values.
                </p>
                <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap", marginBottom: 18 }}>
                  <input
                    className="search"
                    placeholder="Search recipients…"
                    value={q}
                    onChange={(e) => setQ(e.target.value)}
                    style={{ flex: 1, minWidth: 200, maxWidth: 320, padding: "10px 14px", border: "1px solid var(--border-1)", borderRadius: 10, fontSize: 13.5, background: "white", outline: "none", fontFamily: "inherit" }}
                  />
                  {tileFilters.map(([id, label, count]) => (
                    <button key={id} className={cx("filter-chip", tileFilter === id && "active")} onClick={() => setTileFilter(id)}>
                      {label}
                      {count > 0 ? <span style={{ marginLeft: 7, padding: "1px 7px", borderRadius: 999, background: tileFilter === id ? "rgba(255,255,255,.22)" : "var(--bg-1)", fontSize: 11, fontWeight: 600 }}>{count}</span> : null}
                    </button>
                  ))}
                </div>
                {q.trim() && filteredTiles.length < visible.length ? (
                  <div className="meta" style={{ fontSize: 12, marginBottom: 10 }}>
                    {filteredTiles.length} of {visible.length} match <code>{q}</code>
                  </div>
                ) : null}
              </>
            ) : (
              <p className="text" style={{ marginTop: 0, marginBottom: 14, color: "var(--fg-2)", fontSize: 13 }}>
                One row per (supplier, part). Click a row to open the supplier's thread, or tick rows to enable <strong>Download selected</strong> (the header checkbox selects every visible row, respecting any filter).
              </p>
            )}
          </div>

          {/* Middle zone: tile grid (suppliers view) OR parts table. Scrolls. */}
          <div className="mock-coll-tiles-scroll">
            {viewMode === "suppliers" ? (
              <div className="tile-grid">
                {filteredTiles.length === 0 ? (
                  <div style={{ gridColumn: "1 / -1", padding: 24, color: "var(--fg-2)", fontStyle: "italic" }}>
                    {q.trim()
                      ? <>No recipients match <code>{q}</code>{tileFilter !== "all" && <> in <strong>{tileFilter.replace("-", " ")}</strong></>}.</>
                      : "No recipients in this view."}
                  </div>
                ) : null}
                {filteredTiles.map((r) => (
                  <div key={r.id} className={cx(
                    "tile",
                    r.needs_approval && "needs-approval",
                    !r.needs_approval && r.status === "escalated" && "escalated",
                    !r.needs_approval && r.status === "completed" && "completed",
                  )} onClick={() => onPickRecipient(r)}>
                    {r.needs_approval ? <span className="approval-badge">Needs approval</span> : null}
                    <div className="name">{r.name}</div>
                    <div className="email">{r.email}</div>
                    {r.needs_approval && r.approval_reason ? <div className="approval-reason">{r.approval_reason}</div> : null}
                    <div className="meta">
                      <span className={cx("agent-status", r.status)}>{r.status.replace(/_/g, " ")}</span>
                      <span className="fill-bar"><span style={{ width: r.fields_total ? `${(r.fields_filled / r.fields_total) * 100}%` : 0 }} /></span>
                      <span style={{ whiteSpace: "nowrap" }}>{r.fields_filled}/{r.fields_total}</span>
                    </div>
                  </div>
                ))}
              </div>
            ) : (
              <PartsTable apiBase={apiBase} campaignId={campaignId} onPickRecipient={onPickRecipient} />
            )}
          </div>

          {/* Bottom zone: brief, pinned. Stays in view while you scroll
              the tile grid. Long briefs collapse to ~240 chars + a
              "Show full brief" toggle so they don't crowd the tiles. */}
          {data.campaign.brief ? (
            <div className="mock-coll-tiles-brief">
              <BriefCard text={data.campaign.brief} />
            </div>
          ) : null}
        </div>

        <aside className="workspace-chat">
          <div className="head">
            <h3>🤖 Collection agent</h3>
            <div className="sub">Cross-recipient commands for this collection.</div>
          </div>
          <div className="msgs">
            {chatMessages.length === 0 ? (
              <div style={{ color: "var(--fg-2)", fontSize: 12.5, fontStyle: "italic" }}>
                Try: "pause beta and helios", "what needs approval?", "draft a custom note for alice".
              </div>
            ) : null}
            {chatMessages.map((m, i) => {
              if (m.role === "user") return <div key={i} className="mock-event-user" style={{ alignSelf: "flex-end", maxWidth: "100%" }}>{m.content}</div>;
              if (m.role === "assistant") return <div key={i} className="mock-event-agent" style={{ alignSelf: "flex-start", maxWidth: "100%" }}>{m.content}</div>;
              if (m.role === "tool") {
                const f = describeToolCall(m.content.name, m.content.args);
                return <div key={i} className="agent-msg-tool"><span className="tname">▸ {f.label}</span>{f.detail ? <span className="tdetail"> · {f.detail}</span> : null}</div>;
              }
              if (m.role === "choices") {
                const { question, options, answered, picked } = m.content;
                if (answered) {
                  return question ? (
                    <div key={i} className="mock-event-agent" style={{ alignSelf: "flex-start", maxWidth: "100%" }}>{question}</div>
                  ) : null;
                }
                return (
                  <div key={i} className="agent-choices" style={_PENDING_CHOICE_STYLE}>
                    {question ? <div className="agent-choices-q">{question}</div> : null}
                    <div className="agent-choices-row">
                      {(options || []).map((o) => (
                        <button
                          key={o.id}
                          className={cx("agent-choice-btn", answered && picked === o.id && "picked")}
                          disabled={answered || chatBusy}
                          onClick={() => pickChoice(i, o)}
                        >
                          {o.label}
                        </button>
                      ))}
                    </div>
                  </div>
                );
              }
              if (m.role === "exports") {
                return <ExportsBlock key={i} apiBase={apiBase} exports={m.content.exports} />;
              }
              return null;
            })}
            {chatBusy ? <div className="agent-thinking"><span className="agent-thinking-spinner" /><span className="agent-thinking-label">Agent is thinking…</span></div> : null}
          </div>
          <div className={cx("agent-composer", chatBusy && "busy")}>
            <textarea
              rows={1}
              placeholder={chatBusy ? "Wait for the agent to finish…" : "e.g. 'pause beta and helios until next monday'"}
              value={chatInput}
              onChange={(e) => setChatInput(e.target.value)}
              disabled={chatBusy || !convId}
              onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); const v = chatInput.trim(); if (!v) return; setChatInput(""); sendChat(v); } }}
            />
            <button onClick={() => { const v = chatInput.trim(); if (!v) return; setChatInput(""); sendChat(v); }} disabled={chatBusy || !chatInput.trim() || !convId}>
              {chatBusy ? "Working…" : "Send"}
            </button>
          </div>
        </aside>
      </div>
    </div>
  );
}

// =========================================================================
// Screen — Recipient (per-supplier chat + extracted values panel)
// =========================================================================
function ScreenRecipient({ apiBase, recipientId, onBackList, onBackCollection }) {
  const [thread, setThread] = useState(null);
  const [convId, setConvId] = useState(null);
  const [chatMessages, setChatMessages] = useState([]);
  const [chatInput, setChatInput] = useState("");
  const [chatBusy, setChatBusy] = useState(false);
  const [editingDraft, setEditingDraft] = useState(null); // { id, subject, body_text } | null
  const [busyDrafts, setBusyDrafts] = useState(() => new Set());

  const refresh = useCallback(() => {
    fetch(`${apiBase}/recipients/${recipientId}/operate-chat`)
      .then((r) => r.json()).then(setThread)
      .catch((e) => setThread({ error: e.message }));
  }, [apiBase, recipientId]);

  useEffect(() => {
    refresh();
    fetch(`${apiBase}/recipients/${recipientId}/conversations`, {
      method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}),
    }).then((r) => r.json()).then((d) => {
      setConvId(d.conversation_id);
      if (Array.isArray(d.display_log) && d.display_log.length) {
        setChatMessages(_replayDisplayLog(d.display_log));
      }
    });
    // Poll the operate-chat endpoint every 5s so background events
    // (orchestrator writing a draft, an inbound reply landing) appear
    // without a page reload. Cheap — it's a small JSON read.
    const id = setInterval(() => { refresh(); }, 5000);
    return () => clearInterval(id);
  }, [apiBase, recipientId, refresh]);

  async function approveDraft(draftId) {
    if (busyDrafts.has(draftId)) return;
    setBusyDrafts((p) => new Set(p).add(draftId));
    try {
      const r = await fetch(`${apiBase}/drafts/${draftId}/approve`, { method: "POST" });
      if (!r.ok && r.status !== 409) console.error("approve failed", r.status);
    } finally {
      refresh();
    }
  }
  async function rejectDraft(draftId) {
    if (busyDrafts.has(draftId)) return;
    setBusyDrafts((p) => new Set(p).add(draftId));
    try {
      const r = await fetch(`${apiBase}/drafts/${draftId}/reject`, { method: "POST" });
      if (!r.ok && r.status !== 409) console.error("reject failed", r.status);
    } finally {
      refresh();
    }
  }
  async function saveDraftEdit() {
    if (!editingDraft) return;
    await fetch(`${apiBase}/drafts/${editingDraft.id}/edit`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ subject: editingDraft.subject, body_text: editingDraft.body_text }),
    });
    setEditingDraft(null);
    refresh();
  }

  async function sendChat(text, opts = {}) {
    if (!convId || !text.trim()) return;
    setChatBusy(true);
    if (!opts.suppressUserInUI) {
      setChatMessages((m) => [...m, { role: "user", content: opts.displayContent || text }]);
    }
    try {
      await _streamOperatorMessage(apiBase, "recipient", recipientId, convId, text, (event, payload) => {
        if (event === "text") {
          setChatMessages((m) => _appendBeforeTrailingChoice(m, { role: "assistant", content: payload.text }));
        } else if (event === "tool_call") {
          if (payload.name === "present_choices" || payload.name === "offer_exports") return;
          setChatMessages((m) => _appendBeforeTrailingChoice(m, { role: "tool", content: { name: payload.name, args: payload.args } }));
        } else if (event === "ui_choice") {
          setChatMessages((m) => [..._dismissOlderPendingInteractives(m), { role: "choices", content: { ...payload, answered: false } }]);
        } else if (event === "ui_exports") {
          setChatMessages((m) => [...m, { role: "exports", content: payload }]);
        } else if (event === "tool_result") {
          refresh();
        } else if (event === "error") {
          setChatMessages((m) => [...m, { role: "assistant", content: `[error] ${payload.message}` }]);
        }
      }, {
        displayContent: opts.displayContent,
        suppressUserInLog: !!opts.suppressUserInLog,
      });
    } catch (e) {
      setChatMessages((m) => [...m, { role: "assistant", content: `[network error] ${e.message}` }]);
    } finally {
      setChatBusy(false);
      refresh();
    }
  }
  function pickChoice(idx, option) {
    setChatMessages((m) => {
      const copy = [...m];
      const blk = copy[idx];
      if (blk && blk.role === "choices") {
        copy[idx] = { ...blk, content: { ...blk.content, answered: true, picked: option.id } };
      }
      return copy;
    });
    setChatMessages((m) => [...m, { role: "user", content: option.label }]);
    sendChat(option.send_message, { suppressUserInUI: true, displayContent: option.label });
  }

  if (!thread) return <div style={{ padding: 36, color: "var(--fg-2)" }}>Loading…</div>;
  if (thread.error) return <div style={{ padding: 36, color: "#9b1d1d" }}>Couldn't load: {thread.error}</div>;

  const r = thread.recipient;
  const tlEvents = thread.events || [];
  // Merge timeline events with the live chat messages (which represent
  // the user's typed turns + the agent's text responses to them).
  // _liveIdx points back to the original chatMessages index so the
  // choices renderer can call pickChoice(idx, option).
  const mergedEvents = [
    ...tlEvents,
    ...chatMessages.map((m, i) => ({
      kind: m.role === "user" ? "user"
        : m.role === "assistant" ? "assistant"
        : m.role,
      id: `live-${i}`,
      _liveIdx: i,
      ts: new Date().toISOString(),
      content: m.content,
    })),
  ];

  return (
    <div className="mock-rcp" style={{ display: "grid", gridTemplateRows: "auto 1fr", height: "100vh", minHeight: 0, background: "var(--bg-soft)" }}>
      <PageHeader
        crumbs={[
          { label: "Data collections", onClick: onBackList },
          { label: thread.campaign.name, onClick: onBackCollection },
          { label: r.name },
        ]}
        subtitle={r.email}
        meta={(
          <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
            <span className={cx("agent-status", r.status)}>{r.status.replace(/_/g, " ")}</span>
            {r.needs_approval ? (
              <span className="approval-badge" style={{ position: "static" }}>Needs approval</span>
            ) : null}
            {r.needs_approval && r.approval_reason ? (
              <span className="approval-reason" style={{ marginTop: 0 }}>{r.approval_reason}</span>
            ) : null}
          </div>
        )}
      />

      <div className="mock-rcp-body">
        <div className="mock-rcp-chat-col">
          <div className="mock-rcp-events">
            {mergedEvents.length === 0 ? (
              <div style={{ color: "var(--fg-2)", fontStyle: "italic", fontSize: 13 }}>
                No activity yet. The first-touch email will appear here once it's sent.
              </div>
            ) : null}
            {mergedEvents.map((e) => {
              if (e.kind === "user") return <div key={e.id} className="mock-event-user">{e.content}</div>;
              if (e.kind === "assistant") return <div key={e.id} className="mock-event-agent">{e.content}</div>;
              if (e.kind === "tool" && e.content) {
                const f = describeToolCall(e.content.name, e.content.args);
                return <div key={e.id} className="agent-msg-tool"><span className="tname">▸ {f.label}</span>{f.detail ? <span className="tdetail"> · {f.detail}</span> : null}</div>;
              }
              if (e.kind === "exports") {
                return <ExportsBlock key={e.id} apiBase={apiBase} exports={e.content?.exports || []} />;
              }
              if (e.kind === "choices") {
                const { question, options, answered, picked } = e.content || {};
                if (answered) {
                  return question ? <div key={e.id} className="mock-event-agent">{question}</div> : null;
                }
                return (
                  <div key={e.id} className="agent-choices" style={_PENDING_CHOICE_STYLE}>
                    {question ? <div className="agent-choices-q">{question}</div> : null}
                    <div className="agent-choices-row">
                      {(options || []).map((o) => (
                        <button
                          key={o.id}
                          className={cx("agent-choice-btn", answered && picked === o.id && "picked")}
                          disabled={answered || chatBusy}
                          onClick={() => pickChoice(e._liveIdx, o)}
                        >
                          {o.label}
                        </button>
                      ))}
                    </div>
                  </div>
                );
              }
              if (e.kind === "tool" && e.name) {
                const label = _AGENT_RUN_LABELS[e.name] || e.name.replace(/_/g, " ");
                return (
                  <div key={e.id} className="mock-event-tool">
                    {e.running
                      ? <span className="agent-thinking-spinner" style={{ width: 12, height: 12, borderWidth: 1.5, display: "inline-block", verticalAlign: "middle", marginRight: 6 }} />
                      : <span className="tname-mark" style={{ marginRight: 4 }}>▸</span>}
                    <span className="tname">{e.running ? `${label}…` : label}</span>
                    {e.summary ? <span className="tdetail"> · {e.summary}</span> : null}
                  </div>
                );
              }
              if (e.kind === "inbound") return (
                <div key={e.id} className="mock-event-inbound">
                  <span className="mock-event-lane supplier"><span className="dot" />Supplier email</span>
                  <div className="ts">{new Date(e.ts).toLocaleString()}</div>
                  <div className="from"><span className="who">{r.name}</span><span>&lt;{e.from || r.email}&gt;</span></div>
                  {e.subject ? <div style={{ fontWeight: 600, fontSize: 13.5, color: "var(--fg-0)", marginBottom: 4 }}>{e.subject}</div> : null}
                  <div className="body-text">{e.body_text}</div>
                </div>
              );
              if (e.kind === "bounce") return (
                <div key={e.id} className="mock-event-bounce">
                  <span className="mock-event-lane bounce"><span className="dot" />Mail delivery failed</span>
                  <div className="ts">{new Date(e.ts).toLocaleString()}</div>
                  <div className="from">
                    <span className="who" style={{ color: "#9b1d1d" }}>⚠ {r.email}</span>
                  </div>
                  {e.subject ? <div style={{ fontWeight: 600, fontSize: 13.5, color: "var(--fg-0)", marginBottom: 4 }}>{e.subject}</div> : null}
                  <div className="body-text">{e.body_text}</div>
                </div>
              );
              if (e.kind === "outbound") return (
                <div key={e.id} className="mock-event-outbound">
                  <span className="mock-event-lane agent"><span className="dot" />Agent email · sent</span>
                  <div className="ts">{new Date(e.ts).toLocaleString()}</div>
                  <div className="from"><span className="who">→ {r.name}</span></div>
                  {e.subject ? <div style={{ fontWeight: 600, fontSize: 13.5, color: "var(--fg-0)", marginBottom: 4 }}>{e.subject}</div> : null}
                  <div className="body-text" style={{ fontSize: 13 }}>{e.body_text}</div>
                </div>
              );
              if (e.kind === "draft") {
                const isEditing = editingDraft && editingDraft.id === e.draft_id;
                const sending = busyDrafts.has(e.draft_id);
                return (
                  <div key={e.id} className={cx("mock-event-draft", isEditing && "editing")}>
                    <span className="mock-event-lane draft"><span className="dot" />Draft · awaiting your approval</span>
                    <div className="ts">{new Date(e.ts).toLocaleString()}</div>
                    <div className="from"><span className="who">→ {r.email}</span></div>
                    {isEditing ? (
                      <div className="mock-draft-edit">
                        <input
                          className="mock-draft-subject-input"
                          value={editingDraft.subject}
                          onChange={(ev) => setEditingDraft((d) => ({ ...d, subject: ev.target.value }))}
                          placeholder="Subject"
                        />
                        <textarea
                          className="mock-draft-body-input"
                          rows={8}
                          value={editingDraft.body_text}
                          onChange={(ev) => setEditingDraft((d) => ({ ...d, body_text: ev.target.value }))}
                        />
                        <div className="mock-draft-actions">
                          <button className="primary" onClick={saveDraftEdit}>Save</button>
                          <button onClick={() => setEditingDraft(null)}>Cancel</button>
                        </div>
                      </div>
                    ) : (
                      <>
                        <div className="mock-draft-head">
                          <div style={{ fontWeight: 600, fontSize: 13.5, color: "var(--fg-0)" }}>{e.subject}</div>
                          <button
                            className="agent-edit-btn"
                            title="Edit draft"
                            aria-label="Edit draft"
                            onClick={() => setEditingDraft({ id: e.draft_id, subject: e.subject, body_text: e.body_text })}
                          >✎</button>
                        </div>
                        <div className="body-text">{e.body_text}</div>
                        <div className="mock-draft-actions">
                          <button className="primary" disabled={sending} onClick={() => approveDraft(e.draft_id)}>{sending ? "Sending…" : "Approve & send"}</button>
                          <button disabled={sending} onClick={() => rejectDraft(e.draft_id)}>Skip</button>
                        </div>
                      </>
                    )}
                  </div>
                );
              }
              return null;
            })}
            {chatBusy ? <div className="agent-thinking"><span className="agent-thinking-spinner" /><span className="agent-thinking-label">Agent is thinking…</span></div> : null}
          </div>

          <div className={cx("agent-composer", chatBusy && "busy")}>
            <textarea rows={1}
              placeholder={chatBusy ? "Wait for the agent…" : `Talk to the agent about ${r.name}…`}
              value={chatInput}
              onChange={(e) => setChatInput(e.target.value)}
              disabled={chatBusy || !convId}
              onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); const v = chatInput.trim(); if (!v) return; setChatInput(""); sendChat(v); } }}
            />
            <button onClick={() => { const v = chatInput.trim(); if (!v) return; setChatInput(""); sendChat(v); }} disabled={chatBusy || !chatInput.trim() || !convId}>
              {chatBusy ? "Working…" : "Send"}
            </button>
          </div>
        </div>

        <div className="mock-rcp-side">
          <RecipientValuesPanel apiBase={apiBase} recipientId={recipientId} thread={thread} />
          <RecipientActionsPanel apiBase={apiBase} recipientId={recipientId} onChange={refresh} />
        </div>
      </div>
    </div>
  );
}

function RecipientValuesPanel({ apiBase, recipientId, thread }) {
  // Fetch the schema once (for the campaign-wide fallback view).
  const [fields, setFields] = useState(null);
  useEffect(() => {
    if (!thread?.campaign?.id) return;
    fetch(`${apiBase}/campaigns/${thread.campaign.id}/operate`)
      .then((r) => r.json()).then((d) => setFields(d.fields || []))
      .catch(() => setFields([]));
  }, [apiBase, thread?.campaign?.id]);
  // Poll the full thread-view every 5s so per-part rollups + flat
  // extractions stay current without a page reload.
  const [view, setView] = useState(null);
  useEffect(() => {
    let stop = false;
    const tick = () => {
      fetch(`${apiBase}/recipients/${recipientId}/thread-view`)
        .then((r) => r.json()).then((d) => { if (!stop) setView(d || {}); })
        .catch(() => { if (!stop) setView((v) => v ?? {}); });
    };
    tick();
    const id = setInterval(tick, 5000);
    return () => { stop = true; clearInterval(id); };
  }, [apiBase, recipientId]);
  if (!fields || !view) return <div className="mock-panel"><div className="mock-panel-body">Loading values…</div></div>;

  const partsRollup = view.parts || [];
  const oos = view.out_of_scope_mpns || [];
  const values = view.fields || [];
  const byName = new Map(values.map((v) => [v.name, v]));
  const filledFlat = fields.filter((f) => byName.get(f.name)?.extracted_value).length;

  // Per-part view when the campaign has parts in scope; otherwise the
  // flat campaign-wide list (same as before).
  if (partsRollup.length > 0) {
    // Download menu in the header: 4 formats. For single-part
    // recipients the "PDF" option points at that part's PDF; for
    // multi-part it points at the combined recipient-level packet.
    // Keeps the per-row accordions clean — one prominent download
    // affordance per panel.
    const dlOptions = recipientDownloadOptions({
      apiBase,
      recipientId,
      partsCount: partsRollup.length,
      singlePartMpn: partsRollup.length === 1 ? partsRollup[0].mpn : null,
    });
    return (
      <div className="mock-panel">
        <header>
          <h3>Extracted values</h3>
          <DownloadMenu
            label="↓ Download"
            title="Download options for this supplier"
            options={dlOptions}
            chipStyle={{ padding: "4px 10px", fontSize: 11.5 }}
          />
        </header>
        <div className="mock-panel-body" style={{ padding: "8px 10px" }}>
          {partsRollup.map((p) => (
            <PartValuesAccordion
              key={p.mpn}
              part={p}
              apiBase={apiBase}
              recipientId={recipientId}
              showDownload={false}
            />
          ))}
          {oos.length > 0 && (
            <div style={{
              padding: 10, marginTop: 10, borderRadius: 8,
              background: "#fff5e6", border: "1px solid var(--warm)", fontSize: 12,
            }}>
              <div style={{ fontWeight: 600, color: "var(--warm)", marginBottom: 4 }}>
                Out-of-scope MPNs mentioned
              </div>
              <div style={{ color: "var(--fg-2)" }}>
                {oos.join(", ")} — the supplier referenced these but they aren't in this collection.
              </div>
            </div>
          )}
        </div>
      </div>
    );
  }

  return (
    <div className="mock-panel">
      <header><h3>Extracted values</h3><span className="agent-pill">{filledFlat}/{fields.length}</span></header>
      <div className="mock-panel-body" style={{ padding: "8px 14px" }}>
        {fields.length === 0 ? <div style={{ color: "var(--fg-2)", fontStyle: "italic" }}>(no schema fields)</div> : null}
        {fields.map((f) => {
          const v = byName.get(f.name);
          const value = v?.extracted_value;
          return (
            <div key={f.name} className="field-line">
              <div>
                <div className="l">{f.label}</div>
                <div style={{ fontSize: 11, color: "var(--fg-3)", fontFamily: "ui-monospace, monospace" }}>{f.name}</div>
              </div>
              {value ? <div><span className="v">{value}</span></div> : <div><span className="v missing">missing</span></div>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function PartsTable({ apiBase, campaignId, onPickRecipient }) {
  const [data, setData] = useState(null);
  const [q, setQ] = useState("");
  const [statusFilter, setStatusFilter] = useState("all");
  // Set of selected row keys, format: `${recipient_id}::${mpn}`. Kept
  // as a Set so toggling is O(1). Empty Set => "no selection",
  // bulk-download chip falls back to its "all parts" mode.
  const [selected, setSelected] = useState(() => new Set());
  // ref to the header checkbox so we can drive its `.indeterminate`
  // tri-state — there's no React prop for it.
  const headerCheckRef = useRef(null);

  const refresh = useCallback(() => {
    fetch(`${apiBase}/campaigns/${campaignId}/parts-view`)
      .then((r) => r.json()).then(setData)
      .catch((e) => setData({ error: e.message }));
  }, [apiBase, campaignId]);

  useEffect(() => {
    refresh();
    const id = setInterval(refresh, 5000);
    return () => clearInterval(id);
  }, [refresh]);

  // Derived selection state — compute on every render (cheap) before any
  // early-return so subsequent hooks remain in the same order across
  // renders (Rules of Hooks). When `data` is null, these are empty
  // arrays / false flags and the indeterminate effect below is a no-op.
  const rowKey = (p) => `${p.recipient_id}::${p.mpn}`;
  const partsForSel = (data && data.parts) || [];
  const visibleForSel = statusFilter === "all" ? partsForSel : partsForSel.filter((p) => p.status === statusFilter);
  const filteredForSel = q.trim()
    ? visibleForSel.filter((p) => _matchesQuery(
        [p.mpn, p.name, p.recipient_name, p.recipient_email, p.status].join(" "), q
      ))
    : visibleForSel;
  const filteredKeys = filteredForSel.map(rowKey);
  const allFilteredSelected = filteredKeys.length > 0 && filteredKeys.every((k) => selected.has(k));
  const someFilteredSelected = filteredKeys.some((k) => selected.has(k));

  // Drive the header checkbox's indeterminate state from JS each render
  // (React has no prop for it). Safe when the ref is null — guards
  // run before the assignment. Must live BEFORE any early-return so
  // hook count is stable across the loading/loaded transition.
  useEffect(() => {
    if (headerCheckRef.current) {
      headerCheckRef.current.indeterminate = someFilteredSelected && !allFilteredSelected;
    }
  });

  if (!data) return <div style={{ padding: 24, color: "var(--fg-2)" }}>Loading parts…</div>;
  if (data.error) return <div style={{ padding: 24, color: "#9b1d1d" }}>Couldn't load: {data.error}</div>;

  const parts = partsForSel;
  const filtered = filteredForSel;

  const counts = {
    all: parts.length,
    completed: parts.filter((p) => p.status === "completed").length,
    in_progress: parts.filter((p) => p.status === "in_progress").length,
    not_started: parts.filter((p) => p.status === "not_started").length,
  };
  const filters = [
    ["all", "All", counts.all],
    ["completed", "Completed", counts.completed],
    ["in_progress", "In progress", counts.in_progress],
    ["not_started", "Not started", counts.not_started],
  ];

  function toggleRow(p) {
    setSelected((prev) => {
      const next = new Set(prev);
      const k = rowKey(p);
      if (next.has(k)) next.delete(k); else next.add(k);
      return next;
    });
  }
  function toggleHeader() {
    setSelected((prev) => {
      const next = new Set(prev);
      if (allFilteredSelected) {
        for (const k of filteredKeys) next.delete(k);
      } else {
        for (const k of filteredKeys) next.add(k);
      }
      return next;
    });
  }
  function clearSelection() {
    setSelected(new Set());
  }

  // Build the items= query value for the "selected" download menu.
  // Backend expects `rid:mpn,rid:mpn,...` — same shape, just swap
  // the `::` delimiter for `:`.
  const selectionItems = Array.from(selected).map((k) => k.replace("::", ":")).join(",");

  function openRecipient(p) {
    // Hash signals PartValuesAccordion to auto-open that part on mount.
    window.location.hash = `part=${encodeURIComponent(p.mpn)}`;
    onPickRecipient({ id: p.recipient_id });
  }

  return (
    <div>
      <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap", marginBottom: 14 }}>
        <input
          className="search"
          placeholder="Search parts (supplier, MPN, name)…"
          value={q}
          onChange={(e) => setQ(e.target.value)}
          style={{ flex: 1, minWidth: 200, maxWidth: 320, padding: "10px 14px", border: "1px solid var(--border-1)", borderRadius: 10, fontSize: 13.5, background: "white", outline: "none", fontFamily: "inherit" }}
        />
        {filters.map(([id, label, count]) => (
          <button key={id} className={cx("filter-chip", statusFilter === id && "active")} onClick={() => setStatusFilter(id)}>
            {label}
            {count > 0 ? <span style={{ marginLeft: 7, padding: "1px 7px", borderRadius: 999, background: statusFilter === id ? "rgba(255,255,255,.22)" : "var(--bg-1)", fontSize: 11, fontWeight: 600 }}>{count}</span> : null}
          </button>
        ))}
        {selected.size > 0 ? (
          <>
            <DownloadMenu
              label={`↓ Download selected (${selected.size})`}
              title={`Download options for ${selected.size} selected part${selected.size === 1 ? "" : "s"}`}
              options={[
                { id: "pdf",  label: "Combined PDF (selected)",   url: `${apiBase}/campaigns/${campaignId}/parts-audit.pdf?items=${encodeURIComponent(selectionItems)}` },
                { id: "zip",  label: "ZIP of per-part PDFs",       url: `${apiBase}/campaigns/${campaignId}/parts-audit.zip?items=${encodeURIComponent(selectionItems)}` },
                { id: "csv",  label: "Selection table (CSV)",      url: `${apiBase}/campaigns/${campaignId}/parts-view.csv?items=${encodeURIComponent(selectionItems)}` },
                { id: "json", label: "Selection table (JSON)",     url: `${apiBase}/campaigns/${campaignId}/parts-view.json?items=${encodeURIComponent(selectionItems)}` },
              ]}
              chipStyle={{ marginLeft: 4, background: "var(--sb-pale-amethyst)", borderColor: "var(--sb-amethyst)", color: "var(--sb-amethyst)", fontWeight: 600 }}
            />
            <button
              className="filter-chip"
              onClick={clearSelection}
              title="Clear selection"
              style={{ padding: "6px 10px", fontSize: 12, color: "var(--fg-2)" }}
            >Clear</button>
          </>
        ) : null}
      </div>
      <div style={{ background: "white", border: "1px solid var(--border-1)", borderRadius: 10, overflow: "hidden" }}>
        <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
          <thead>
            <tr style={{ background: "var(--bg-1)", color: "var(--fg-2)", fontSize: 11, textTransform: "uppercase", letterSpacing: "0.04em" }}>
              <th style={{ width: 38, padding: "10px 6px 10px 14px" }}>
                <input
                  ref={headerCheckRef}
                  type="checkbox"
                  checked={allFilteredSelected}
                  onChange={toggleHeader}
                  disabled={filteredKeys.length === 0}
                  title={allFilteredSelected ? "Deselect all (visible rows)" : "Select all (visible rows)"}
                  style={{ cursor: filteredKeys.length === 0 ? "not-allowed" : "pointer", margin: 0 }}
                  onClick={(e) => e.stopPropagation()}
                />
              </th>
              <th style={{ textAlign: "left", padding: "10px 12px", fontWeight: 600 }}>Supplier</th>
              <th style={{ textAlign: "left", padding: "10px 12px", fontWeight: 600 }}>Part MPN</th>
              <th style={{ textAlign: "left", padding: "10px 12px", fontWeight: 600 }}>Part name</th>
              <th style={{ textAlign: "left", padding: "10px 12px", fontWeight: 600 }}>Status</th>
              <th style={{ textAlign: "left", padding: "10px 12px", fontWeight: 600 }}>Fields</th>
              <th style={{ textAlign: "right", padding: "10px 12px", fontWeight: 600 }}>Audit packet</th>
            </tr>
          </thead>
          <tbody>
            {filtered.length === 0 ? (
              <tr>
                <td colSpan={7} style={{ padding: 18, color: "var(--fg-2)", fontStyle: "italic" }}>
                  {parts.length === 0
                    ? "No parts in scope on this campaign yet."
                    : q.trim() ? <>No parts match <code>{q}</code>.</> : "No parts in this filter."}
                </td>
              </tr>
            ) : null}
            {filtered.map((p, i) => {
              const k = rowKey(p);
              const isSel = selected.has(k);
              return (
              <tr
                key={k}
                onClick={() => openRecipient(p)}
                style={{
                  borderTop: i === 0 ? "none" : "1px solid var(--border-1)",
                  cursor: "pointer",
                  background: isSel ? "var(--sb-pale-amethyst)" : undefined,
                }}
              >
                <td style={{ width: 38, padding: "10px 6px 10px 14px" }} onClick={(e) => e.stopPropagation()}>
                  <input
                    type="checkbox"
                    checked={isSel}
                    onChange={() => toggleRow(p)}
                    title={isSel ? "Deselect this part" : "Select this part"}
                    style={{ cursor: "pointer", margin: 0 }}
                  />
                </td>
                <td style={{ padding: "10px 12px" }}>
                  <div style={{ fontWeight: 600 }}>{p.recipient_name}</div>
                  <div style={{ fontSize: 11, color: "var(--fg-2)" }}>{p.recipient_email}</div>
                </td>
                <td style={{ padding: "10px 12px", fontFamily: "ui-monospace, monospace", fontSize: 12 }}>{p.mpn}</td>
                <td style={{ padding: "10px 12px", color: "var(--fg-2)" }}>{p.name || "—"}</td>
                <td style={{ padding: "10px 12px" }}>
                  <span className={cx(
                    "agent-status",
                    p.status === "completed" ? "completed"
                      : p.status === "in_progress" ? "reply_received"
                      : "queued",
                  )}>
                    {p.status.replace(/_/g, " ")}
                  </span>
                </td>
                <td style={{ padding: "10px 12px", color: "var(--fg-2)" }}>
                  <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
                    <span className="fill-bar" style={{ width: 60 }}>
                      <span style={{ width: p.total_required ? `${(p.filled / p.total_required) * 100}%` : 0 }} />
                    </span>
                    <span style={{ whiteSpace: "nowrap" }}>{p.filled}/{p.total_required}</span>
                  </span>
                </td>
                <td style={{ padding: "10px 12px", textAlign: "right" }} onClick={(e) => e.stopPropagation()}>
                  <DownloadMenu
                    label="↓ Download"
                    title={`Download options for ${p.mpn}`}
                    options={partDownloadOptions({ apiBase, recipientId: p.recipient_id, mpn: p.mpn })}
                    chipStyle={{ padding: "6px 12px" }}
                  />
                </td>
              </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// Reusable "↓ Download ▾" dropdown menu. Caller supplies the trigger
// label, options ({id, label, url}), the chip style overrides, and a
// disabled flag. Used in 3 spots:
//   - PartsTable filter bar — campaign-wide bulk download (4 options)
//   - RecipientValuesPanel header — recipient-scoped (4 options)
//   - PartsTable per-row + PartValuesAccordion — per-part (3 options)
// Click-outside closes; ESC closes; menu anchors to the right edge so
// it doesn't fly past narrow table columns. The menu is position:fixed
// (anchored from the trigger's rect) so overflow-clipping ancestors —
// e.g. the rounded PartsTable wrapper — can't cut it off; scrolling
// closes it so it never floats detached from its trigger.
function DownloadMenu({ label = "↓ Download", title, options, disabled, chipStyle, anchorRight = true }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState(null);
  const ref = useRef(null);

  useEffect(() => {
    if (!open) return;
    function onDoc(e) {
      if (ref.current && !ref.current.contains(e.target)) setOpen(false);
    }
    function onKey(e) { if (e.key === "Escape") setOpen(false); }
    function onScroll() { setOpen(false); }
    document.addEventListener("mousedown", onDoc);
    document.addEventListener("keydown", onKey);
    window.addEventListener("scroll", onScroll, true);
    return () => {
      document.removeEventListener("mousedown", onDoc);
      document.removeEventListener("keydown", onKey);
      window.removeEventListener("scroll", onScroll, true);
    };
  }, [open]);

  function toggle(e) {
    e.stopPropagation();
    const r = e.currentTarget.getBoundingClientRect();
    setPos({ top: r.bottom + 6, left: r.left, right: window.innerWidth - r.right });
    setOpen((o) => !o);
  }

  async function pick(e, opt) {
    e.stopPropagation();
    setOpen(false);
    await _authedDownload(opt.url, opt.label);
  }

  return (
    <div ref={ref} style={{ position: "relative", display: "inline-block" }} onClick={(e) => e.stopPropagation()}>
      <button
        className="filter-chip"
        onClick={toggle}
        disabled={!!disabled}
        title={title || "Download options"}
        style={{ opacity: disabled ? 0.55 : 1, cursor: disabled ? "not-allowed" : "pointer", ...(chipStyle || {}) }}
      >
        {label} <span style={{ marginLeft: 6, fontSize: 10, opacity: 0.7 }}>▾</span>
      </button>
      {open && pos ? (
        <div
          role="menu"
          style={{
            position: "fixed", top: pos.top,
            ...(anchorRight ? { right: pos.right } : { left: pos.left }),
            background: "white", border: "1px solid var(--border-1)",
            borderRadius: 10, boxShadow: "0 6px 24px rgba(20,18,40,.10)",
            minWidth: 280, zIndex: 50, padding: 6,
          }}
        >
          {options.map((opt) => (
            <button
              key={opt.id}
              role="menuitem"
              onClick={(e) => pick(e, opt)}
              style={{
                display: "block", width: "100%", textAlign: "left",
                padding: "9px 12px", border: 0, background: "transparent",
                borderRadius: 7, fontSize: 13, fontFamily: "inherit",
                cursor: "pointer", color: "var(--fg-0)", whiteSpace: "nowrap",
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-1)"; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
            >
              {opt.label}
            </button>
          ))}
        </div>
      ) : null}
    </div>
  );
}

// Recipient-scoped options — used by the EXTRACTED VALUES header.
// `partsCount` decides the label ("PDF for this part" when there's
// only one part vs "Combined PDF (all parts for this supplier)").
function recipientDownloadOptions({ apiBase, recipientId, partsCount, singlePartMpn }) {
  if (partsCount === 1 && singlePartMpn) {
    return [
      { id: "pdf",  label: "Audit packet (PDF)", url: `${apiBase}/recipients/${recipientId}/parts/${encodeURIComponent(singlePartMpn)}/audit-package.pdf` },
      { id: "zip",  label: "ZIP archive",         url: `${apiBase}/recipients/${recipientId}/audit-package.zip` },
      { id: "csv",  label: "Values (CSV)",        url: `${apiBase}/recipients/${recipientId}/values.csv` },
      { id: "json", label: "Values (JSON)",       url: `${apiBase}/recipients/${recipientId}/values.json` },
    ];
  }
  return [
    { id: "pdf",  label: "Combined PDF (all parts for this supplier)", url: `${apiBase}/recipients/${recipientId}/audit-package.pdf` },
    { id: "zip",  label: "ZIP of per-part PDFs",                        url: `${apiBase}/recipients/${recipientId}/audit-package.zip` },
    { id: "csv",  label: "Values (CSV)",                                 url: `${apiBase}/recipients/${recipientId}/values.csv` },
    { id: "json", label: "Values (JSON)",                                url: `${apiBase}/recipients/${recipientId}/values.json` },
  ];
}

// Per-part options — used by PartsTable rows and PartValuesAccordion.
// No ZIP at this scope (single file).
function partDownloadOptions({ apiBase, recipientId, mpn }) {
  const enc = encodeURIComponent(mpn);
  return [
    { id: "pdf",  label: "Audit packet (PDF)", url: `${apiBase}/recipients/${recipientId}/parts/${enc}/audit-package.pdf` },
    { id: "csv",  label: "Values (CSV)",        url: `${apiBase}/recipients/${recipientId}/parts/${enc}/values.csv` },
    { id: "json", label: "Values (JSON)",       url: `${apiBase}/recipients/${recipientId}/parts/${enc}/values.json` },
  ];
}


function PartValuesAccordion({ part, apiBase, recipientId, showDownload = true }) {
  // Auto-open when the URL hash names this part (set by PartsTable when
  // the user clicks a row → land on the recipient screen with the right
  // accordion already expanded).
  const _hashMpn = (() => {
    const h = (typeof window !== "undefined" && window.location.hash) || "";
    const m = h.match(/part=([^&]+)/);
    return m ? decodeURIComponent(m[1]) : null;
  })();
  const [open, setOpen] = useState(_hashMpn != null && _hashMpn === part.mpn);
  const filled = part.filled || 0;
  const total = part.total_required || 0;
  const allDone = total > 0 && filled === total;
  return (
    <div style={{
      border: "1px solid var(--border-1)", borderRadius: 8,
      background: "white", marginBottom: 8,
    }}>
      <div
        onClick={() => setOpen((o) => !o)}
        style={{
          display: "flex", alignItems: "center", justifyContent: "space-between",
          padding: "10px 12px", cursor: "pointer",
          background: allDone ? "#e7f6ee" : "transparent",
        }}
      >
        <div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
          <span style={{ fontSize: 12, color: "var(--fg-3)", width: 12 }}>{open ? "▾" : "▸"}</span>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontWeight: 600, fontSize: 13, color: "var(--fg-0)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
              {part.mpn}
            </div>
            {part.name ? (
              <div style={{ fontSize: 11, color: "var(--fg-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                {part.name}
              </div>
            ) : null}
          </div>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }} onClick={(e) => e.stopPropagation()}>
          {showDownload && apiBase && recipientId ? (
            <DownloadMenu
              label="↓ Download"
              title={`Download options for ${part.mpn}`}
              options={partDownloadOptions({ apiBase, recipientId, mpn: part.mpn })}
              chipStyle={{ padding: "4px 10px", fontSize: 11.5 }}
            />
          ) : null}
          <span
            className="agent-pill"
            title={`${filled} of ${total} required fields filled`}
            style={{ background: allDone ? "#2bbb7a" : undefined, color: allDone ? "white" : undefined }}
          >
            {filled}/{total}
          </span>
        </div>
      </div>
      {open ? (
        <div style={{ padding: "4px 12px 10px", borderTop: "1px solid var(--border-1)" }}>
          {(part.fields || []).length === 0 ? (
            <div style={{ color: "var(--fg-2)", fontStyle: "italic", fontSize: 12, padding: "8px 0" }}>(no fields)</div>
          ) : null}
          {(part.fields || []).map((f) => (
            <div key={f.name} className="field-line">
              <div>
                <div className="l">{f.label}{f.required ? <span style={{ color: "var(--warm)" }}> *</span> : null}</div>
                <div style={{ fontSize: 11, color: "var(--fg-3)", fontFamily: "ui-monospace, monospace" }}>{f.name}</div>
              </div>
              {f.extracted_value
                ? <div><span className="v">{f.extracted_value}</span></div>
                : f.required
                  ? <div><span className="v missing">missing</span></div>
                  : <div><span style={{ color: "var(--fg-3)", fontSize: 12 }}>—</span></div>}
            </div>
          ))}
          {(part.fields || []).some((f) => f.required) ? (
            <div style={{ fontSize: 10.5, color: "var(--fg-3)", paddingTop: 6 }}>* required</div>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

function RecipientActionsPanel({ apiBase, recipientId, onChange }) {
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState("");
  async function addAttestation() {
    if (busy) return;
    setBusy(true); setMsg("");
    try {
      const r = await fetch(`${apiBase}/recipients/${recipientId}/draft/add-attestation`, { method: "POST" });
      const data = await r.json();
      if (!r.ok) {
        setMsg(data?.detail || "Couldn't add attestation");
      } else {
        setMsg("Attestation link added to draft.");
        if (onChange) onChange();
      }
    } catch (e) {
      setMsg(`Network error: ${e.message}`);
    } finally {
      setBusy(false);
      setTimeout(() => setMsg(""), 4000);
    }
  }
  return (
    <div className="mock-panel">
      <header><h3>Quick actions</h3></header>
      <div className="mock-panel-body" style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        <button className="back-btn" style={{ textAlign: "left" }} disabled={busy} onClick={addAttestation}>
          {busy ? "Adding…" : "Add Attestation to draft"}
        </button>
        {msg ? <div style={{ fontSize: 12, color: "var(--fg-2)" }}>{msg}</div> : null}
        <button className="back-btn" style={{ textAlign: "left" }} onClick={() => alert("Use the chat: 'pause this recipient'")}>Pause this recipient</button>
        <button className="back-btn" style={{ textAlign: "left" }} onClick={() => alert("Use the chat: 'mark completed'")}>Mark completed</button>
        <button className="back-btn" style={{ textAlign: "left" }} onClick={() => alert("Use the chat: 'send a custom note saying ...'")}>Send custom note</button>
        <div style={{ fontSize: 11, color: "var(--fg-2)", marginTop: 4 }}>
          (Most actions live in the chat — type a request and the agent picks the right tool.)
        </div>
      </div>
    </div>
  );
}

function App() {
  const live = useLiveBackend();
  // Default screen = the new collections list. Drafts → setup chat,
  // live campaigns → tile view. The old wizard / dash / thread / audit
  // screens are unrouted (left in code for future revival).
  //
  // Nav state (`screen` / `activeCampaignId` / `activeRecipientId`) is
  // mirrored to the URL — `?screen=...&campaign=<uuid>&recipient=<uuid>`
  // — so a hard-reload keeps the user where they were. The screen=list
  // default is encoded as the *absence* of `?screen=` (clean URL).
  // Sub-screens that require an id (collection / recipient) fall back
  // to "list" if that id is missing on initial mount.
  const _NAV_VALID_SCREENS = ["list", "inbox", "sent", "contacts", "agent", "collection", "recipient"];
  const _initialNav = useMemo(() => {
    const params = new URLSearchParams(window.location.search);
    const fromUrl = params.get("screen");
    const campaign = params.get("campaign");
    const recipient = params.get("recipient");
    let s = (fromUrl && _NAV_VALID_SCREENS.includes(fromUrl)) ? fromUrl : "list";
    if (s === "collection" && !campaign) s = "list";
    if (s === "recipient" && !(campaign && recipient)) s = "list";
    return { screen: s, campaign, recipient };
  }, []);

  const [screen, _setScreenRaw] = useState(_initialNav.screen);
  const setScreen = useCallback((s) => {
    _setScreenRaw(s);
    _setUrlParam("screen", s === "list" ? null : s);
  }, []);
  const [activeCampaignId, _setActiveCampaignIdRaw] = useState(_initialNav.campaign);
  const setActiveCampaignId = useCallback((id) => {
    _setActiveCampaignIdRaw(id);
    _setUrlParam("campaign", id);
  }, []);
  const [activeRecipientId, _setActiveRecipientIdRaw] = useState(_initialNav.recipient);
  const setActiveRecipientId = useCallback((id) => {
    _setActiveRecipientIdRaw(id);
    _setUrlParam("recipient", id);
  }, []);

  // Browser back/forward: react to `popstate` so the buttons feel native.
  useEffect(() => {
    const onPop = () => {
      const params = new URLSearchParams(window.location.search);
      const fromUrl = params.get("screen");
      const next = (fromUrl && _NAV_VALID_SCREENS.includes(fromUrl)) ? fromUrl : "list";
      _setScreenRaw(next);
      _setActiveCampaignIdRaw(params.get("campaign") || null);
      _setActiveRecipientIdRaw(params.get("recipient") || null);
    };
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);
  const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
    try { return localStorage.getItem("sb-side-collapsed") === "1"; } catch { return false; }
  });
  useEffect(() => {
    try { localStorage.setItem("sb-side-collapsed", sidebarCollapsed ? "1" : "0"); } catch {}
  }, [sidebarCollapsed]);
  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // Pending seed message handed off from the Inbox → setup agent.
  // Consumed by ScreenAgent on its first ready turn.
  const [pendingSeedMessage, setPendingSeedMessage] = useState(null);

  // Sidebar inbox-count badge — refreshed every 15s so newly-arrived
  // forwarded threads show up without a manual reload.
  const [inboxCount, setInboxCount] = useState(0);
  useEffect(() => {
    let stop = false;
    const tick = async () => {
      try {
        const r = await fetch(`${live.apiBase}/intake/counts`);
        if (r.ok && !stop) {
          const c = await r.json();
          setInboxCount(c.pending || 0);
        }
      } catch {}
    };
    tick();
    const t = setInterval(tick, 15000);
    return () => { stop = true; clearInterval(t); };
  }, [live.apiBase]);

  // Customer brand — surfaced in the sidebar account chip. Fetched
  // once on mount + refetched whenever the user saves an edit via
  // the chip-popover editor (see Sidebar). Defaults seeded so the
  // chip renders during the initial fetch.
  const [branding, setBranding] = useState({
    name: "DigiKey", name_long: "DigiKey Electronics", initials: "DE", color: "#CC0000",
  });
  const refreshBranding = useCallback(async () => {
    try {
      const r = await fetch(`${live.apiBase}/branding`);
      if (r.ok) {
        const b = await r.json();
        if (b) setBranding(b);
      }
    } catch {}
  }, [live.apiBase]);
  useEffect(() => { refreshBranding(); }, [refreshBranding]);

  return (
    <div className={cx("app", sidebarCollapsed && "collapsed")}>
      <Sidebar screen={screen} setScreen={setScreen} collapsed={sidebarCollapsed} setCollapsed={setSidebarCollapsed} inboxCount={inboxCount} branding={branding} apiBase={live.apiBase} onBrandingChanged={refreshBranding} />
      <main className={cx("canvas", (screen === "agent" || screen === "collection" || screen === "recipient" || screen === "list" || screen === "inbox" || screen === "sent" || screen === "contacts") && "canvas-agent")} data-screen-label={`screen: ${screen}`}>
        {(screen === "list" || screen === "inbox" || screen === "sent" || screen === "contacts") && (
          <ScreenWorkspace
            apiBase={live.apiBase}
            live={live}
            screen={screen}
            setScreen={setScreen}
            onPickDraft={(c) => {
              // Drafts open the existing setup chat; reuse the live
              // hook's wizardCampaignId so the URL ?campaign= survives.
              if (live.setWizardCampaignId) live.setWizardCampaignId(c.id);
              setActiveCampaignId(c.id);
              setScreen("agent");
            }}
            onPickLive={(c) => {
              setActiveCampaignId(c.id);
              setScreen("collection");
            }}
            onNew={() => {
              if (live.setWizardCampaignId) live.setWizardCampaignId(null);
              setActiveCampaignId(null);
              setScreen("agent");
            }}
            onAttachedToCampaign={(cid) => {
              setActiveCampaignId(cid);
              setScreen("collection");
            }}
            onConvertToNewCampaign={(seed) => {
              if (live.setWizardCampaignId) live.setWizardCampaignId(null);
              setActiveCampaignId(null);
              setPendingSeedMessage(seed);
              setScreen("agent");
            }}
            onOpenRecipient={(rid) => {
              setActiveRecipientId(rid);
              setScreen("recipient");
            }}
          />
        )}
        {screen === "collection" && activeCampaignId && (
          <ScreenCollection
            apiBase={live.apiBase}
            campaignId={activeCampaignId}
            onBack={() => { setActiveCampaignId(null); setScreen("list"); }}
            onPickRecipient={(r) => { setActiveRecipientId(r.id); setScreen("recipient"); }}
          />
        )}
        {screen === "recipient" && activeRecipientId && (
          <ScreenRecipient
            apiBase={live.apiBase}
            recipientId={activeRecipientId}
            onBackList={() => { setActiveRecipientId(null); setActiveCampaignId(null); setScreen("list"); }}
            onBackCollection={() => { setActiveRecipientId(null); setScreen("collection"); }}
          />
        )}
        {screen === "agent" && (
          <ScreenAgent
            live={live}
            seedMessage={pendingSeedMessage}
            onSeedConsumed={() => setPendingSeedMessage(null)}
            onLaunched={(cid) => {
              setActiveCampaignId(cid);
              setScreen("collection");
            }}
          />
        )}
      </main>

      <TweaksPanel title="Tweaks">
        <TweakSection title="Schema editor variant">
          <TweakSelect
            label="Editor"
            value={tweaks.schemaVariant}
            options={["Notion-list", "Spreadsheet", "Card-form"]}
            onChange={(v) => setTweak("schemaVariant", v)}
          />
          <div className="meta" style={{ marginTop: 6 }}>Open the Schema step to compare.</div>
        </TweakSection>

        <TweakSection title="Annotations">
          <TweakToggle
            label="Show notes & callouts"
            value={tweaks.showAnnotations}
            onChange={(v) => setTweak("showAnnotations", v)}
          />
        </TweakSection>
      </TweaksPanel>
    </div>
  );
}

// =========================================================================
// Boot-time version handshake — blocks the app on backend mismatch.
// =========================================================================
// On mount, fetches GET /version from the backend. If the backend's
// APP_VERSION differs from EXPECTED_VERSION (or the backend is
// unreachable), an overlay tells the user what to do and the app
// itself does not render. Re-checks every 5s while in error state so
// the overlay clears automatically once the backend catches up.
function useVersionCheck() {
  const params = new URLSearchParams(window.location.search);
  const apiBase = params.get("api") || _DEFAULT_API_ORIGIN;
  // status: "checking" | "ok" | "mismatch" | "unreachable"
  const [state, setState] = useState({ status: "checking", backendVersion: null });

  useEffect(() => {
    let stop = false;
    const tick = async () => {
      try {
        const r = await fetch(`${apiBase}/version`, { cache: "no-store" });
        if (!r.ok) throw new Error(`http ${r.status}`);
        const data = await r.json();
        if (stop) return;
        if (data.version === EXPECTED_VERSION) {
          setState({ status: "ok", backendVersion: data.version });
        } else {
          setState({ status: "mismatch", backendVersion: data.version });
        }
      } catch {
        if (!stop) setState({ status: "unreachable", backendVersion: null });
      }
    };
    tick();
    // Re-poll every 5s so the overlay clears as soon as the dev
    // restarts the backend — no manual reload needed.
    const t = setInterval(tick, 5000);
    return () => { stop = true; clearInterval(t); };
  }, [apiBase]);

  return { ...state, apiBase };
}

function VersionGate({ children }) {
  const v = useVersionCheck();

  if (v.status === "ok") return children;

  // While the very first check is pending, render nothing — avoids a
  // flash-of-app for the common case where the backend is up.
  if (v.status === "checking") {
    return (
      <div style={_GATE_STYLE.overlay}>
        <div style={_GATE_STYLE.box}>
          <div style={_GATE_STYLE.spinner}>Checking backend…</div>
        </div>
      </div>
    );
  }

  const isMismatch = v.status === "mismatch";
  return (
    <div style={_GATE_STYLE.overlay}>
      <div style={_GATE_STYLE.box}>
        <div style={_GATE_STYLE.title}>
          {isMismatch ? "Backend out of date" : "Backend unreachable"}
        </div>
        <div style={_GATE_STYLE.body}>
          {isMismatch ? (
            <>
              The frontend expects <code style={_GATE_STYLE.code}>{EXPECTED_VERSION}</code> but the backend
              is running <code style={_GATE_STYLE.code}>{v.backendVersion}</code>.
              <br /><br />
              Pull <code style={_GATE_STYLE.code}>main</code>, then run from <code style={_GATE_STYLE.code}>backend/</code>:
              <pre style={_GATE_STYLE.pre}>
                make db-migrate{"\n"}
                # restart the running `make run`{"\n"}
                make run
              </pre>
              This page will continue automatically once the backend catches up.
            </>
          ) : (
            <>
              Couldn’t reach <code style={_GATE_STYLE.code}>{v.apiBase}</code>.
              <br /><br />
              Start the backend from <code style={_GATE_STYLE.code}>backend/</code>:
              <pre style={_GATE_STYLE.pre}>make run</pre>
              This page will continue automatically once the backend is up.
            </>
          )}
        </div>
        <div style={_GATE_STYLE.foot}>Re-checking every 5s…</div>
      </div>
    </div>
  );
}

const _GATE_STYLE = {
  overlay: {
    position: "fixed", inset: 0, zIndex: 99999,
    background: "rgba(20, 16, 40, 0.94)",
    display: "flex", alignItems: "center", justifyContent: "center",
    fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
  },
  box: {
    background: "white", borderRadius: 12, maxWidth: 560, width: "calc(100% - 32px)",
    padding: "28px 32px", boxShadow: "0 24px 80px rgba(0,0,0,0.4)",
  },
  title: { fontSize: 20, fontWeight: 700, marginBottom: 12, color: "#1a1233" },
  body: { fontSize: 14, lineHeight: 1.55, color: "#382a5c" },
  foot: { fontSize: 12, color: "#7a6ea8", marginTop: 16 },
  spinner: { fontSize: 14, color: "#7a6ea8" },
  code: { background: "#f3f0fa", padding: "1px 6px", borderRadius: 4, fontSize: 13, fontFamily: "ui-monospace, monospace" },
  pre: { background: "#1a1233", color: "#dcd0ff", padding: "10px 12px", borderRadius: 6, fontSize: 12.5, marginTop: 8, overflowX: "auto" },
  input: {
    display: "block", width: "100%", boxSizing: "border-box", marginTop: 12,
    padding: "10px 12px", fontSize: 14, borderRadius: 8, border: "1px solid #d8d0ee",
    fontFamily: "inherit",
  },
  err: { color: "#c0392b", fontSize: 13, marginTop: 12 },
  btn: {
    marginTop: 16, width: "100%", padding: "10px 12px", fontSize: 14, fontWeight: 600,
    color: "white", background: "#5b3df5", border: "none", borderRadius: 8, cursor: "pointer",
  },
  linkBtn: {
    marginTop: 12, width: "100%", padding: 4, fontSize: 13, fontWeight: 500,
    color: "#5b3df5", background: "none", border: "none", cursor: "pointer",
  },
};

// =========================================================================
// Auth gate — HTTP Basic login screen.
// =========================================================================
// Sits inside VersionGate (so we only probe once the backend is known
// reachable). On boot it GETs /auth/check with whatever credentials are in
// sessionStorage; 200 means we're in (or auth is disabled server-side),
// 401 shows the login form. Submitting stores base64("user:pass") and
// re-probes — the fetch wrapper attaches it from there on.
function AuthGate({ children }) {
  const apiBase = _API_ORIGIN;
  const [status, setStatus] = useState("checking"); // checking | ok | login
  const [mode, setMode] = useState("login"); // login | forgot | forgot-sent
  const [user, setUser] = useState("");
  const [pass, setPass] = useState("");
  const [forgotEmail, setForgotEmail] = useState("");
  const [error, setError] = useState(null);
  const [submitting, setSubmitting] = useState(false);

  const probe = useCallback(async () => {
    const r = await fetch(`${apiBase}/auth/check`, { cache: "no-store" });
    return r.ok;
  }, [apiBase]);

  useEffect(() => {
    let stop = false;
    probe()
      .then((ok) => { if (!stop) setStatus(ok ? "ok" : "login"); })
      // VersionGate already proved the backend reachable, so a throw here is
      // almost certainly the 401's missing-CORS edge — treat as login.
      .catch(() => { if (!stop) setStatus("login"); });
    return () => { stop = true; };
  }, [probe]);

  const onSubmit = useCallback(async (e) => {
    e.preventDefault();
    setSubmitting(true);
    setError(null);
    sessionStorage.setItem(AUTH_STORAGE_KEY, btoa(`${user}:${pass}`));
    let ok = false;
    try { ok = await probe(); } catch { ok = false; }
    if (ok) {
      setStatus("ok");
    } else {
      sessionStorage.removeItem(AUTH_STORAGE_KEY);
      setError("Incorrect email or password.");
    }
    setSubmitting(false);
  }, [user, pass, probe]);

  const onForgot = useCallback(async (e) => {
    e.preventDefault();
    setSubmitting(true);
    setError(null);
    try {
      await fetch(`${apiBase}/auth/forgot`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: forgotEmail }),
      });
    } catch {
      // The endpoint always returns 200 by design; a network blip shouldn't
      // reveal anything either. Show the same confirmation regardless.
    }
    setMode("forgot-sent");
    setSubmitting(false);
  }, [apiBase, forgotEmail]);

  if (status === "ok") return children;
  if (status === "checking") {
    return (
      <div style={_GATE_STYLE.overlay}>
        <div style={_GATE_STYLE.box}>
          <div style={_GATE_STYLE.spinner}>Checking access…</div>
        </div>
      </div>
    );
  }

  if (mode === "forgot-sent") {
    return (
      <div style={_GATE_STYLE.overlay}>
        <div style={_GATE_STYLE.box}>
          <div style={_GATE_STYLE.title}>Check your email</div>
          <div style={_GATE_STYLE.body}>
            If an account exists for that address, we've sent a link to reset
            your password. It's valid for one hour.
            <button
              type="button"
              style={_GATE_STYLE.btn}
              onClick={() => { setMode("login"); setError(null); }}
            >
              Back to sign in
            </button>
          </div>
        </div>
      </div>
    );
  }

  if (mode === "forgot") {
    return (
      <div style={_GATE_STYLE.overlay}>
        <form style={_GATE_STYLE.box} onSubmit={onForgot}>
          <div style={_GATE_STYLE.title}>Reset password</div>
          <div style={_GATE_STYLE.body}>
            Enter your email and we'll send you a link to set a new password.
            <input
              type="email" placeholder="Email" autoFocus value={forgotEmail}
              autoComplete="username" onChange={(e) => setForgotEmail(e.target.value)} style={_GATE_STYLE.input}
            />
            <button type="submit" disabled={submitting || !forgotEmail} style={_GATE_STYLE.btn}>
              {submitting ? "Sending…" : "Send reset link"}
            </button>
            <button
              type="button"
              style={_GATE_STYLE.linkBtn}
              onClick={() => { setMode("login"); setError(null); }}
            >
              Back to sign in
            </button>
          </div>
        </form>
      </div>
    );
  }

  return (
    <div style={_GATE_STYLE.overlay}>
      <form style={_GATE_STYLE.box} onSubmit={onSubmit}>
        <div style={_GATE_STYLE.title}>Sign in</div>
        <div style={_GATE_STYLE.body}>
          Enter the credentials for the Tariff Agent.
          <input
            type="email" placeholder="Email" autoFocus value={user}
            autoComplete="username" onChange={(e) => setUser(e.target.value)} style={_GATE_STYLE.input}
          />
          <input
            type="password" placeholder="Password" value={pass} autoComplete="current-password"
            onChange={(e) => setPass(e.target.value)} style={_GATE_STYLE.input}
          />
          {error && <div style={_GATE_STYLE.err}>{error}</div>}
          <button type="submit" disabled={submitting} style={_GATE_STYLE.btn}>
            {submitting ? "Signing in…" : "Sign in"}
          </button>
          <button
            type="button"
            style={_GATE_STYLE.linkBtn}
            onClick={() => { setMode("forgot"); setError(null); setForgotEmail(user); }}
          >
            Forgot password?
          </button>
        </div>
      </form>
    </div>
  );
}

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