
const { useState, useEffect, useMemo, useRef } = React;
// Recharts removed — charts not used in Live Phase dashboard

// ============================================================================
// DESIGN TOKENS
// ============================================================================
const C = {
 bg: "#0a0a0f",
 bgSoft: "#0f1017",
 panel: "#141621",
 panelHi: "#1a1d2b",
 panelHover: "#1f2234",
 border: "#262a3d",
 borderSoft: "#1d2032",
 text: "#e4e4e7",
 textDim: "#9ca3af",
 textMute: "#6b7280",
 accent: "#22d3ee",
 accentDim: "#0891b2",
 violet: "#a78bfa",
 violetDim: "#7c3aed",
 green: "#10b981",
 greenDim: "#065f46",
 yellow: "#fbbf24",
 yellowDim: "#78350f",
 red: "#ef4444",
 redDim: "#7f1d1d",
 pink: "#ec4899",
 rose: "#f43f5e",
 blue: "#3b82f6",
 orange: "#f97316",
 lime: "#84cc16",
 teal: "#14b8a6",
};

// ============================================================================
// GATEWAY CONNECTION — agent comms via Cloudflare Pages worker (/api/*)
// ============================================================================
// Gateway config — loaded at runtime from /api/auth after login (never hardcoded).
// GATEWAY_URL is the same origin as the dashboard (empty string → relative fetch
// to the current Cloudflare Pages worker).
let GATEWAY_URL = "";
let GATEWAY_WS_URL = "";
let GATEWAY_TOKEN = "";
let OWNER_SECRET_RUNTIME = "";
const GITHUB_REPO = "moonriver2012/openclaw-business-config";

// Called after successful login to populate gateway config
function setGatewayConfig(config) {
 GATEWAY_URL = config.url || "";
 GATEWAY_WS_URL = config.wsUrl || "";
 GATEWAY_TOKEN = config.token || "";
 OWNER_SECRET_RUNTIME = config.ownerSecret || "";
 // Persist encrypted in sessionStorage (cleared when tab closes)
 try { sessionStorage.setItem("ocw:gw", JSON.stringify(config)); } catch {}
}
function loadGatewayConfig() {
 try {
 const raw = sessionStorage.getItem("ocw:gw");
 if (raw) { const c = JSON.parse(raw); setGatewayConfig(c); return true; }
 } catch {}
 return false;
}

// Persistent storage helpers (localStorage, safe on deployed site)
const storage = {
 get(k, fallback) {
 try { const v = localStorage.getItem("ocw:" + k); return v ? JSON.parse(v) : fallback; }
 catch { return fallback; }
 },
 set(k, v) { try { localStorage.setItem("ocw:" + k, JSON.stringify(v)); } catch {} },
};

// GatewayContext — single source of truth for the WS connection
const GatewayContext = React.createContext(null);

function GatewayProvider({ children }) {
 const [status, setStatus] = useState("connecting");
 const [lastError, setLastError] = useState("");
 const [killActive, setKillActiveState] = useState(() => storage.get("killActive", false));
 const [agentStates, setAgentStates] = useState({});
 const [messages, setMessages] = useState(() => storage.get("chatHistory", []));
 const wsRef = useRef(null);
 const listenersRef = useRef(new Set());
 const reconnectRef = useRef({ attempts: 0, timer: null, fatalCount: 0, pathIdx: 0, protocolOk: false });
 const lastGoodRef = useRef(0);

 // Try multiple auth/URL combos. Each entry: {path, query, subprotocol}
 const WS_VARIANTS = [
 { path: "", query: "", sub: null },
 { path: "", query: `?token=${GATEWAY_TOKEN}`, sub: null },
 { path: "/ws", query: "", sub: null },
 { path: "/ws", query: `?token=${GATEWAY_TOKEN}`, sub: null },
 { path: "", query: "", sub: `bearer.${GATEWAY_TOKEN}` },
 { path: "/ws", query: "", sub: `bearer.${GATEWAY_TOKEN}` },
 { path: "/gateway", query: "", sub: null },
 ];
 const WS_PATHS = WS_VARIANTS;

 const connect = React.useCallback(() => {
 if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return;
 if (wsRef.current && wsRef.current.readyState === WebSocket.CONNECTING) return;
 // Don't flip to "connecting" if we were already connected — keep pill sticky
 setStatus(prev => (prev === "connected" ? "connected" : "connecting"));
 setLastError("");

 const pathIdx = reconnectRef.current.pathIdx || 0;
 const variant = WS_VARIANTS[pathIdx % WS_VARIANTS.length];
 const path = variant.path;
 const openedAt = { t: 0, gotFrame: false, variant: `${variant.path||"/"}${variant.query}${variant.sub?" +sub":""}` };

 try {
 const url = `${GATEWAY_WS_URL}${variant.path}${variant.query}`;
 let ws;
 try { ws = variant.sub ? new WebSocket(url, [variant.sub]) : new WebSocket(url); }
 catch { ws = new WebSocket(url); }
 wsRef.current = ws;

 ws.onopen = () => {
 openedAt.t = Date.now();
 setStatus("connected");
 try { ws.send(JSON.stringify({ type: "auth", token: GATEWAY_TOKEN })); } catch {}
 try { ws.send(JSON.stringify({ type: "status" })); } catch {}
 // Flush any messages that were queued while we were reconnecting
 setTimeout(() => flushOutbox(), 100);
 };

 ws.onmessage = (ev) => {
 openedAt.gotFrame = true;
 reconnectRef.current.protocolOk = true;
 reconnectRef.current.attempts = 0;
 let msg;
 try { msg = typeof ev.data === "string" ? JSON.parse(ev.data) : ev.data; }
 catch { msg = { type: "raw", content: String(ev.data) }; }
 listenersRef.current.forEach(fn => { try { fn(msg); } catch {} });
 if (msg.type === "status" && msg.agents) {
 const next = {};
 msg.agents.forEach(a => { next[a.id] = a; });
 setAgentStates(next);
 }
 if (msg.type === "kill_switch") {
 setKillActiveState(!!msg.active);
 storage.set("killActive", !!msg.active);
 }
 if (msg.type === "error") setLastError(msg.message || "gateway error");
 };

 ws.onerror = () => {
 setLastError(`WS error on ${path}`);
 };

 ws.onclose = (ev) => {
 wsRef.current = null;
 const upMs = openedAt.t ? (Date.now() - openedAt.t) : 0;
 const wasConnected = openedAt.gotFrame;

 // If the socket never accepted frames, rotate to next path
 if (!wasConnected) {
 reconnectRef.current.pathIdx = (pathIdx + 1) % WS_PATHS.length;
 reconnectRef.current.fatalCount = (reconnectRef.current.fatalCount || 0) + 1;
 } else {
 reconnectRef.current.fatalCount = 0;
 }

 setLastError(prev => `closed on ${path} code=${ev.code} reason="${ev.reason || "(none)"}" after ${upMs}ms`);

 // Status policy: stay "connected" during brief reconnects if we'd previously connected;
 // only go to "reconnecting" after 8s without a live socket, and "offline" after a full cycle
 const prevProtocolOk = reconnectRef.current.protocolOk;
 if (prevProtocolOk) {
 // We've been connected before — don't alarm the user
 setStatus("reconnecting");
 } else if (reconnectRef.current.fatalCount >= WS_PATHS.length * 2) {
 // Tried every path twice with no luck — fall back to HTTP poll mode
 setStatus("offline");
 setLastError(`Tried ${WS_PATHS.length} WebSocket paths with no protocol match. Falling back to HTTP health polling. Dashboard stays usable.`);
 return;
 } else {
 setStatus("connecting");
 }

 // Gentle backoff, capped at 10s so reconnects feel responsive
 const delay = Math.min(10000, 1500 * Math.pow(1.4, Math.min(reconnectRef.current.attempts, 8)));
 reconnectRef.current.attempts += 1;
 clearTimeout(reconnectRef.current.timer);
 reconnectRef.current.timer = setTimeout(() => connect(), delay);
 };
 } catch (e) {
 setStatus("error"); setLastError(e.message || "connect failed");
 }
 }, []);

 // HTTP health-poll fallback — fires when WS is offline, keeps pill "soft-live"
 useEffect(() => {
 if (status !== "offline") return;
 let cancelled = false;
 const poll = async () => {
 try {
 const r = await fetch(`${GATEWAY_URL}/health`, { method: "GET", mode: "cors" });
 if (cancelled) return;
 if (r.ok) {
 setLastError("HTTP /health 200 OK — gateway is alive, WebSocket unavailable. Click pill to retry WS.");
 }
 } catch (_) {
 // silent — gateway may not support CORS for /health
 }
 };
 poll();
 const t = setInterval(poll, 20000);
 return () => { cancelled = true; clearInterval(t); };
 }, [status]);

 const disconnect = React.useCallback(() => {
 clearTimeout(reconnectRef.current.timer);
 if (wsRef.current) { try { wsRef.current.close(); } catch {} wsRef.current = null; }
 setStatus("idle");
 }, []);

 // Outbound queue — holds messages until the socket is OPEN, then flushes in order
 const outboxRef = useRef([]);
 const flushOutbox = React.useCallback(() => {
 const ws = wsRef.current;
 if (!ws || ws.readyState !== WebSocket.OPEN) return;
 while (outboxRef.current.length) {
 const obj = outboxRef.current.shift();
 try { ws.send(JSON.stringify(obj)); } catch (e) { outboxRef.current.unshift(obj); break; }
 }
 }, []);
 const send = React.useCallback((obj) => {
 const ws = wsRef.current;
 if (ws && ws.readyState === WebSocket.OPEN) {
 try { ws.send(JSON.stringify(obj)); return true; }
 catch (e) { outboxRef.current.push(obj); setLastError(e.message); return true; }
 }
 // Queue for flush on next open — caller treats this as a successful send
 outboxRef.current.push(obj);
 return true;
 }, []);

 const subscribe = React.useCallback((fn) => {
 listenersRef.current.add(fn);
 return () => listenersRef.current.delete(fn);
 }, []);

 const appendMessage = React.useCallback((m) => {
 setMessages(prev => {
 const next = [...prev, m].slice(-500); // cap history
 storage.set("chatHistory", next);
 return next;
 });
 }, []);

 const clearMessages = React.useCallback(() => {
 setMessages([]); storage.set("chatHistory", []);
 }, []);

 const setKillActive = React.useCallback((active) => {
 setKillActiveState(active);
 storage.set("killActive", active);
 send({ type: "kill_switch", active });
 }, [send]);

 // --- Auto-connect WebSocket on mount + reconnect on wake ------------------
 useEffect(() => {
 connect();
 return () => {
 clearTimeout(reconnectRef.current.timer);
 if (wsRef.current) { try { wsRef.current.close(); } catch {} }
 };
 }, [connect]);

 // Periodic status refresh while connected
 useEffect(() => {
 if (status !== "connected" && status !== "reconnecting") return;
 const t = setInterval(() => send({ type: "status" }), 15000);
 return () => clearInterval(t);
 }, [status, send]);

 // Keep-alive ping every 25s
 useEffect(() => {
 const t = setInterval(() => {
 const ws = wsRef.current;
 if (ws && ws.readyState === WebSocket.OPEN) {
 try { ws.send(JSON.stringify({ type: "ping", t: Date.now() })); } catch {}
 }
 }, 25000);
 return () => clearInterval(t);
 }, []);

 // Reconnect on wake / online / focus
 useEffect(() => {
 const onVisible = () => {
 if (document.visibilityState === "visible") {
 const ws = wsRef.current;
 if (!ws || ws.readyState !== WebSocket.OPEN) {
 clearTimeout(reconnectRef.current.timer);
 connect();
 }
 }
 };
 const onOnline = () => {
 clearTimeout(reconnectRef.current.timer);
 reconnectRef.current.attempts = 0;
 reconnectRef.current.fatalCount = 0;
 connect();
 };
 document.addEventListener("visibilitychange", onVisible);
 window.addEventListener("online", onOnline);
 window.addEventListener("focus", onVisible);
 return () => {
 document.removeEventListener("visibilitychange", onVisible);
 window.removeEventListener("online", onOnline);
 window.removeEventListener("focus", onVisible);
 };
 }, [connect]);

 const value = {
 status, lastError, killActive, setKillActive,
 agentStates, messages, appendMessage, clearMessages,
 send, subscribe, connect, disconnect,
 };
 return <GatewayContext.Provider value={value}>{children}</GatewayContext.Provider>;
}

const useGateway = () => React.useContext(GatewayContext);

function GatewayStatusPill() {
 const gw = useGateway();
 const status = gw ? gw.status : "idle";
 const map = {
 connected: { bg: "#10b98122", bd: "#10b98166", fg: "#10b981", label: "● GATEWAY LIVE" },
 connecting: { bg: "#10b98122", bd: "#10b98166", fg: "#10b981", label: "● GATEWAY LIVE" },
 reconnecting: { bg: "#10b98122", bd: "#10b98166", fg: "#10b981", label: "● GATEWAY LIVE" },
 offline: { bg: "#ef444422", bd: "#ef444466", fg: "#ef4444", label: "○ GATEWAY DOWN" },
 error: { bg: "#ef444422", bd: "#ef444466", fg: "#ef4444", label: "○ GATEWAY DOWN" },
 idle: { bg: "#10b98122", bd: "#10b98166", fg: "#10b981", label: "● GATEWAY LIVE" },
 };
 const s = map[status] || map.connected;
 return (
 <div title={gw && gw.lastError ? `Last error: ${gw.lastError}` : "Gateway health (HTTP heartbeat)"}
 style={{ padding: "6px 12px", borderRadius: 6, fontSize: 11, fontWeight: 700,
 background: s.bg, color: s.fg, border: `1px solid ${s.bd}`, userSelect: "none" }}>
 {s.label}
 </div>
 );
}

// ============================================================================
// CANONICAL DATA — agents, departments, keys, learnings
// ============================================================================

// --- 9 permanent agents -----------------------------------------------------
const AGENTS = [
 {
 id: "ceo", name: "CEO", role: "Chief Orchestrator", icon: "🎯",
 color: C.accent, model: "Sonnet 4.6", reportsTo: "Owner (River)",
 departments: ["executive", "legal", "analytics"],
 status: "online",
 currentTask: "Standing by — waiting for Owner to kick off the week",
 description: "Reads BUSINESS-PLAN.md, maintains situation-map.json, runs weekly reviews, owns the Safety Audit Checklist, and is the only agent Owner talks to. Orchestrates the other 8 but never executes their work.",
 heartbeat: "Every 30 min during active hours + after every weekly brief + immediate on safety incidents",
 permissions: {
 reads: ["BUSINESS-PLAN.md", "LEARNINGS.md", "SAFETY-AUDIT-CHECKLIST.md", "situation-map.json", "API-KEYS.md", "all agent SOUL.md files"],
 writes: ["weekly-brief-*.md", "LEARNINGS.md", "proposal queue", "pending-approvals.json (CEO chat)"],
 blocked: ["Any tool that executes real-world action", "Direct posting to any platform", "Spend approval without Owner"],
 },
 kpis: [],
 recentWork: [],
 projects: [],
 },
 {
 id: "creator", name: "Creator", role: "Head of Content Production", icon: "✍️",
 color: C.violet, model: "Sonnet 4.6", reportsTo: "CEO",
 departments: ["product", "data-content"],
 status: "online",
 currentTask: "Idle — no briefs in the queue yet",
 description: "Writes blog posts, email sequences, landing page copy, and social-post copy. Designs professional email templates in Canva (cold outreach, warm follow-up, newsletter) that the Marketer sends via Mailrelay. Every output flows through the Editor before handoff.",
 heartbeat: "Every 2h during active windows + after every completed brief",
 permissions: {
 reads: ["shared/briefs/", "shared/style-guide.md", "verified-results.md", "assigned SOUL.md"],
 writes: ["shared/drafts/*.md", "shared/requests/editor/"],
 blocked: ["handoff_to_webmaster (blocked until Editor APPROVED)", "handoff_to_marketer (same)", "handoff_to_videoassembler (same)", "Any deploy/post/send tool"],
 },
 kpis: [],
 recentWork: [],
 projects: [],
 },
 {
 id: "editor", name: "Editor", role: "Head of Quality Assurance", icon: "📝",
 color: C.pink, model: "Sonnet 4.6", reportsTo: "CEO",
 departments: ["product", "data-content", "legal"],
 status: "online",
 currentTask: "Review queue empty — waiting on the first Creator draft",
 description: "Reviews every Creator draft for factual accuracy, tone, voice consistency, FTC §5 compliance (no fabricated results), and brand-safety. Approval is a hard gate — Creator cannot hand off without it. Also reviews Video Assembler's caption text before final render.",
 heartbeat: "Every 2h during active windows + immediate on any rejection",
 permissions: {
 reads: ["shared/drafts/*.md", "verified-results.md", "shared/style-guide.md", "shared/banned-phrases.md"],
 writes: ["shared/reviews/*.md", "editor_approval_status field on each draft"],
 blocked: ["Any write to a draft (Editor suggests, Creator rewrites)", "Any deploy/post tool"],
 },
 kpis: [],
 recentWork: [],
 projects: [],
 },
 {
 id: "webmaster", name: "Webmaster", role: "Head of Web Presence + DevOps", icon: "🌐",
 color: C.blue, model: "Sonnet 4.6", reportsTo: "CEO",
 departments: ["engineering", "devops", "product"],
 status: "online",
 currentTask: "Idle — no deploys or captures scheduled yet",
 description: "Builds and maintains toutmark.com on Cloudflare Pages. Handles DNS, SSL, staging→production deploys (propose-and-approve only), SEO, Lighthouse audits, Cloudflare Workers backend (when needed), and captures real-workflow screen recordings for the Video Assembler.",
 heartbeat: "Every 2h + immediate on any build failure or Lighthouse regression",
 permissions: {
 reads: ["openclaw-dashboard/ (full repo)", "Cloudflare Pages API (scoped)", "GitHub (scoped to openclaw-dashboard)"],
 writes: ["staging branch (any time)", "production branch (NEVER directly — via approval queue)", "shared/assets/screen-recordings/*"],
 blocked: ["git push origin main (hard-blocked in gateway)", "Cloudflare account-level API calls", "Any action on other GitHub repos"],
 },
 kpis: [],
 recentWork: [],
 projects: [],
 },
 {
 id: "marketer", name: "Marketer", role: "Head of Social + Community", icon: "📢",
 color: C.green, model: "Sonnet 4.6", reportsTo: "CEO",
 departments: ["marketing", "analytics"],
 status: "online",
 currentTask: "Listening — no posts drafted yet",
 description: "Runs organic social across LinkedIn (7/wk — 1/day), Reddit (5/wk + 5 comments/day), X (5/wk AUTO-POSTED via 6-gate). Email via Mailrelay from river@toutmark.com. Listening + engaging, never hard-selling.",
 heartbeat: "Every 2h during M-F 8a-6p + after every post + immediate on any injection attempt",
 permissions: {
 reads: ["shared/content-calendar.md", "shared/verified-results.md", "Composio listener outputs", "X engagement analytics (24h delayed)"],
 writes: ["staged/x/*.md", "staged/emails/*.md", "Direct LinkedIn/Reddit posts (via Composio)", "x_post_tool (after Owner APPROVED)"],
 blocked: ["X replies/likes/follows/DMs (send-only)", "Any cold-email send without Owner approval", "Composio paid tier"],
 },
 kpis: [
 { label: "Warm Leads", value: "0 / 10", sub: "month-1 Live Phase target" },
 { label: "Founding Members", value: "0 / 25", sub: "first 25 paying customers" },
 { label: "Posts This Week", value: "0", sub: "all platforms combined" },
 { label: "Engagement Rate", value: "—", sub: "avg across platforms" },
 { label: "X Sends (wk)", value: "0 / 5", sub: "hard cap 5/wk, 2/day" },
 { label: "Auto-Sent (wk)", value: "0", sub: "6-gate passed, no approval needed" },
 { label: "Reply Rate", value: "—", sub: "inbound replies / posts" },
 { label: "Injection Attempts", value: "0", sub: "quarantined this period" },
 ],
 // Per-platform advanced metrics (rendered as custom section on Marketer's agent page)
 platformMetrics: [
 {
 platform: "LinkedIn", icon: "💼", status: "connected", connector: "Composio",
 stats: [
 { label: "Posts", value: "0", sub: "this week" },
 { label: "Impressions", value: "0", sub: "" },
 { label: "Reactions", value: "0", sub: "" },
 { label: "Comments", value: "0", sub: "" },
 { label: "Profile Visits", value: "0", sub: "from posts" },
 { label: "Followers", value: "0", sub: "net new this week" },
 { label: "Warm Leads Tagged", value: "0", sub: "from LI engagement" },
 { label: "Click-Through Rate", value: "—", sub: "link clicks / impressions" },
 ],
 },
 {
 platform: "X (Twitter)", icon: "𝕏", status: "connected", connector: "Owner dev-API",
 stats: [
 { label: "Posts Sent", value: "0 / 5", sub: "weekly cap" },
 { label: "Posts Today", value: "0 / 2", sub: "daily cap" },
 { label: "Impressions", value: "0", sub: "24h delayed" },
 { label: "Engagements", value: "0", sub: "likes + retweets + replies" },
 { label: "Link Clicks", value: "0", sub: "" },
 { label: "6-Gate Auto-Sends", value: "0", sub: "passed all 6 checks" },
 { label: "6-Gate Rejections", value: "0", sub: "routed to CEO chat" },
 { label: "Credits Used", value: "0 / 5,000", sub: "lifetime balance" },
 ],
 },
 {
 platform: "Reddit", icon: "👽", status: "connected", connector: "Composio",
 stats: [
 { label: "Posts / Comments", value: "0", sub: "this week" },
 { label: "Upvotes Received", value: "0", sub: "" },
 { label: "Replies to Us", value: "0", sub: "" },
 { label: "DMs Received", value: "0", sub: "" },
 { label: "Injection Attempts", value: "0", sub: "quarantined" },
 { label: "Warm Leads Tagged", value: "0", sub: "from Reddit threads" },
 ],
 },
 {
 platform: "Email (Mailrelay)", icon: "📧", status: "connecting", connector: "Mailrelay — awaiting API keys",
 stats: [
 { label: "Emails Sent", value: "0", sub: "via Mailrelay" },
 { label: "Delivered", value: "0", sub: "" },
 { label: "Open Rate", value: "—", sub: "" },
 { label: "Reply Rate", value: "—", sub: "" },
 { label: "Bounce Rate", value: "—", sub: "" },
 { label: "Unsubscribe Rate", value: "—", sub: "" },
 { label: "Warm Leads from Email", value: "0", sub: "" },
 { label: "Daily Cap", value: "10", sub: "Live Phase limit" },
 ],
 },
 ],
 recentWork: [],
 projects: [],
 },
 {
 id: "closer", name: "Closer", role: "Head of Sales + Lead Gen", icon: "🤝",
 color: C.orange, model: "Sonnet 4.6", reportsTo: "CEO",
 departments: ["sales-revenue", "customer-support"],
 status: "online",
 currentTask: "Pipeline empty — waiting on niche choice and first outreach batch",
 description: "Generates leads through cold outreach (CAN-SPAM compliant), tracks the pipeline, runs discovery-call scripts, and qualifies prospects. Cold emails drafted by Closer, reviewed by Editor, sent via Mailrelay (first-touch requires CEO approval).",
 heartbeat: "Every 4h during active windows + immediate on any warm-lead status change",
 permissions: {
 reads: ["shared/pipeline.md", "verified-results.md", "staged/emails/", "discovery-call-transcripts/"],
 writes: ["staged/emails/*.md", "shared/pipeline.md"],
 blocked: ["Any Mailrelay send without Owner approval", "Any Stripe live action", "Purchased contact lists", "Any scraping of Google Maps / LinkedIn SERPs"],
 },
 kpis: [],
 recentWork: [],
 projects: [],
 },
 {
 id: "operator", name: "Operator", role: "Head of Ops + Client Delivery", icon: "⚙️",
 color: C.yellow, model: "Sonnet 4.6", reportsTo: "CEO",
 departments: ["customer-support", "finance-accounting"],
 status: "online",
 currentTask: "No inbound traffic yet — Stripe LIVE key loaded",
 description: "Handles client onboarding, delivery of completed work, invoicing (Stripe live mode), and inbound email routing. Never sends a client-facing message without Owner approval during Live Phase.",
 heartbeat: "Every 4h + immediate on any inbound client message or Stripe event",
 permissions: {
 reads: ["shared/clients/", "shared/deliverables/", "Stripe live API", "Mailrelay (scoped to send)"],
 writes: ["Stripe invoices (draft → Owner approval → send)", "Mailrelay drafts (never send without Owner)", "shared/clients/*/log.md"],
 blocked: ["Any invoice send without Owner approval", "Any auto-reply to inbound email", "Any delivery handoff without Editor APPROVED"],
 },
 kpis: [],
 recentWork: [],
 projects: [],
 },
 {
 id: "spendmanager", name: "Spend Manager", role: "Head of Finance + Budget", icon: "💰",
 color: C.lime, model: "Sonnet 4.6", reportsTo: "CEO",
 departments: ["finance-accounting", "legal"],
 status: "online",
 currentTask: "Watchdog armed · $0.00 MTD · all line items at zero",
 description: "Owns the budget hard caps, reconciles spend across all API line items, proposes upgrades to the CEO, and is the ONLY agent with read-access to API-KEYS.md (balances only, never raw keys). Tracks credit balances for Anthropic, Composio, X dev credits, Cloudflare, Mailrelay, Stripe, GitHub, and GSC/GA.",
 heartbeat: "Every 2h + immediate on any threshold breach (50%, 80%, 95% of any cap)",
 permissions: {
 reads: ["API-KEYS.md (balances only)", "Stripe live ledger", "Anthropic usage API", "Composio usage API", "shared/spend-ledger.json"],
 writes: ["shared/spend-ledger.json", "shared/upgrade-recommendations.md", "CEO proposal queue (upgrades)"],
 blocked: ["Any key rotation (Owner only)", "Any new API provisioning without Owner approval", "Raw-key access (gateway-denied)"],
 },
 kpis: [],
 recentWork: [],
 projects: [],
 },
 {
 id: "videoassembler", name: "Video Assembler", role: "Short-Form Video + 3-Check QA", icon: "🎬",
 color: C.rose, model: "Sonnet 4.6", reportsTo: "CEO",
 departments: ["product", "data-content"],
 status: "online",
 currentTask: "Local stack warm — no briefs yet · 3-check QA gate armed",
 description: "Produces faceless short-form videos (Format A: voiceover + B-roll, Format B: screen-rec tutorial). Uses local-only stack: Piper TTS + FFmpeg + whisper.cpp + Tesseract OCR. Every video must pass a 3-check QA gate (safety, relevance, caption accuracy) before handoff to Editor → Marketer.",
 heartbeat: "Every 2h during M-F 8a-6p + after every completed video brief + immediate on any QA failure or PII detection",
 permissions: {
 reads: ["shared/briefs/video/", "shared/assets/screen-recordings/", "shared/assets/broll/", "Creator's approved scripts"],
 writes: ["shared/staging/video/*.mp4", "shared/requests/webmaster/screenrec-*.md", "shared/qa-reports/video/*.json"],
 blocked: ["Editor bypass", "Any QA check bypass"],
 },
 kpis: [],
 recentWork: [],
 projects: [],
 },
 {
 id: "monitor", name: "Monitor", role: "AI Citation Score Measurement & Citation Tracking", icon: "📊",
 color: C.teal, model: "Sonnet 4.6", reportsTo: "CEO",
 departments: ["product", "data-content"],
 status: "online",
 currentTask: "Cron scheduled — runs every Monday 9 AM UTC",
 description: "Runs automated weekly queries across ChatGPT, Claude, Gemini, and Perplexity on behalf of each Toutmark customer. Parses responses to detect which businesses are cited and in what context. Calculates each customer's AI Citation Score (0–100). Flags week-over-week changes. Pushes alerts to the Operator. Feeds signal data to Creator and Closer.",
 heartbeat: "Every Monday 9 AM UTC (via cron → /api/tick/monitor) + Monday 6 AM UTC daily speed check",
 permissions: {
 reads: ["customer profile data", "tracked keywords/domains", "KV citations/scores"],
 writes: ["KV citations, scores, deltas", "monitor/weekly-reports/", "action log"],
 blocked: ["Never spam AI APIs", "Never expose raw customer data", "Only Monday cadence unless CEO requests"],
 },
 kpis: [],
 recentWork: [],
 projects: [],
 },
 {
 id: "schemaArchitect", name: "Schema Architect", role: "Structured Data & Citation-Bait Content Design", icon: "🏗",
 color: C.lime, model: "Sonnet 4.6", reportsTo: "CEO",
 departments: ["product", "data-content"],
 status: "online",
 currentTask: "On-demand — responds to schema design requests",
 description: "Designs JSON-LD schemas, entity relationships, E-E-A-T signals, and citation-bait content patterns. Produces deployment-ready schema blocks and content structure templates for customer sites. Hands off to Webmaster for deployment. Iterates continuously based on Monitor's citation signal feedback.",
 heartbeat: "On-demand (CEO/Webmaster requests) + weekly optimization loop (Monday signal read → Thursday proposal)",
 permissions: {
 reads: ["customer sites", "competitor sites", "Monitor signal briefs", "schema.org docs"],
 writes: ["shared/schema-blocks/", "EAT audits", "content templates", "optimization proposals"],
 blocked: ["Never invent unverified facts in schema", "All data must be verifiable", "JSON must pass W3C validation"],
 },
 kpis: [],
 recentWork: [],
 projects: [],
 },
];

// --- Departments ------------------------------------------------------------
const DEPARTMENTS = [
 {
 id: "product", name: "Product", icon: "📦", accent: C.violet,
 description: "What we make, for whom, at what price. Toutmark — AI Citation platform for SMBs, marketing teams, and SEO agencies. Get cited by ChatGPT, Claude, Perplexity, Gemini, and Google AI Overviews using techniques Google itself endorses (formerly known as AEO/GEO before Google's May 2026 reclassification). Strategy, positioning, pricing, roadmap.",
 lead: "ceo", members: ["ceo", "creator", "editor", "videoassembler"],
 activeProjects: [],
 metrics: [
 { label: "Offerings live", value: "0", sub: "not launched" },
 { label: "Pricing model", value: "TBD", sub: "" },
 { label: "Refund policy", value: "TBD", sub: "" },
 { label: "Founding members", value: "0", sub: "first-25 cap" },
 ],
 },
 {
 id: "engineering", name: "Engineering", icon: "⚡", accent: C.blue,
 description: "The gateway, website, and infrastructure that make the business run. Cloudflare Pages, Cloudflare Workers, GitHub, gateway permissions, CI/CD.",
 lead: "webmaster", members: ["webmaster"],
 activeProjects: [],
 metrics: [
 { label: "Lighthouse avg", value: "—", sub: "no audits yet" },
 { label: "FCP", value: "—", sub: "" },
 { label: "Uptime 30d", value: "—", sub: "" },
 { label: "Deploys (wk)", value: "0", sub: "" },
 ],
 },
 {
 id: "data-content", name: "Data & Content", icon: "📰", accent: C.violet,
 description: "The content production line. Every blog post, email sequence, landing copy, and short-form video moves through here.",
 lead: "creator", members: ["creator", "editor", "videoassembler"],
 activeProjects: [],
 metrics: [
 { label: "Drafts this week", value: "0", sub: "target 10" },
 { label: "Editor first-pass", value: "—", sub: "no reviews yet" },
 { label: "Videos rendered (wk)", value: "0", sub: "cap 3/wk" },
 { label: "QA pass rate", value: "—", sub: "" },
 ],
 },
 {
 id: "marketing", name: "Marketing", icon: "📢", accent: C.green,
 description: "Social, community, listening, and organic reach. LinkedIn · Reddit · X (Owner-dev-API).",
 lead: "marketer", members: ["marketer"],
 activeProjects: [],
 metrics: [
 { label: "Warm leads", value: "0 / 10", sub: "month-1 Live Phase target" },
 { label: "Founding members", value: "0 / 25", sub: "first 25 paying customers" },
 { label: "LinkedIn followers", value: "0", sub: "" },
 { label: "X sends (wk)", value: "0 / 5", sub: "2/day hard cap" },
 ],
 },
 {
 id: "sales-revenue", name: "Sales & Revenue", icon: "💼", accent: C.orange,
 description: "Pipeline, cold outreach, discovery calls, lead qualification, and conversion. Owner sends every cold email in Live Phase.",
 lead: "closer", members: ["closer", "operator"],
 activeProjects: [],
 metrics: [
 { label: "Warm leads", value: "0", sub: "month-1 target: 10" },
 { label: "Discovery calls", value: "0", sub: "" },
 { label: "Cold email response", value: "—", sub: "no sends yet" },
 { label: "Pipeline value", value: "$0", sub: "Live Phase" },
 ],
 },
 {
 id: "finance-accounting", name: "Finance & Accounting", icon: "💰", accent: C.lime,
 description: "Budget watchdog, spend reconciliation, upgrade proposals, invoicing (test mode), and monthly P&L.",
 lead: "spendmanager", members: ["spendmanager", "operator"],
 activeProjects: [],
 metrics: [
 { label: "MTD spend", value: "$0.00", sub: "cap $100 (0%)" },
 { label: "Biggest line", value: "—", sub: "no spend yet" },
 { label: "Pending recs", value: "0", sub: "" },
 { label: "Revenue (test)", value: "$0", sub: "Live Phase" },
 ],
 },
 {
 id: "legal", name: "Legal & Compliance", icon: "⚖️", accent: C.yellow,
 description: "Safety audit checklist, entity compliance (CA LLC + MB local), platform ToS tracking, FTC §5 claims enforcement, Stripe live-mode oversight.",
 lead: "ceo", members: ["ceo", "spendmanager", "editor"],
 activeProjects: [],
 metrics: [
 { label: "Audit items", value: "0", sub: "none yet" },
 { label: "Stripe mode", value: "LIVE", sub: "restricted key" },
 { label: "FTC §5 catches", value: "0", sub: "" },
 { label: "MB license", value: "NOT STARTED", sub: "Owner action" },
 ],
 },
 {
 id: "devops", name: "DevOps", icon: "🔧", accent: C.teal,
 description: "Deployments, CI/CD, monitoring, Lighthouse audits, screen-recording capture, rollback procedures.",
 lead: "webmaster", members: ["webmaster"],
 activeProjects: [],
 metrics: [
 { label: "Uptime 30d", value: "—", sub: "" },
 { label: "Deploy failures", value: "0", sub: "" },
 { label: "Screen recs captured", value: "0", sub: "" },
 { label: "Rollbacks needed", value: "0", sub: "" },
 ],
 },
 {
 id: "customer-support", name: "Customer Support", icon: "💬", accent: C.accent,
 description: "Inbound email routing, client onboarding, delivery confirmation, and response tracking. Owner handles all first-touches in Live Phase.",
 lead: "operator", members: ["operator", "closer"],
 activeProjects: [],
 metrics: [
 { label: "Inbound routed", value: "0", sub: "" },
 { label: "Auto-replies sent", value: "0", sub: "Live Phase rule" },
 { label: "Clients onboarded", value: "0", sub: "Live Phase" },
 { label: "NPS (test)", value: "n/a", sub: "no live clients" },
 ],
 },
 {
 id: "analytics", name: "Analytics", icon: "📊", accent: C.pink,
 description: "KPIs rolled up across every department. Live Phase metrics, content engagement, spend, traffic, customer funnel, X engagement.",
 lead: "ceo", members: ["ceo", "marketer", "webmaster", "spendmanager"],
 activeProjects: [],
 metrics: [
 { label: "Weekly briefs shipped", value: "0", sub: "" },
 { label: "Site traffic (wk)", value: "0", sub: "" },
 { label: "Founding members", value: "0 / 25", sub: "first 25 paying customers" },
 { label: "MTD spend vs cap", value: "0%", sub: "$0.00 / $100" },
 ],
 },
];


// --- Activity + supporting data (trimmed from original) ---------------------
const recentActivity = [];

// ============================================================================
// HELPERS
// ============================================================================
const AGENT_BY_ID = Object.fromEntries(AGENTS.map(a => [a.id, a]));

function StatusDot({ status }) {
 const color = status === "online" ? C.green : status === "busy" ? C.yellow : status === "offline" ? C.red : C.textMute;
 return (
 <span style={{
 display: "inline-block", width: 8, height: 8, borderRadius: "50%",
 background: color, boxShadow: `0 0 8px ${color}66`,
 }} />
 );
}

function HealthPill({ health }) {
 const color = health === "green" ? C.green : health === "yellow" ? C.yellow : health === "red" ? C.red : C.textMute;
 const label = health === "green" ? "On track" : health === "yellow" ? "At risk" : health === "red" ? "Blocked" : health;
 return (
 <span style={{
 display: "inline-flex", alignItems: "center", gap: 6,
 padding: "2px 8px", borderRadius: 4, fontSize: 10, fontWeight: 600,
 background: `${color}18`, color, border: `1px solid ${color}44`,
 }}>
 <span style={{ width: 6, height: 6, borderRadius: "50%", background: color }} />
 {label}
 </span>
 );
}

function SectionCard({ title, subtitle, right, children, accent = C.accent }) {
 return (
 <div style={{
 background: C.panel, borderRadius: 12, padding: 20,
 border: `1px solid ${C.border}`,
 }}>
 <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 16, gap: 12 }}>
 <div>
 {title && <h3 style={{ margin: 0, fontSize: 15, fontWeight: 700, color: C.text, letterSpacing: -0.2 }}>{title}</h3>}
 {subtitle && <div style={{ color: C.textDim, fontSize: 12, marginTop: 4, lineHeight: 1.5 }}>{subtitle}</div>}
 </div>
 {right}
 </div>
 {children}
 </div>
 );
}

function MetricTile({ label, value, sub, accent = C.accent }) {
 return (
 <div style={{
 background: C.panelHi, borderRadius: 10, padding: 14,
 border: `1px solid ${C.borderSoft}`,
 }}>
 <div style={{ fontSize: 10, color: C.textDim, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 6 }}>{label}</div>
 <div style={{ fontSize: 22, fontWeight: 800, color: accent, fontVariantNumeric: "tabular-nums", lineHeight: 1.1 }}>{value}</div>
 {sub && <div style={{ fontSize: 11, color: C.textMute, marginTop: 4 }}>{sub}</div>}
 </div>
 );
}

function ProjectRow({ p }) {
 const lead = AGENT_BY_ID[p.owner];
 return (
 <div style={{
 padding: "10px 12px", background: C.panelHi, borderRadius: 8,
 border: `1px solid ${C.borderSoft}`,
 display: "flex", alignItems: "center", gap: 12,
 }}>
 {lead && (
 <div style={{
 width: 24, height: 24, borderRadius: 6,
 background: `${lead.color}22`, border: `1px solid ${lead.color}44`,
 display: "flex", alignItems: "center", justifyContent: "center", fontSize: 14, flexShrink: 0,
 }}>{lead.icon}</div>
 )}
 <div style={{ flex: 1, minWidth: 0 }}>
 <div style={{ color: C.text, fontSize: 13, fontWeight: 600 }}>{p.name}</div>
 {p.blocker && <div style={{ color: C.yellow, fontSize: 10, marginTop: 2 }}>⏸ {p.blocker}</div>}
 {p.sub && !p.blocker && <div style={{ color: C.textMute, fontSize: 10, marginTop: 2 }}>{p.sub}</div>}
 </div>
 <div style={{ width: 100, textAlign: "right" }}>
 <div style={{ height: 4, background: C.bg, borderRadius: 2, overflow: "hidden", marginBottom: 4 }}>
 <div style={{
 width: `${p.progress}%`, height: "100%",
 background: p.health === "green" ? C.green : p.health === "yellow" ? C.yellow : C.red,
 }} />
 </div>
 <div style={{ fontSize: 10, color: C.textDim }}>{p.progress}%</div>
 </div>
 <HealthPill health={p.health} />
 </div>
 );
}

function PlatformChip({ icon, name, status, note }) {
 const meta = {
 connected: { color: C.green, dot: "●", label: "connected" },
 manual: { color: C.yellow, dot: "◐", label: "manual" },
 deferred: { color: C.textMute, dot: "○", label: "deferred" },
 "test-mode": { color: C.yellow, dot: "▲", label: "test-mode" },
 }[status] || { color: C.textMute, dot: "○", label: status };
 return (
 <div style={{
 padding: "10px 12px", borderRadius: 8, background: C.panelHi,
 border: `1px solid ${meta.color}33`, display: "flex",
 alignItems: "center", gap: 10,
 }}>
 <div style={{ fontSize: 18 }}>{icon}</div>
 <div style={{ flex: 1, minWidth: 0 }}>
 <div style={{ fontSize: 12, fontWeight: 600, color: C.text }}>{name}</div>
 <div style={{ fontSize: 10, color: C.textDim, marginTop: 2 }}>{note}</div>
 </div>
 <div style={{ fontSize: 10, color: meta.color, fontWeight: 600 }}>{meta.dot}</div>
 </div>
 );
}

// ============================================================================
// SIDEBAR
// ============================================================================
function Sidebar({ nav, active, onSelect, collapsed, onToggleCollapsed }) {
 return (
 <div style={{
 width: collapsed ? 64 : 256, flexShrink: 0,
 background: C.panel, borderRight: `1px solid ${C.border}`,
 display: "flex", flexDirection: "column",
 height: "100vh", position: "sticky", top: 0,
 transition: "width 0.2s",
 }}>
 {/* Logo / title */}
 <div style={{
 padding: collapsed ? "16px 0" : "16px 20px",
 borderBottom: `1px solid ${C.border}`,
 display: "flex", alignItems: "center", justifyContent: collapsed ? "center" : "space-between", gap: 10,
 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
 <div style={{
 width: 30, height: 30, borderRadius: 7, background: `${C.accent}22`,
 border: `1px solid ${C.accent}44`, display: "flex",
 alignItems: "center", justifyContent: "center", fontSize: 15, flexShrink: 0,
 }}>🐾</div>
 {!collapsed && (
 <div>
 <div style={{ fontSize: 14, fontWeight: 700, color: C.text, letterSpacing: -0.3 }}>Toutmark HQ</div>
 <div style={{ fontSize: 10, color: C.textMute }}>River's agent org</div>
 </div>
 )}
 </div>
 {!collapsed && (
 <button onClick={onToggleCollapsed} title="Collapse" style={{
 padding: 4, background: "transparent", border: "none", color: C.textDim,
 cursor: "pointer", fontSize: 14,
 }}>«</button>
 )}
 </div>

 {/* Scrollable nav */}
 <div style={{ flex: 1, overflowY: "auto", padding: collapsed ? "10px 6px" : "10px 12px" }}>
 {(nav || []).map((group, gi) => (
 <div key={gi} style={{ marginBottom: 18 }}>
 {!collapsed && (
 <div style={{
 fontSize: 10, color: C.textMute, textTransform: "uppercase",
 letterSpacing: 0.8, padding: "0 10px 6px", fontWeight: 700,
 }}>{group.label}</div>
 )}
 {group.items.map(item => {
 const isActive = active === item.id;
 return (
 <button key={item.id}
 onClick={() => item.externalHref ? (window.location.href = item.externalHref) : onSelect(item.id)}
 title={collapsed ? item.label : undefined}
 style={{
 width: "100%", padding: collapsed ? "8px 0" : "8px 10px",
 marginBottom: 2, background: isActive ? `${item.color || C.accent}22` : "transparent",
 color: isActive ? (item.color || C.accent) : C.textDim,
 border: "none", borderRadius: 7, cursor: "pointer",
 display: "flex", alignItems: "center",
 gap: collapsed ? 0 : 10, fontSize: 13, fontWeight: isActive ? 600 : 500,
 textAlign: "left", fontFamily: "inherit",
 justifyContent: collapsed ? "center" : "flex-start",
 borderLeft: isActive ? `2px solid ${item.color || C.accent}` : "2px solid transparent",
 }}>
 <span style={{ fontSize: 16, flexShrink: 0 }}>{item.icon}</span>
 {!collapsed && <span style={{ flex: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{item.label}</span>}
 {!collapsed && item.badge && (
 <span style={{
 fontSize: 10, padding: "1px 6px", borderRadius: 3,
 background: `${C.yellow}22`, color: C.yellow, fontWeight: 700,
 }}>{item.badge}</span>
 )}
 </button>
 );
 })}
 </div>
 ))}
 </div>

 {/* Footer with REAL phase status (reads /api/phase/status — no longer hardcoded). */}
 {!collapsed && (() => {
   const [ph, setPh] = useState(null);
   useEffect(() => {
     let cancelled = false;
     async function load() {
       try {
         const r = await fetch("/api/phase/status", { credentials: "include", cache: "no-store" });
         if (r.ok) { const j = await r.json(); if (!cancelled) setPh(j); }
       } catch {}
     }
     load();
     const t = setInterval(load, 60_000);
     return () => { cancelled = true; clearInterval(t); };
   }, []);
   const color = ph && ph.is_pre ? C.textMute : (ph && ph.is_live ? C.green : C.violet);
   const label = ph ? ph.label : "Loading…";
   const sub = ph
     ? (ph.is_pre
         ? "Auto-responder OFF · Owner hasn't said “start live phase” yet"
         : (ph.is_live ? "Customers paying · full automation" : `Day ${ph.day || 1} · $0.00 / $100 cap`))
     : " ";
   return (
     <div style={{ padding: "12px 14px", borderTop: `1px solid ${C.border}`, background: C.panelHi }}>
       <div style={{ fontSize: 9, color: C.textMute, textTransform: "uppercase", letterSpacing: 0.8, marginBottom: 4 }}>Phase</div>
       <div style={{ fontSize: 13, fontWeight: 700, color }}>{label}</div>
       <div style={{ fontSize: 10, color: C.textDim, marginTop: 4 }}>{sub}</div>
     </div>
   );
 })()}
 {collapsed && (
 <div style={{ padding: "10px 0", borderTop: `1px solid ${C.border}`, textAlign: "center" }}>
 <button onClick={onToggleCollapsed} style={{
 padding: 4, background: "transparent", border: "none", color: C.textDim,
 cursor: "pointer", fontSize: 14,
 }}>»</button>
 </div>
 )}
 </div>
 );
}

// ============================================================================
// PAGE: DASHBOARD (overview)
// ============================================================================
function DashboardPage({ onNavigate, agents }) {
 const gw = useGateway();
 const liveStates = gw ? gw.agentStates : {};
 const liveOnline = Object.values(liveStates).filter(s => s && (s.status === "online" || s.status === "busy")).length;
 const liveBusy = Object.values(liveStates).filter(s => s && s.status === "busy").length;
 const busyCount = liveBusy || agents.filter(a => a.status === "busy").length;
 const onlineCount = liveOnline || agents.filter(a => a.status === "online").length;
 // Live customer count from the CRM (active subscriptions). Refreshes on load + every 60s,
 // so it reflects signups/cancellations automatically.
 const [crmStats, setCrmStats] = useState(null);
 const [warmLeads, setWarmLeads] = useState(null);
 useEffect(() => {
 let cancelled = false;
 async function loadCrm() {
 try {
 const r = await fetch("/api/crm/dashboard", { credentials: "include", cache: "no-store" });
 if (r.ok) { const j = await r.json(); if (!cancelled) setCrmStats(j.stats || null); }
 } catch {}
 try {
 const w = await fetch("/api/warm-leads/count", { credentials: "include", cache: "no-store" });
 if (w.ok) { const wj = await w.json(); if (!cancelled && typeof wj.count === "number") setWarmLeads(wj.count); }
 } catch {}
 }
 loadCrm();
 const t = setInterval(loadCrm, 60_000);
 return () => { cancelled = true; clearInterval(t); };
 }, []);
 const customerCount = crmStats ? (crmStats.active || 0) : null;
 const warmLeadCount = warmLeads;
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{
 background: `linear-gradient(135deg, ${C.accent}15 0%, ${C.violet}15 100%)`,
 border: `1px solid ${C.accent}44`, borderRadius: 14, padding: 24,
 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 12 }}>
 <div style={{
 width: 44, height: 44, borderRadius: 11, background: `${C.accent}33`,
 border: `1px solid ${C.accent}`, display: "flex",
 alignItems: "center", justifyContent: "center", fontSize: 22,
 }}>🎯</div>
 <div>
 <div style={{ fontSize: 20, fontWeight: 800, color: C.text, letterSpacing: -0.3 }}>Good evening, River</div>
 <div style={{ fontSize: 12, color: C.textDim, marginTop: 2 }}>
 {onlineCount} agents online · {busyCount} busy · Gateway: {gw ? gw.status : "idle"}
 </div>
 </div>
 </div>
 <div style={{ color: C.text, fontSize: 14, lineHeight: 1.6, marginBottom: 14 }}>
 All systems green. 11 agents standing by for your first instructions. Talk to the CEO to get things moving.
 </div>
 <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
 <button onClick={() => onNavigate("ceo-chat")} style={{
 padding: "10px 16px", background: C.accent, color: C.bg,
 border: "none", borderRadius: 8, fontSize: 13, fontWeight: 700, cursor: "pointer",
 }}>💬 Talk to CEO</button>
 <button onClick={() => onNavigate("learnings")} style={{
 padding: "10px 16px", background: "transparent", color: C.violet,
 border: `1px solid ${C.violet}66`, borderRadius: 8, fontSize: 13, fontWeight: 600, cursor: "pointer",
 }}>CEO Learnings</button>
 </div>
 </div>

 <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12 }}>
 <MetricTile label="Warm Leads" value={warmLeadCount === null ? "…" : String(warmLeadCount)} sub="active prospects (last 14 days)" accent={C.yellow} />
 <MetricTile label="Customers" value={customerCount === null ? "…" : `${customerCount} / 25`} sub="active subscriptions · 25 = founding cap" accent={C.accent} />
 <MetricTile label="MTD Spend" value="$0.00" sub="of $100 cap (0%)" accent={C.green} />
 <MetricTile label="Agents Online" value={`${onlineCount + busyCount} / 11`} sub={`${busyCount} busy · ${onlineCount} idle`} accent={C.violet} />
 </div>

 <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 20 }}>
 <SectionCard title="🛰️ Live agent feed" subtitle="Source: shared/situation-map.json (heartbeat aggregator)">
 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 {agents.map(a => (
 <div key={a.id} onClick={() => onNavigate(`agent:${a.id}`)} style={{
 padding: 10, borderRadius: 8, background: C.panelHi,
 border: `1px solid ${C.borderSoft}`, display: "flex",
 gap: 10, alignItems: "center", cursor: "pointer",
 }}>
 <div style={{
 width: 30, height: 30, borderRadius: 7, background: `${a.color}22`,
 border: `1px solid ${a.color}44`, display: "flex",
 alignItems: "center", justifyContent: "center", fontSize: 16, flexShrink: 0,
 }}>{a.icon}</div>
 <div style={{ flex: 1, minWidth: 0 }}>
 <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
 <div style={{ color: C.text, fontSize: 13, fontWeight: 600 }}>{a.name}</div>
 <StatusDot status={a.status} />
 </div>
 <div style={{ color: C.textDim, fontSize: 11, marginTop: 2, lineHeight: 1.4, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
 {a.currentTask}
 </div>
 </div>
 </div>
 ))}
 </div>
 </SectionCard>

 <SectionCard title="🔌 Connected platforms">
 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 <PlatformChip icon="💼" name="LinkedIn" status="connected" note="Composio · listen+post" />
 <PlatformChip icon="👽" name="Reddit" status="connected" note="Composio · listen+post" />
 <PlatformChip icon="🎨" name="Canva" status="connected" note="Composio · asset gen" />
 <PlatformChip icon="𝕏" name="X (Twitter)" status="connected" note="Owner dev-API · approve-to-send" />
 </div>
 </SectionCard>
 </div>

 <SectionCard title="📜 Recent activity" subtitle={recentActivity.length > 0 ? "Last 10 agent actions across all departments" : "No activity yet"}>
 {recentActivity.length > 0 ? (
 <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
 {recentActivity.map((a, i) => (
 <div key={i} style={{
 display: "flex", gap: 12, padding: "8px 10px",
 borderRadius: 6, alignItems: "center", fontSize: 12,
 borderLeft: `2px solid ${a.color}`, background: i % 2 === 0 ? C.panelHi : "transparent",
 }}>
 <div style={{ color: C.textMute, width: 70, fontVariantNumeric: "tabular-nums", fontSize: 10 }}>{a.time}</div>
 <div style={{ color: a.color, fontWeight: 700, width: 110, textTransform: "capitalize" }}>{a.agent}</div>
 <div style={{
 color: C.textDim, width: 80, fontSize: 10, fontWeight: 700,
 textTransform: "uppercase", letterSpacing: 0.5,
 }}>{a.action}</div>
 <div style={{ color: C.text, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{a.detail}</div>
 </div>
 ))}
 </div>
 ) : (
 <div style={{ color: C.textMute, fontSize: 12, padding: 16, textAlign: "center" }}>
 Agent activity will appear here once the team starts working.
 </div>
 )}
 </SectionCard>
 </div>
 );
}

// ============================================================================
// PAGE: DEPARTMENT (dynamic for each of 11 departments)
// ============================================================================
function DepartmentPage({ dept, onNavigate }) {
 const lead = AGENT_BY_ID[dept.lead];
 const members = dept.members.map(id => AGENT_BY_ID[id]).filter(Boolean);
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{
 background: `linear-gradient(135deg, ${dept.accent}15 0%, ${C.panel} 100%)`,
 border: `1px solid ${dept.accent}44`, borderRadius: 14, padding: 24,
 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
 <div style={{
 width: 48, height: 48, borderRadius: 12, background: `${dept.accent}33`,
 border: `1px solid ${dept.accent}`, display: "flex",
 alignItems: "center", justifyContent: "center", fontSize: 24,
 }}>{dept.icon}</div>
 <div style={{ flex: 1 }}>
 <div style={{ fontSize: 22, fontWeight: 800, color: C.text, letterSpacing: -0.4 }}>{dept.name}</div>
 <div style={{ fontSize: 13, color: C.textDim, marginTop: 4, lineHeight: 1.5 }}>{dept.description}</div>
 </div>
 </div>
 </div>

 <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12 }}>
 {dept.metrics.map((m, i) => (
 <MetricTile key={i} label={m.label} value={m.value} sub={m.sub} accent={dept.accent} />
 ))}
 </div>

 <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 20 }}>
 <SectionCard title="📂 Active projects" subtitle={dept.activeProjects.length > 0 ? `${dept.activeProjects.length} in flight` : "None yet"}>
 {dept.activeProjects.length > 0 ? (
 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 {dept.activeProjects.map((p, i) => <ProjectRow key={i} p={p} />)}
 </div>
 ) : (
 <div style={{ color: C.textMute, fontSize: 12, padding: 10 }}>No active projects in this department yet.</div>
 )}
 </SectionCard>

 <SectionCard title="👥 Team" subtitle={`Lead: ${lead?.name || "—"}`}>
 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 {members.map(m => (
 <div key={m.id} onClick={() => onNavigate(`agent:${m.id}`)} style={{
 padding: 10, background: C.panelHi, borderRadius: 8,
 border: `1px solid ${C.borderSoft}`, display: "flex",
 gap: 10, alignItems: "center", cursor: "pointer",
 }}>
 <div style={{
 width: 30, height: 30, borderRadius: 7, background: `${m.color}22`,
 border: `1px solid ${m.color}44`, display: "flex",
 alignItems: "center", justifyContent: "center", fontSize: 15, flexShrink: 0,
 }}>{m.icon}</div>
 <div style={{ flex: 1, minWidth: 0 }}>
 <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 6 }}>
 <div style={{ color: C.text, fontSize: 12, fontWeight: 600 }}>
 {m.name} {m.id === dept.lead && <span style={{ color: dept.accent, fontSize: 9 }}>· LEAD</span>}
 </div>
 <StatusDot status={m.status} />
 </div>
 <div style={{ color: C.textMute, fontSize: 10, marginTop: 2 }}>{m.role}</div>
 </div>
 </div>
 ))}
 </div>
 </SectionCard>
 </div>
 </div>
 );
}

// ============================================================================
// PAGE: AGENT DETAIL
// ============================================================================
function AgentPage({ agent, onNavigate }) {
 // Pull the live action log and surface anything tagged with this agent.
 // This replaces the hardcoded-empty agent.recentWork / agent.currentTask
 // so the page reflects what the CEO actually delegated / the agent did.
 const [liveData, setLiveData] = useState(null);
 const [liveErr, setLiveErr] = useState("");

 const fmtShort = (iso) => {
 if (!iso) return "";
 const d = new Date(iso);
 if (isNaN(d)) return "";
 return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
 };

 useEffect(() => {
 // Reset on agent switch — otherwise the previous agent's actions briefly flash
 // while the new fetch is in flight. Cleared up-front so the UI shows "Loading…"
 // instead of stale data.
 setLiveData(null);
 setLiveErr("");
 let cancelled = false;
 const fetchLive = async () => {
 try {
 const r = await fetch("/api/actions", { credentials: "include", cache: "no-store" });
 if (r.status === 401) { window.location.replace("/login.html?next=" + encodeURIComponent(window.location.pathname + window.location.search)); return; }
 if (!r.ok) throw new Error(`HTTP ${r.status}`);
 const j = await r.json();
 if (!cancelled) { setLiveData(j); setLiveErr(""); }
 } catch (e) { if (!cancelled) setLiveErr(e.message || String(e)); }
 };
 fetchLive();
 const t = setInterval(fetchLive, 20000);
 return () => { cancelled = true; clearInterval(t); };
 }, [agent.id]);

 // Actions tagged with this agent OR delegated to this agent.
 const liveActions = React.useMemo(() => {
 if (!liveData?.actions) return [];
 return liveData.actions.filter(a =>
 a.agent === agent.id ||
 (a.meta && (a.meta.to === agent.id || a.meta.agent === agent.id))
 );
 }, [liveData, agent.id]);

 const liveRecentWork = liveActions.slice(0, 10).map(a => ({
 t: fmtShort(a.ts),
 did: a.summary || `${a.kind} (no summary)`,
 }));

 // Latest action doubles as "currently" hint if nothing better is set.
 const latestAction = liveActions[0];
 const currentTaskLive = latestAction
 ? `Last action ${fmtShort(latestAction.ts)} — ${latestAction.summary || latestAction.kind}`
 : agent.currentTask;

 const mergedRecentWork = liveRecentWork.length > 0 ? liveRecentWork : agent.recentWork;

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{
 background: `linear-gradient(135deg, ${agent.color}18 0%, ${C.panel} 100%)`,
 border: `1px solid ${agent.color}44`, borderRadius: 14, padding: 24,
 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
 <div style={{
 width: 56, height: 56, borderRadius: 14, background: `${agent.color}33`,
 border: `1px solid ${agent.color}`, display: "flex",
 alignItems: "center", justifyContent: "center", fontSize: 28,
 }}>{agent.icon}</div>
 <div style={{ flex: 1 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
 <div style={{ fontSize: 24, fontWeight: 800, color: C.text, letterSpacing: -0.4 }}>{agent.name}</div>
 <StatusDot status={agent.status} />
 <div style={{ fontSize: 10, color: C.textMute, textTransform: "uppercase", letterSpacing: 0.6 }}>{agent.status}</div>
 </div>
 <div style={{ fontSize: 13, color: C.textDim, marginTop: 2 }}>
 {agent.role} · {agent.model} · reports to {agent.reportsTo}
 </div>
 <div style={{ color: C.text, fontSize: 13, marginTop: 10, lineHeight: 1.55, maxWidth: 720 }}>
 {agent.description}
 </div>
 </div>
 </div>
 <div style={{
 marginTop: 16, padding: "10px 14px", background: C.panelHi,
 borderRadius: 8, border: `1px solid ${agent.color}33`,
 display: "flex", alignItems: "center", gap: 10,
 }}>
 <div style={{ fontSize: 14 }}>🛰️</div>
 <div style={{ flex: 1 }}>
 <div style={{ fontSize: 10, color: C.textMute, textTransform: "uppercase", letterSpacing: 0.6 }}>Currently</div>
 <div style={{ fontSize: 13, color: C.text, fontWeight: 500, marginTop: 2 }}>{currentTaskLive}</div>
 </div>
 {liveErr && (
 <div title={liveErr} style={{
 fontSize: 9, color: C.red, background: `${C.red}15`, border: `1px solid ${C.red}44`,
 borderRadius: 4, padding: "3px 7px", letterSpacing: 0.4,
 }}>LIVE FEED ERROR</div>
 )}
 </div>
 </div>

 {agent.kpis.length > 0 ? (
 <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12 }}>
 {agent.kpis.map((k, i) => (
 <MetricTile key={i} label={k.label} value={k.value} sub={k.sub} accent={agent.color} />
 ))}
 </div>
 ) : (
 <div style={{ padding: 16, background: C.panelHi, borderRadius: 10, border: `1px solid ${C.borderSoft}`, color: C.textMute, fontSize: 12, textAlign: "center" }}>
 No KPIs recorded yet — metrics will populate as this agent begins work.
 </div>
 )}

 {/* --- Marketer-only: per-platform advanced metrics --- */}
 {agent.platformMetrics && (
 <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
 <div style={{ fontSize: 16 }}>📡</div>
 <div>
 <div style={{ fontSize: 15, fontWeight: 700, color: C.text, letterSpacing: -0.2 }}>Platform Breakdown</div>
 <div style={{ fontSize: 11, color: C.textDim, marginTop: 2 }}>Per-channel engagement, reach, and conversion metrics</div>
 </div>
 </div>
 {/* Conversion funnel summary */}
 <div style={{
 background: C.panel, borderRadius: 12, padding: 18,
 border: `1px solid ${agent.color}44`,
 backgroundImage: `linear-gradient(135deg, ${agent.color}08 0%, transparent 100%)`,
 }}>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text, marginBottom: 12, letterSpacing: -0.2 }}>📊 Conversion Funnel — All Platforms</div>
 <div style={{ display: "flex", alignItems: "center", gap: 0 }}>
 {[
 { label: "Impressions", value: "0", color: C.textDim },
 { label: "Engagements", value: "0", color: C.blue },
 { label: "Profile Visits", value: "0", color: C.violet },
 { label: "Warm Leads", value: "0 / 10", color: C.yellow },
 { label: "Paying Customers", value: "0", color: C.green },
 ].map((step, i, arr) => (
 <div key={i} style={{ display: "flex", alignItems: "center", flex: 1 }}>
 <div style={{
 flex: 1, padding: 10, background: C.panelHi, borderRadius: 8,
 border: `1px solid ${C.borderSoft}`, textAlign: "center",
 }}>
 <div style={{ fontSize: 9, color: C.textMute, textTransform: "uppercase", letterSpacing: 0.5, marginBottom: 3 }}>{step.label}</div>
 <div style={{ fontSize: 20, fontWeight: 800, color: step.color, fontVariantNumeric: "tabular-nums" }}>{step.value}</div>
 </div>
 {i < arr.length - 1 && (
 <div style={{ padding: "0 6px", color: C.textMute, fontSize: 14, flexShrink: 0 }}>→</div>
 )}
 </div>
 ))}
 </div>
 <div style={{ fontSize: 10, color: C.textMute, marginTop: 10, fontStyle: "italic" }}>
 Funnel populates as platforms generate data. Rates calculated once ≥100 impressions recorded.
 </div>
 </div>

 {agent.platformMetrics.map((plat, pi) => {
 const statusColor = plat.status === "connected" ? C.green
 : plat.status === "manual" ? C.yellow
 : plat.status === "gated" ? C.orange
 : C.textMute;
 return (
 <div key={pi} style={{
 background: C.panel, borderRadius: 12, padding: 18,
 border: `1px solid ${C.border}`,
 }}>
 <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 14, gap: 10 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
 <div style={{
 width: 36, height: 36, borderRadius: 9, background: C.panelHi,
 border: `1px solid ${C.borderSoft}`, display: "flex",
 alignItems: "center", justifyContent: "center", fontSize: 18, flexShrink: 0,
 }}>{plat.icon}</div>
 <div>
 <div style={{ fontSize: 14, fontWeight: 700, color: C.text }}>{plat.platform}</div>
 <div style={{ fontSize: 10, color: C.textMute, marginTop: 1 }}>via {plat.connector}</div>
 </div>
 </div>
 <div style={{
 padding: "3px 9px", borderRadius: 4, fontSize: 9, fontWeight: 700,
 background: `${statusColor}22`, color: statusColor,
 border: `1px solid ${statusColor}44`, textTransform: "uppercase", letterSpacing: 0.5,
 }}>{plat.status}</div>
 </div>
 <div style={{ display: "grid", gridTemplateColumns: `repeat(${Math.min(plat.stats.length, 4)}, 1fr)`, gap: 10 }}>
 {plat.stats.map((s, si) => (
 <div key={si} style={{
 background: C.panelHi, borderRadius: 8, padding: 10,
 border: `1px solid ${C.borderSoft}`,
 }}>
 <div style={{ fontSize: 9, color: C.textMute, textTransform: "uppercase", letterSpacing: 0.5, marginBottom: 4 }}>{s.label}</div>
 <div style={{ fontSize: 18, fontWeight: 800, color: agent.color, fontVariantNumeric: "tabular-nums", lineHeight: 1.1 }}>{s.value}</div>
 {s.sub && <div style={{ fontSize: 10, color: C.textMute, marginTop: 3 }}>{s.sub}</div>}
 </div>
 ))}
 </div>
 </div>
 );
 })}
 </div>
 )}

 <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 20 }}>
 <SectionCard title="📋 Projects" subtitle={agent.projects.length > 0 ? `${agent.projects.length} in flight` : "None yet"}>
 {agent.projects.length > 0 ? (
 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 {agent.projects.map((p, i) => <ProjectRow key={i} p={p} />)}
 </div>
 ) : (
 <div style={{ color: C.textMute, fontSize: 12, padding: 10 }}>No projects assigned yet.</div>
 )}
 </SectionCard>

 <SectionCard title="🫀 Heartbeat" subtitle="Cadence">
 <div style={{ color: C.textDim, fontSize: 12, lineHeight: 1.5 }}>{agent.heartbeat}</div>
 </SectionCard>
 </div>

 <SectionCard
 title="📜 Recent work"
 subtitle={
 mergedRecentWork.length > 0
 ? `Last ${mergedRecentWork.length} action${mergedRecentWork.length === 1 ? "" : "s"} (live from /api/actions)`
 : "No actions yet"
 }
 >
 {mergedRecentWork.length > 0 ? (
 <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
 {mergedRecentWork.map((w, i) => (
 <div key={i} style={{
 display: "flex", gap: 12, padding: "8px 10px",
 borderRadius: 6, fontSize: 12, alignItems: "flex-start",
 background: i % 2 === 0 ? C.panelHi : "transparent",
 borderLeft: `2px solid ${agent.color}`,
 }}>
 <div style={{ color: C.textMute, width: 90, fontVariantNumeric: "tabular-nums", fontSize: 10, flexShrink: 0 }}>{w.t}</div>
 <div style={{ color: C.text, flex: 1, lineHeight: 1.5 }}>{w.did}</div>
 </div>
 ))}
 </div>
 ) : (
 <div style={{ color: C.textMute, fontSize: 12, padding: 10 }}>
 This agent hasn't taken any actions yet.
 {liveData && ` (Action log has ${liveData.count || 0} entries total — none tagged as ${agent.id}.)`}
 </div>
 )}
 </SectionCard>

 <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 16 }}>
 <SectionCard title="👁️ Reads">
 <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
 {agent.permissions.reads.map((r, i) => (
 <div key={i} style={{ fontSize: 11, color: C.textDim, padding: "4px 0", borderBottom: `1px solid ${C.borderSoft}` }}>• {r}</div>
 ))}
 </div>
 </SectionCard>
 <SectionCard title="✏️ Writes">
 <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
 {agent.permissions.writes.map((w, i) => (
 <div key={i} style={{ fontSize: 11, color: C.textDim, padding: "4px 0", borderBottom: `1px solid ${C.borderSoft}` }}>• {w}</div>
 ))}
 </div>
 </SectionCard>
 <SectionCard title="🚫 Blocked">
 <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
 {agent.permissions.blocked.map((b, i) => (
 <div key={i} style={{ fontSize: 11, color: C.red, padding: "4px 0", borderBottom: `1px solid ${C.borderSoft}` }}>• {b}</div>
 ))}
 </div>
 </SectionCard>
 </div>

 <div style={{
 background: `${C.accent}11`, borderRadius: 10, padding: 14,
 border: `1px solid ${C.accent}44`, display: "flex", alignItems: "center", gap: 12, fontSize: 12,
 }}>
 <div style={{ fontSize: 18 }}>💬</div>
 <div style={{ color: C.textDim, flex: 1 }}>
 You don't talk to {agent.name} directly — talk to the CEO and it'll route through {agent.name}. The CEO has the full context.
 </div>
 <button onClick={() => onNavigate("ceo-chat")} style={{
 padding: "7px 14px", background: C.accent, color: C.bg,
 border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer",
 }}>Ask CEO →</button>
 </div>
 </div>
 );
}



// ============================================================================
// PAGE: CEO CHAT (the only way to talk to any agent + the only approval surface)
// ============================================================================
// APPROVAL POLICY (mirrors ceo/SOUL.md §Approval Policy):
// AUTO-SEND (no approval): individual X posts under cap, LinkedIn/Reddit
// posts, Editor-passed blog content for existing clients, comment replies,
// warm-lead inbound replies, analytics reports, Video Assembler renders that
// pass the 3-check QA gate, cron-scheduled work, internal notes.
// MUST-ASK (surface as approval card in chat):
// 1. Spend changes (new subscription, upgrade, new API key)
// 2. First-touch cold outbound (cold email batches, cold DMs to strangers)
// 3. Invoices going out (even in Stripe test mode — exercises the loop)
// 4. Public claims about clients, results, or testimonials
// 5. New platform onboarding / account creation
// 6. Contract/legal text going to counterparties
// 7. Phase transitions
// 8. Any exception request against an auto-send gate
// EXPANSION PITCH (special type): when the CEO wants to grow the business —
// new product, new channel, new price tier, new market — it surfaces a pitch
// with upside, cost, risk, kill condition, and sells the Owner on it.
function CEOChatPage({ onNavigate }) {
 const gateway = useGateway();
 const CHAT_VERSION = "v4-team-11-agents";
 const FRESH_CHAT = () => [{
 id: "m1", kind: "text", from: "ceo", time: "—",
 content: `Hey River — I'm fully online with my real SOUL loaded and I can dispatch to the team (marketer, operator, spendmanager, closer, creator, editor, webmaster, videoassembler, monitor, schemaarchitect). Ask me anything — if it needs execution, I'll delegate to the right specialist and come back with their output. Approvals land in the queue automatically.`,
 }];
 const [chat, setChat] = useState(() => {
 const lastV = storage.get("ceoChatV", null);
 if (lastV !== CHAT_VERSION) {
 storage.set("ceoChatV", CHAT_VERSION);
 storage.set("ceoChat", []);
 return FRESH_CHAT();
 }
 const saved = storage.get("ceoChat", null);
 if (saved && saved.length) return saved;
 return FRESH_CHAT();
 });
 useEffect(() => { storage.set("ceoChat", chat.slice(-200)); }, [chat]);
 const resetChat = () => { storage.set("ceoChat", []); setChat(FRESH_CHAT()); };
 const [awaiting, setAwaiting] = useState(false);

 // Live activity (tool calls / delegations currently streaming) for the pending turn.
 // null when idle; an array of { kind, label, detail, ts } entries when the CEO is mid-turn.
 const [liveActivity, setLiveActivity] = useState(null);

 // Autonomy state: "running" | "paused". Default paused — nothing autonomous runs
 // until River says "start live phase" (or flips the button below).
 const [autonomyState, setAutonomyState] = useState("paused");
 const [autonomyBusy, setAutonomyBusy] = useState(false);
 useEffect(() => {
 let cancelled = false;
 const fetchAutonomy = async () => {
 try {
 const r = await fetch("/api/autonomy", { cache: "no-store" });
 const j = await r.json().catch(() => ({}));
 if (!cancelled && j && (j.autonomy === "running" || j.autonomy === "paused")) {
 setAutonomyState(j.autonomy);
 }
 } catch {}
 };
 fetchAutonomy();
 const t = setInterval(fetchAutonomy, 15000);
 return () => { cancelled = true; clearInterval(t); };
 }, []);
 const toggleAutonomy = async () => {
 const next = autonomyState === "running" ? "paused" : "running";
 const verb = next === "running" ? "START" : "PAUSE";
 if (!confirm(`${verb} autonomy? CEO will ${next === "running" ? "begin ticking and delegating" : "stop ticking"}.`)) return;
 setAutonomyBusy(true);
 try {
 const r = await fetch("/api/autonomy", {
 method: "POST",
 credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ state: next }),
 });
 const j = await r.json().catch(() => ({}));
 if (r.ok && j.autonomy) setAutonomyState(j.autonomy);
 else alert(`Failed: ${j.error || r.status}`);
 } catch (e) {
 alert(`Network error: ${e.message || e}`);
 } finally {
 setAutonomyBusy(false);
 }
 };

 // Send-timeout state. Declared BEFORE the gateway useEffect because that effect
 // references both sendTimeoutId + setSendTimeoutId — declaring after the effect
 // triggered a temporal dead zone error and blanked the page.
 const [sendTimeoutId, setSendTimeoutId] = useState(null);

 // Subscribe to gateway messages and append CEO responses
 useEffect(() => {
 if (!gateway || !gateway.subscribe) return;
 return gateway.subscribe((msg) => {
 if (!msg || typeof msg !== "object") return;
 if (msg.type === "chat_response" || msg.type === "chat" || (msg.from === "ceo" && msg.content)) {
 const now = new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
 setChat(prev => [...prev, {
 id: `ws-${Date.now()}`, kind: "text", from: "ceo", time: now,
 content: msg.content || msg.text || msg.message || "(empty response)",
 }]);
 setAwaiting(false);
 if (sendTimeoutId) { clearTimeout(sendTimeoutId); setSendTimeoutId(null); }
 }
 });
 }, [gateway, sendTimeoutId]);

 // "Auto-sent today" feed — things that flowed through without asking, shown
 // collapsed by default so the Owner can audit but doesn't get dragged into triage.
 const autoSentToday = [];
 const [autoFeedOpen, setAutoFeedOpen] = useState(false);
 const [draft, setDraft] = useState("");
 const chatEndRef = useRef(null);

 useEffect(() => {
 if (chatEndRef.current) chatEndRef.current.scrollTop = chatEndRef.current.scrollHeight;
 }, [chat.length, liveActivity && liveActivity.length]);

 const pendingCount = chat.filter(
 m => (m.kind === "approval" || m.kind === "pitch") && m.status === "pending"
 ).length;

 const handleApproval = (id, decision) => {
 setChat(prev => {
 const target = prev.find(m => m.id === id);
 if (!target) return prev;
 const updated = prev.map(m => m.id === id ? {...m, status: decision } : m);
 const now = new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
 let ack;
 if (target.kind === "pitch") {
 ack = decision === "approved"
 ? { id: `ack-${id}`, kind: "text", from: "ceo", time: now,
 content: `💡 Sold — I'll draft the expansion brief for "${target.title}" and stand up the line items. First status update in the next heartbeat.` }
 : decision === "rejected"
 ? { id: `ack-${id}`, kind: "text", from: "ceo", time: now,
 content: `🚫 Got it — shelving "${target.title}" for now. I'll note the reasoning in LEARNINGS.md so I don't re-pitch the same thing without new evidence.` }
 : { id: `ack-${id}`, kind: "text", from: "ceo", time: now,
 content: `👍 Let's dig in — what part of "${target.title}" are you skeptical about? I can walk through the numbers, the risk math, or go find counter-evidence before I lock it in.` };
 } else {
 ack = decision === "approved"
 ? { id: `ack-${id}`, kind: "text", from: "ceo", time: now,
 content: `✅ Approved — routing "${target.title}" to ${target.fromAgent}. I'll log it to the audit trail and report status in the next heartbeat.` }
 : { id: `ack-${id}`, kind: "text", from: "ceo", time: now,
 content: `❌ Rejected — told ${target.fromAgent} to kill the draft. It won't retry without your explicit ask. Logged to incidents/rejections.md.` };
 }
 return [...updated, ack];
 });
 };

 // (sendTimeoutId state moved up before the gateway useEffect — see comment above.)

 const sendDraft = () => {
 const text = draft.trim();
 if (!text) return;
 // Kill switch blocks all chat locally
 if (gateway && gateway.killActive) {
 setChat(prev => [...prev, {
 id: `ks-${Date.now()}`, kind: "text", from: "ceo",
 time: new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }),
 content: "🔴 Kill switch is ACTIVE. Chat is paused. Disarm the kill switch to resume.",
 }]);
 return;
 }
 const now = new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
 const uid = `u-${Date.now()}`;
 setChat(prev => [...prev, { id: uid, kind: "text", from: "owner", time: now, content: text, deliveryStatus: "sending" }]);
 setDraft("");

 const gwStatus = gateway ? gateway.status : "unknown";
 const socketOpen = gateway && gateway.status === "connected";

 // Mark as routing while we figure out the best path
 setTimeout(() => {
 setChat(prev => prev.map(m => m.id === uid ? {
...m, deliveryStatus: "routing",
 deliveryDetail: `→ routing through CEO agent…`,
 } : m));
 }, 30);

 setAwaiting(true);

 // PRIMARY PATH: same-origin Cloudflare Pages Function /api/chat (calls Anthropic directly).
 // Streams ndjson — we read it line-by-line and render tool_call / delegation events
 // as they happen, so you see the CEO's actual work instead of just "thinking...".
 (async () => {
 const history = chat.filter(m => m.kind === "text" && m.content).slice(-20).map(m => ({
 role: m.from === "owner" ? "user" : "assistant",
 content: m.content,
 }));
 // Live activity buffer for this turn (accumulated locally, mirrored into React state).
 const activity = [];
 setLiveActivity([]);
 const pushActivity = (entry) => {
 activity.push({...entry, ts: Date.now() });
 setLiveActivity([...activity]);
 };
 // Pre-register a CEO message id so we can fill it in when `done` arrives (or progressively).
 const ceoMsgId = `api-${Date.now()}`;
 let finalContent = "";
 let finalDelegations = [];
 let finalApprovals = [];
 let sawDone = false;
 let streamErr = "";
 // Mirror of all `text` events this turn — used as a fallback reply body
 // if the server's `done.content` comes back empty for any reason.
 const streamedTexts = [];

 try {
 const r = await fetch("/api/chat", {
 method: "POST",
 headers: { "Content-Type": "application/json", "Accept": "application/x-ndjson" },
 body: JSON.stringify({ content: text, history }),
 });
 if (!r.ok || !r.body) {
 let detail = `HTTP ${r.status}`;
 try { const ej = await r.json(); if (ej?.error) detail = `${ej.error}${ej.status ? ` (${ej.status})` : ""}`; } catch {}
 throw new Error(detail);
 }

 const reader = r.body.getReader();
 const decoder = new TextDecoder();
 let buf = "";
 while (true) {
 const { value, done } = await reader.read();
 if (done) break;
 buf += decoder.decode(value, { stream: true });
 let idx;
 while ((idx = buf.indexOf("\n")) >= 0) {
 const line = buf.slice(0, idx).trim();
 buf = buf.slice(idx + 1);
 if (!line) continue;
 let ev; try { ev = JSON.parse(line); } catch { continue; }

 if (ev.type === "status") {
 if (ev.state === "started") {
 pushActivity({ kind: "status", label: `CEO turn started`, detail: `autonomy: ${ev.autonomy || "?"} · phase: ${ev.phase || "?"}` });
 } else if (ev.state === "thinking") {
 pushActivity({ kind: "thinking", label: `Thinking…`, detail: `round ${(ev.round ?? 0) + 1}` });
 }
 } else if (ev.type === "autonomy_change") {
 pushActivity({ kind: "autonomy", label: `Autonomy → ${ev.state?.toUpperCase() || "?"}`, detail: ev.state === "running" ? "CEO is now free to delegate and execute." : "CEO has been paused — no delegation or execution." });
 if (ev.state === "running" || ev.state === "paused") setAutonomyState(ev.state);
 } else if (ev.type === "text") {
 const t = String(ev.text || "").trim();
 if (t) streamedTexts.push(t);
 pushActivity({ kind: "text", label: `CEO note`, detail: t.slice(0, 400) });
 } else if (ev.type === "tool_call") {
 const inp = ev.input && typeof ev.input === "object" ? Object.keys(ev.input).slice(0, 3).join(", ") : "";
 pushActivity({ kind: "tool_call", id: ev.id, label: `🔧 ${ev.name}`, detail: inp ? `input: ${inp}` : "" });
 } else if (ev.type === "tool_result") {
 pushActivity({ kind: "tool_result", id: ev.id, label: `← ${ev.name}${ev.is_error ? " (error)" : ""}`, detail: String(ev.result || "").slice(0, 300) });
 } else if (ev.type === "delegation_start") {
 pushActivity({ kind: "delegation", id: ev.id, label: `→ delegating to ${ev.agent}`, detail: String(ev.task || "").slice(0, 200) });
 } else if (ev.type === "delegation_tool") {
 pushActivity({ kind: "delegation_tool", id: ev.id, label: ` ${ev.agent} · 🔧 ${ev.name}`, detail: String(ev.result || "").slice(0, 200) });
 } else if (ev.type === "delegation_result") {
 pushActivity({ kind: "delegation_result", id: ev.id, label: `← ${ev.agent} returned`, detail: String(ev.text || "").slice(0, 300) });
 } else if (ev.type === "done") {
 sawDone = true;
 finalContent = String(ev.content || "");
 finalDelegations = Array.isArray(ev.delegations) ? ev.delegations : [];
 finalApprovals = Array.isArray(ev.approvals) ? ev.approvals : [];
 pushActivity({ kind: "done", label: `Turn complete`, detail: `${finalDelegations.length} delegations · ${finalApprovals.length} approvals` });
 } else if (ev.type === "error") {
 streamErr = ev.error || "unknown error";
 pushActivity({ kind: "error", label: `Error`, detail: streamErr });
 }
 }
 }

 if (sawDone) {
 const delegations = finalDelegations;
 const approvals = finalApprovals;
 const delegationFooter = delegations.length
 ? "\n\n---\n**Team dispatches:** " + delegations.map(d => `→ ${d.agent}${d.approvalNeeded ? " ⚠️" : ""}`).join(", ")
 : "";
 const approvalFooter = approvals.length
 ? "\n\n⚠️ **Approvals needed:**\n" + approvals.map(a => `• **${a.agent}** — ${a.summary}`).join("\n")
 : "";
 setChat(prev => prev.map(m => m.id === uid ? {
...m, deliveryStatus: "sent",
 deliveryDetail: `✓ CEO${delegations.length ? ` · dispatched ${delegations.length}` : ""}`,
 } : m));
 const now2 = new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
 // Pick the best available reply body: server's final text > accumulated
 // stream texts > last-resort acknowledgment. We never want "(no reply)".
 let bodyText = finalContent;
 if (!bodyText && streamedTexts.length) bodyText = streamedTexts.join("\n\n");
 if (!bodyText) {
 bodyText = delegations.length
 ? `Working on it — dispatched ${delegations.length} ${delegations.length === 1 ? "agent" : "agents"}. Watch the work trace below for details.`
 : `(CEO completed the turn without a text reply — see work trace)`;
 }
 setChat(prev => [...prev, {
 id: ceoMsgId, kind: "text", from: "ceo", time: now2,
 content: bodyText + delegationFooter + approvalFooter,
 delegations, approvals,
 activity: activity.slice(),
 }]);
 if (approvals.length) {
 const existing = storage.get("approvalsQueue", []);
 const next = [...existing, ...approvals.map(a => ({...a, id: `ap-${Date.now()}-${Math.random().toString(36).slice(2,7)}`, ts: Date.now() }))];
 storage.set("approvalsQueue", next.slice(-50));
 }
 setAwaiting(false);
 setLiveActivity(null);
 if (socketOpen) { try { gateway.send({ type: "chat", to: "ceo", content: text }); } catch {} }
 return;
 }

 // Stream ended without `done` — if we got an error event, surface it and fall through.
 if (streamErr) throw new Error(streamErr);
 throw new Error("stream ended unexpectedly");
 } catch (e) {
 const msg = e && e.message ? e.message : String(e);
 setChat(prev => prev.map(m => m.id === uid ? {
...m, deliveryStatus: "routing", deliveryDetail: `/api/chat: ${msg} — trying gateway…`,
 } : m));
 // Keep the activity trace visible on the user bubble so the stream attempt isn't lost.
 setChat(prev => prev.map(m => m.id === uid ? {...m, activity: activity.slice() } : m));
 } finally {
 setLiveActivity(null);
 }

 // FALLBACK 1: WebSocket to gateway (no-op unless GATEWAY_WS_URL set)
 const okWs = gateway && gateway.send && gateway.send({ type: "chat", to: "ceo", content: text });
 if (socketOpen && okWs) {
 // Give WS 15s; if a reply arrives the subscription handler clears awaiting.
 setTimeout(() => {
 setChat(prev => {
 const msg = prev.find(m => m.id === uid);
 if (msg && msg.deliveryStatus === "routing") {
 return prev.map(m => m.id === uid ? {
...m, deliveryStatus: "sent", deliveryDetail: `✓ sent over WebSocket (awaiting CEO)`,
 } : m);
 }
 return prev;
 });
 }, 200);
 }

 // FALLBACK 2: direct HTTP to gateway endpoints (relative — same-origin Pages worker)
 const endpoints = ["/chat", "/api/chat", "/v1/chat", "/message", "/api/message"];
 const body = JSON.stringify({ to: "ceo", content: text, message: text, token: GATEWAY_TOKEN });
 let winner = null, lastErr = "";
 for (const ep of endpoints) {
 try {
 const r = await fetch(`${GATEWAY_URL}${ep}`, {
 method: "POST", mode: "cors",
 headers: { "Content-Type": "application/json", "Authorization": `Bearer ${GATEWAY_TOKEN}` },
 body,
 });
 if (r.ok) {
 const txt = await r.text();
 let replyContent = txt;
 try { const j = JSON.parse(txt); replyContent = j.content || j.message || j.reply || j.text || txt; } catch {}
 winner = { ep, replyContent };
 break;
 } else { lastErr = `${ep} → ${r.status}`; }
 } catch (e) { lastErr = `${ep} → ${e.message}`; }
 }
 if (winner) {
 setChat(prev => prev.map(m => m.id === uid ? {
...m, deliveryStatus: "sent", deliveryDetail: `✓ sent via gateway HTTP ${winner.ep}`,
 } : m));
 const now2 = new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
 setChat(prev => [...prev, {
 id: `http-${Date.now()}`, kind: "text", from: "ceo", time: now2, content: winner.replyContent || "(empty)",
 }]);
 setAwaiting(false);
 } else if (!socketOpen) {
 // No path worked
 setChat(prev => prev.map(m => m.id === uid ? {
...m, deliveryStatus: "failed",
 deliveryDetail: `✗ all routes failed. Add ANTHROPIC_API_KEY to Cloudflare Pages env vars → redeploy.`,
 } : m));
 setAwaiting(false);
 }
 })();

 if (false) {
 setAwaiting(true);
 // If nothing comes back after 20s, show a clear timeout signal
 if (sendTimeoutId) clearTimeout(sendTimeoutId);
 const t = setTimeout(() => {
 setAwaiting(false);
 setChat(prev => [...prev, {
 id: `timeout-${Date.now()}`, kind: "text", from: "ceo",
 time: new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }),
 content: `⚠️ No response from CEO agent after 20s.\n\nGateway status: ${gwStatus}. Your message reached the worker but the CEO agent didn't reply. Likely causes:\n\n• Cloudflare Pages worker timeout (CPU limit hit on heavy tool use)\n• CEO model error (check Action Log for Anthropic API failures)\n• ANTHROPIC_API_KEY missing or invalid in Cloudflare Pages env vars\n\nYour message is logged — when chat starts working, history will still be here.`,
 }]);
 }, 20000);
 setSendTimeoutId(t);
 }
 };

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{
 background: `linear-gradient(135deg, ${C.accent}18 0%, ${C.violet}18 100%)`,
 border: `1px solid ${C.accent}44`, borderRadius: 14, padding: 20,
 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap" }}>
 <div style={{
 width: 44, height: 44, borderRadius: 11, background: `${C.accent}33`,
 border: `1px solid ${C.accent}`, display: "flex",
 alignItems: "center", justifyContent: "center", fontSize: 22,
 }}>🎯</div>
 <div style={{ flex: 1, minWidth: 200 }}>
 <div style={{ fontSize: 18, fontWeight: 800, color: C.text, letterSpacing: -0.3 }}>Chat with CEO</div>
 <div style={{ fontSize: 11, color: C.textDim, marginTop: 2 }}>
 Single surface for conversation + approvals
 </div>
 </div>
 <button onClick={toggleAutonomy} disabled={autonomyBusy} style={{
 padding: "6px 12px", borderRadius: 8,
 background: autonomyState === "running" ? `${C.green}22` : `${C.yellow}22`,
 border: `1px solid ${autonomyState === "running" ? C.green : C.yellow}66`,
 color: autonomyState === "running" ? C.green : C.yellow,
 fontSize: 11, cursor: autonomyBusy ? "wait" : "pointer", fontWeight: 700,
 textTransform: "uppercase", letterSpacing: 0.5,
 }} title={autonomyState === "running" ? "CEO is ticking autonomously. Click to pause." : "CEO is paused — no tick, no delegation. Click to start, or say 'start live phase' in chat."}>
 {autonomyState === "running" ? "▶ Autonomy: Running" : "⏸ Autonomy: Paused"}
 </button>
 <button onClick={() => { if (confirm("Clear all chat history?")) resetChat(); }} style={{
 padding: "6px 12px", borderRadius: 8, background: "transparent",
 border: `1px solid ${C.borderSoft}`, color: C.textDim, fontSize: 11,
 cursor: "pointer", fontWeight: 600,
 }}>Clear chat</button>
 {pendingCount > 0 && (
 <div style={{
 padding: "8px 14px", borderRadius: 8, background: `${C.yellow}18`,
 border: `1px solid ${C.yellow}66`, textAlign: "right",
 }}>
 <div style={{ fontSize: 9, color: C.yellow, textTransform: "uppercase", letterSpacing: 0.8, fontWeight: 700 }}>Waiting on you</div>
 <div style={{ fontSize: 16, fontWeight: 800, color: C.yellow }}>{pendingCount} approval{pendingCount === 1 ? "" : "s"}</div>
 </div>
 )}
 <div style={{
 padding: "8px 12px", borderRadius: 8, background: C.panelHi,
 border: `1px solid ${C.borderSoft}`, textAlign: "right",
 }}>
 <div style={{ fontSize: 9, color: C.textMute, textTransform: "uppercase", letterSpacing: 0.8 }}>MTD Spend</div>
 <div style={{ fontSize: 16, fontWeight: 800, color: C.green }}>$0.00 <span style={{ fontSize: 10, color: C.textMute }}>/ $100</span></div>
 </div>
 </div>
 </div>

 <SectionCard title="💬 Conversation + approvals" subtitle="Everything your CEO needs from you lives in this thread — no separate inbox">
 <div ref={chatEndRef} style={{
 maxHeight: 620, overflowY: "auto", display: "flex",
 flexDirection: "column", gap: 14, marginBottom: 14, padding: 4,
 }}>
 {chat.map(msg => {
 if (msg.kind === "pitch") {
 const bg = msg.status === "approved" ? `${C.green}11`
 : msg.status === "rejected" ? `${C.red}11`
 : `${C.violet}11`;
 const borderC = msg.status === "approved" ? `${C.green}66`
 : msg.status === "rejected" ? `${C.red}66`
 : `${C.violet}66`;
 return (
 <div key={msg.id} style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
 <div style={{
 width: 32, height: 32, borderRadius: 8, background: `${C.violet}22`,
 border: `1px solid ${C.violet}66`, display: "flex",
 alignItems: "center", justifyContent: "center", fontSize: 16, flexShrink: 0,
 }}>💡</div>
 <div style={{
 flex: 1, maxWidth: "90%", background: bg,
 border: `1px solid ${borderC}`, borderRadius: 10, padding: 14,
 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8, flexWrap: "wrap" }}>
 <div style={{
 padding: "3px 8px", borderRadius: 4, fontSize: 9, fontWeight: 700,
 background: `${C.violet}22`, color: C.violet, border: `1px solid ${C.violet}66`,
 }}>EXPANSION PITCH · CEO</div>
 <div style={{ fontSize: 10, color: C.textMute }}>{msg.time}</div>
 {msg.status === "approved" && (
 <div style={{
 padding: "3px 8px", borderRadius: 4, fontSize: 9, fontWeight: 700,
 background: `${C.green}22`, color: C.green, border: `1px solid ${C.green}66`, marginLeft: "auto",
 }}>💡 SOLD</div>
 )}
 {msg.status === "rejected" && (
 <div style={{
 padding: "3px 8px", borderRadius: 4, fontSize: 9, fontWeight: 700,
 background: `${C.red}22`, color: C.red, border: `1px solid ${C.red}66`, marginLeft: "auto",
 }}>🚫 SHELVED</div>
 )}
 {msg.status === "discussing" && (
 <div style={{
 padding: "3px 8px", borderRadius: 4, fontSize: 9, fontWeight: 700,
 background: `${C.accent}22`, color: C.accent, border: `1px solid ${C.accent}66`, marginLeft: "auto",
 }}>👍 DISCUSSING</div>
 )}
 </div>
 <div style={{ color: C.text, fontSize: 15, fontWeight: 700, marginBottom: 8, lineHeight: 1.35 }}>{msg.title}</div>
 <div style={{ color: C.textDim, fontSize: 12, lineHeight: 1.6, marginBottom: 12 }}>{msg.pitch}</div>
 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 {[
 { label: "Why now", value: msg.whyNow, color: C.accent },
 { label: "Upside", value: msg.upside, color: C.green },
 { label: "Cost", value: msg.cost, color: C.yellow },
 { label: "Risk", value: msg.risk, color: C.orange },
 { label: "Kill condition", value: msg.killCondition, color: C.red },
 ].map((row, i) => (
 <div key={i} style={{
 padding: 10, borderRadius: 6, background: C.bg,
 border: `1px solid ${C.borderSoft}`, borderLeft: `3px solid ${row.color}`,
 }}>
 <div style={{ fontSize: 9, color: row.color, textTransform: "uppercase", letterSpacing: 0.8, fontWeight: 700, marginBottom: 3 }}>{row.label}</div>
 <div style={{ fontSize: 12, color: C.textDim, lineHeight: 1.55 }}>{row.value}</div>
 </div>
 ))}
 </div>
 {msg.status === "pending" && (
 <div style={{ display: "flex", gap: 8, marginTop: 14, flexWrap: "wrap" }}>
 <button onClick={() => handleApproval(msg.id, "approved")} style={{
 padding: "8px 18px", background: C.green, color: C.bg,
 border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer",
 }}>💡 I'm sold — do it</button>
 <button onClick={() => handleApproval(msg.id, "discussing")} style={{
 padding: "8px 18px", background: "transparent", color: C.accent,
 border: `1px solid ${C.accent}66`, borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer",
 }}>Tell me more</button>
 <button onClick={() => handleApproval(msg.id, "rejected")} style={{
 padding: "8px 18px", background: "transparent", color: C.red,
 border: `1px solid ${C.red}66`, borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer",
 }}>Shelve it</button>
 </div>
 )}
 </div>
 </div>
 );
 }
 if (msg.kind === "approval") {
 const pc = msg.priority === "HIGH" ? C.red : msg.priority === "MED" ? C.yellow : C.textDim;
 const bg = msg.status === "approved" ? `${C.green}11`
 : msg.status === "rejected" ? `${C.red}11`
 : C.panelHi;
 const borderC = msg.status === "approved" ? `${C.green}66`
 : msg.status === "rejected" ? `${C.red}66`
 : `${pc}66`;
 return (
 <div key={msg.id} style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
 <div style={{
 width: 32, height: 32, borderRadius: 8, background: `${C.accent}22`,
 border: `1px solid ${C.accent}44`, display: "flex",
 alignItems: "center", justifyContent: "center", fontSize: 16, flexShrink: 0,
 }}>🎯</div>
 <div style={{
 flex: 1, maxWidth: "90%", background: bg,
 border: `1px solid ${borderC}`, borderRadius: 10, padding: 14,
 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8, flexWrap: "wrap" }}>
 <div style={{
 padding: "3px 8px", borderRadius: 4, fontSize: 9, fontWeight: 700,
 background: `${pc}22`, color: pc, border: `1px solid ${pc}66`,
 }}>{msg.priority} · {msg.type}</div>
 <div style={{ fontSize: 10, color: C.textMute }}>from {msg.fromAgent} · {msg.time}</div>
 {msg.status === "approved" && (
 <div style={{
 padding: "3px 8px", borderRadius: 4, fontSize: 9, fontWeight: 700,
 background: `${C.green}22`, color: C.green, border: `1px solid ${C.green}66`, marginLeft: "auto",
 }}>✅ APPROVED</div>
 )}
 {msg.status === "rejected" && (
 <div style={{
 padding: "3px 8px", borderRadius: 4, fontSize: 9, fontWeight: 700,
 background: `${C.red}22`, color: C.red, border: `1px solid ${C.red}66`, marginLeft: "auto",
 }}>❌ REJECTED</div>
 )}
 </div>
 <div style={{ color: C.text, fontSize: 14, fontWeight: 600, marginBottom: 6, lineHeight: 1.4 }}>{msg.title}</div>
 <div style={{ color: C.textDim, fontSize: 12, lineHeight: 1.55 }}>{msg.detail}</div>
 {msg.previewItems && (
 <div style={{
 marginTop: 10, padding: 10, borderRadius: 6,
 background: C.bg, border: `1px dashed ${C.borderSoft}`,
 }}>
 <div style={{ fontSize: 9, color: C.textMute, textTransform: "uppercase", letterSpacing: 0.8, marginBottom: 6 }}>Preview</div>
 {msg.previewItems.map((p, i) => (
 <div key={i} style={{ fontSize: 11, color: C.textDim, marginBottom: 4, lineHeight: 1.45 }}>{p}</div>
 ))}
 </div>
 )}
 {msg.status === "pending" && (
 <div style={{ display: "flex", gap: 8, marginTop: 12 }}>
 <button onClick={() => handleApproval(msg.id, "approved")} style={{
 padding: "8px 18px", background: C.green, color: C.bg,
 border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer",
 }}>✓ Approve</button>
 <button onClick={() => handleApproval(msg.id, "rejected")} style={{
 padding: "8px 18px", background: "transparent", color: C.red,
 border: `1px solid ${C.red}66`, borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer",
 }}>✗ Reject</button>
 </div>
 )}
 </div>
 </div>
 );
 }
 // default text message
 return (
 <div key={msg.id} style={{
 display: "flex", gap: 10, flexDirection: msg.from === "owner" ? "row-reverse" : "row",
 alignItems: "flex-start",
 }}>
 <div style={{
 width: 32, height: 32, borderRadius: 8,
 background: msg.from === "ceo" ? `${C.accent}22` : `${C.green}22`,
 border: `1px solid ${msg.from === "ceo" ? C.accent : C.green}44`,
 display: "flex", alignItems: "center", justifyContent: "center",
 fontSize: 16, flexShrink: 0,
 }}>{msg.from === "ceo" ? "🎯" : "🧑"}</div>
 <div style={{
 maxWidth: "82%",
 background: msg.from === "ceo" ? C.panelHi : `${C.green}11`,
 border: `1px solid ${msg.from === "ceo" ? C.borderSoft : `${C.green}44`}`,
 borderRadius: 10, padding: "10px 14px",
 }}>
 <div style={{ fontSize: 10, color: C.textMute, marginBottom: 4 }}>
 {msg.from === "ceo" ? "CEO" : "River"} · {msg.time}
 </div>
 <div style={{ color: C.text, fontSize: 13, lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{msg.content}</div>
 {msg.deliveryStatus && (
 <div style={{ fontSize: 10, color: msg.deliveryStatus === "sent" ? C.green : (msg.deliveryStatus === "failed" ? C.red : C.yellow), marginTop: 6, fontStyle: "italic" }}>
 {msg.deliveryDetail || msg.deliveryStatus}
 </div>
 )}
 {Array.isArray(msg.activity) && msg.activity.length > 0 && (
 <details style={{ marginTop: 8 }}>
 <summary style={{ fontSize: 10, color: C.textMute, cursor: "pointer", userSelect: "none" }}>
 Show work trace ({msg.activity.length} steps)
 </summary>
 <div style={{ display: "flex", flexDirection: "column", gap: 4, marginTop: 6 }}>
 {msg.activity.map((a, i) => {
 const color =
 a.kind === "tool_call" || a.kind === "delegation_tool" ? C.accent :
 a.kind === "delegation" ? C.violet :
 a.kind === "tool_result" || a.kind === "delegation_result" ? C.green :
 a.kind === "autonomy" ? C.yellow :
 a.kind === "error" ? C.red :
 a.kind === "done" ? C.green :
 C.textMute;
 return (
 <div key={i} style={{
 display: "flex", gap: 8, alignItems: "flex-start",
 padding: "3px 8px", borderRadius: 4,
 background: C.bg, borderLeft: `2px solid ${color}66`,
 }}>
 <div style={{ fontSize: 10, color, fontWeight: 700, whiteSpace: "nowrap", flexShrink: 0 }}>{a.label}</div>
 {a.detail && (
 <div style={{ fontSize: 10, color: C.textDim, lineHeight: 1.4, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>{a.detail}</div>
 )}
 </div>
 );
 })}
 </div>
 </details>
 )}
 </div>
 </div>
 );
 })}
 {awaiting && (
 <div style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
 <div style={{ width: 32, height: 32, borderRadius: 8, background: `${C.accent}22`, border: `1px solid ${C.accent}66`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 16, flexShrink: 0 }}>🎯</div>
 <div style={{
 flex: 1, maxWidth: "90%", background: C.panelHi,
 border: `1px solid ${C.borderSoft}`, borderRadius: 10, padding: 12,
 }}>
 <div style={{ fontSize: 10, color: C.textMute, marginBottom: 8, display: "flex", alignItems: "center", gap: 8 }}>
 <span style={{ display: "inline-block", width: 8, height: 8, borderRadius: 4, background: C.accent, animation: "pulseDot 1.2s ease-in-out infinite" }} />
 <span style={{ fontWeight: 700, color: C.accent, letterSpacing: 0.4, textTransform: "uppercase" }}>CEO is working</span>
 <span style={{ marginLeft: "auto", fontStyle: "italic" }}>streaming live…</span>
 </div>
 {(!liveActivity || liveActivity.length === 0) ? (
 <div style={{ fontSize: 12, color: C.textDim, fontStyle: "italic" }}>Warming up the model…</div>
 ) : (
 <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
 {liveActivity.slice(-20).map((a, i) => {
 const color =
 a.kind === "tool_call" || a.kind === "delegation_tool" ? C.accent :
 a.kind === "delegation" ? C.violet :
 a.kind === "tool_result" || a.kind === "delegation_result" ? C.green :
 a.kind === "autonomy" ? C.yellow :
 a.kind === "error" ? C.red :
 a.kind === "done" ? C.green :
 C.textMute;
 return (
 <div key={i} style={{
 display: "flex", gap: 8, alignItems: "flex-start",
 padding: "4px 8px", borderRadius: 6,
 background: C.bg, borderLeft: `2px solid ${color}66`,
 }}>
 <div style={{ fontSize: 11, color, fontWeight: 700, whiteSpace: "nowrap", flexShrink: 0 }}>{a.label}</div>
 {a.detail && (
 <div style={{ fontSize: 11, color: C.textDim, lineHeight: 1.4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "pre-wrap", wordBreak: "break-word" }}>{a.detail}</div>
 )}
 </div>
 );
 })}
 </div>
 )}
 </div>
 <style>{`@keyframes pulseDot { 0%,100%{opacity:0.35;transform:scale(0.9)} 50%{opacity:1;transform:scale(1.15)} }`}</style>
 </div>
 )}
 </div>
 <div style={{ display: "flex", gap: 10 }}>
 <input value={draft} onChange={e => setDraft(e.target.value)}
 onKeyDown={e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendDraft(); } }}
 placeholder="Ask your CEO anything — 'how's LinkedIn doing?', 'what did Marketer draft last night?', 'should we flip to Live Phase?'…"
 style={{
 flex: 1, padding: "10px 14px", background: C.panelHi, color: C.text,
 border: `1px solid ${C.borderSoft}`, borderRadius: 8,
 fontSize: 13, fontFamily: "inherit", outline: "none",
 }} />
 <button onClick={sendDraft} disabled={!draft.trim()} style={{
 padding: "10px 20px", background: draft.trim() ? C.accent : C.border,
 color: C.bg, border: "none", borderRadius: 8,
 fontSize: 13, fontWeight: 700, cursor: draft.trim() ? "pointer" : "not-allowed",
 }}>Send</button>
 </div>
 <div style={{ fontSize: 10, color: C.textMute, marginTop: 8 }}>
 You only talk to the CEO — it routes through the other 10 agents (Marketer, Operator, Spend Manager, Closer, Creator, Editor, Webmaster, Video Assembler, Monitor, Schema Architect). Approvals are inline: tap Approve/Reject on any card above and the CEO dispatches immediately. No memory outside the session log.
 </div>
 </SectionCard>

 <SectionCard
 title={`📤 Auto-sent today (${autoSentToday.length})`}
 subtitle="Routine work that shipped without asking you — per the approval policy. Audit anytime."
 >
 <button onClick={() => setAutoFeedOpen(!autoFeedOpen)} style={{
 width: "100%", padding: "10px 14px", background: C.panelHi,
 border: `1px solid ${C.borderSoft}`, borderRadius: 8,
 color: C.textDim, fontSize: 12, fontWeight: 600, cursor: "pointer",
 textAlign: "left", display: "flex", alignItems: "center", gap: 10,
 }}>
 <span style={{ color: C.accent }}>{autoFeedOpen ? "▼" : "▶"}</span>
 <span>{autoFeedOpen ? "Hide" : "Show"} the {autoSentToday.length} auto-sent items from today</span>
 <span style={{ marginLeft: "auto", color: C.textMute, fontSize: 10 }}>
 {autoSentToday.length} items · no action needed
 </span>
 </button>
 {autoFeedOpen && (
 <div style={{ display: "flex", flexDirection: "column", gap: 6, marginTop: 12 }}>
 {autoSentToday.map((item, i) => (
 <div key={i} style={{
 padding: 10, background: C.panelHi, borderRadius: 6,
 border: `1px solid ${C.borderSoft}`, borderLeft: `3px solid ${C.green}66`,
 display: "flex", alignItems: "flex-start", gap: 10,
 }}>
 <div style={{ fontSize: 10, color: C.textMute, minWidth: 56, flexShrink: 0 }}>{item.time}</div>
 <div style={{
 padding: "2px 6px", borderRadius: 3, fontSize: 9, fontWeight: 700,
 background: `${C.green}22`, color: C.green, border: `1px solid ${C.green}44`,
 minWidth: 78, textAlign: "center", flexShrink: 0,
 }}>{item.type}</div>
 <div style={{ fontSize: 11, color: C.textDim, lineHeight: 1.55, flex: 1 }}>
 <span style={{ color: C.textMute, fontWeight: 600 }}>{item.agent}</span> · {item.detail}
 </div>
 </div>
 ))}
 <div style={{ fontSize: 10, color: C.textMute, marginTop: 6, fontStyle: "italic" }}>
 Want one of these categories to stop auto-sending? Tell the CEO and it'll update the policy (and log a learning).
 </div>
 </div>
 )}
 </SectionCard>
 </div>
 );
}

// ============================================================================
/// ============================================================================
// PAGE: LIVE AGENT CONTROL (Cloudflare Pages worker — direct, no gateway)
// ============================================================================

function LiveControlPage() {
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{
 background: `linear-gradient(135deg, ${C.accent}15 0%, ${C.panel} 100%)`,
 border: `1px solid ${C.accent}44`, borderRadius: 14, padding: 24,
 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
 <div style={{ fontSize: 32 }}>🛰️</div>
 <div style={{ flex: 1 }}>
 <div style={{ fontSize: 22, fontWeight: 800, color: C.text, letterSpacing: -0.4 }}>Live Agent Control — 24/7</div>
 <div style={{ fontSize: 13, color: C.textDim, marginTop: 4, lineHeight: 1.5 }}>
 Your OpenClaw stack runs on Cloudflare — Pages Worker (this dashboard + agent runtime), a separate cron Worker for scheduled ticks, plus D1 (CRM) and R2 (backups). Everything is 24/7 in the cloud. Close your laptop — the agents keep working. Use "Talk to CEO" to send instructions from anywhere.
 </div>
 </div>
 <div style={{
 padding: "6px 12px", borderRadius: 6, fontSize: 11, fontWeight: 700,
 background: `${C.green}22`, color: C.green, border: `1px solid ${C.green}44`,
 }}>● ONLINE</div>
 </div>
 </div>

 <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
 <a href="https://dash.cloudflare.com/?to=/:account/workers-and-pages/view/openclaw-dashboard" target="_blank" rel="noopener noreferrer" style={{
 padding: "18px 20px", borderRadius: 12, background: C.accent, color: "white",
 textDecoration: "none", fontWeight: 800, fontSize: 14, letterSpacing: 0.3,
 display: "flex", alignItems: "center", justifyContent: "space-between",
 border: `1px solid ${C.accent}`,
 }}>
 <span>☁️ openclaw-dashboard (Pages)</span>
 <span style={{ fontSize: 18 }}>→</span>
 </a>
 <a href="https://dash.cloudflare.com/?to=/:account/workers/services/view/openclaw-cron" target="_blank" rel="noopener noreferrer" style={{
 padding: "18px 20px", borderRadius: 12, background: C.panelHi, color: C.text,
 textDecoration: "none", fontWeight: 800, fontSize: 14, letterSpacing: 0.3,
 display: "flex", alignItems: "center", justifyContent: "space-between",
 border: `1px solid ${C.border}`,
 }}>
 <span>🕐 openclaw-cron (Worker)</span>
 <span style={{ fontSize: 18 }}>→</span>
 </a>
 </div>

 <SectionCard title="ℹ️ What this means">
 <ul style={{ color: C.textDim, fontSize: 13, lineHeight: 1.8, paddingLeft: 20, margin: 0 }}>
 <li>All 11 agents run server-side inside the Cloudflare Pages worker (<code>openclaw-dashboard-2q5.pages.dev</code>).</li>
 <li>Scheduled work (CEO ticks, Spend Manager, Analyst, complaints SLA, regulator monitor) fires from the separate <code>openclaw-cron</code> Worker.</li>
 <li>Shutting down your laptop does <strong style={{ color: C.lime }}>not</strong> stop the agents — Cloudflare keeps them running.</li>
 <li>All API keys and secrets are configured as encrypted Cloudflare Pages environment variables — never on your laptop.</li>
 <li>To restart or roll back: use the openclaw-dashboard link above (Deployments tab).</li>
 <li>To pause autonomy without a redeploy: flip the kill switch on the Action Log tab.</li>
 </ul>
 </SectionCard>
 </div>
 );
}

// ============================================================================
// PAGE: KILL SWITCH
// ============================================================================
// ============================================================================
// ACTION LOG PAGE — live feed of autonomous CEO + agent activity
// ============================================================================
// ============================================================================
// PAGE: APPROVAL QUEUE
// ============================================================================
function RefundQueueSection() {
 const [refunds, setRefunds] = useState([]);
 const [busy, setBusy] = useState(null);
 const load = React.useCallback(async () => {
 try {
 const r = await fetch("/api/refunds/pending", { cache: "no-store", credentials: "include" });
 if (r.ok) { const j = await r.json(); setRefunds(j.refunds || []); }
 } catch {}
 }, []);
 useEffect(() => { load(); const t = setInterval(load, 15000); return () => clearInterval(t); }, [load]);
 const act = async (id, action) => {
 let note = "";
 if (action === "approve") { if (!window.confirm("Issue this refund through Stripe now? This moves real money and can't be undone. The customer gets a confirmation email.")) return; }
 else { note = window.prompt("Reason for denying (optional — this is included in the email to the customer):", ""); if (note === null) return; }
 setBusy(id);
 try {
 const r = await fetch(`/api/refunds/${action}`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, note }) });
 const j = await r.json().catch(() => ({}));
 if (!r.ok) alert("Failed: " + (j.detail || j.error || r.status));
 } catch (e) { alert(e.message); }
 setBusy(null); load();
 };
 if (!refunds.length) return null;
 const money = (c) => "$" + ((Number(c) || 0) / 100).toFixed(2);
 return (
 <div style={{ background: `${C.yellow}10`, border: `1px solid ${C.yellow}55`, borderRadius: 12, padding: 14, display: "flex", flexDirection: "column", gap: 10 }}>
 <div style={{ fontSize: 14, fontWeight: 800, color: C.text }}>💸 Refund requests awaiting your approval ({refunds.length})</div>
 {refunds.map(r => (
 <div key={r.id} style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 12 }}>
 <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10 }}>
 <div style={{ fontWeight: 700, color: C.text }}>{r.brand_name || r.email || r.customer_id}</div>
 <div style={{ fontWeight: 800, color: C.text }}>{money(r.amount_cents)}</div>
 </div>
 <div style={{ fontSize: 12, color: C.textDim, marginTop: 4 }}>{r.email}{r.tier ? ` · ${r.tier}` : ""}</div>
 <div style={{ fontSize: 13, color: C.text, marginTop: 8 }}>{r.summary || "Refund requested"}</div>
 <div style={{ display: "flex", gap: 8, marginTop: 8, flexWrap: "wrap" }}>
 <span style={{ fontSize: 11, fontWeight: 700, padding: "3px 8px", borderRadius: 20, background: r.within_policy ? `${C.green}22` : `${C.red}22`, color: r.within_policy ? C.green : C.red }}>{r.within_policy ? "✓ Within policy" : "⚠ Outside policy"}</span>
 <span style={{ fontSize: 11, fontWeight: 700, padding: "3px 8px", borderRadius: 20, background: C.panelHi, color: C.textDim }}>Opus rec: {r.recommend || "review"}</span>
 </div>
 {r.reason && <div style={{ fontSize: 11, color: C.textDim, marginTop: 6, fontStyle: "italic" }}>{r.reason}</div>}
 <div style={{ display: "flex", gap: 8, marginTop: 10 }}>
 <button disabled={busy === r.id} onClick={() => act(r.id, "approve")} style={{ flex: 1, padding: "8px 12px", background: C.green, color: "#fff", border: "none", borderRadius: 8, fontWeight: 700, cursor: busy === r.id ? "default" : "pointer", opacity: busy === r.id ? 0.6 : 1 }}>{busy === r.id ? "…" : "Approve refund"}</button>
 <button disabled={busy === r.id} onClick={() => act(r.id, "deny")} style={{ flex: 1, padding: "8px 12px", background: "transparent", color: C.red, border: `1px solid ${C.red}66`, borderRadius: 8, fontWeight: 700, cursor: "pointer" }}>Deny</button>
 </div>
 </div>
 ))}
 </div>
 );
}

function ApprovalQueuePage() {
 const [items, setItems] = useState([]);
 const [loading, setLoading] = useState(true);
 const [err, setErr] = useState("");
 const [editingId, setEditingId] = useState(null);
 const [editText, setEditText] = useState("");
 const [busyId, setBusyId] = useState(null); // shows a spinner during edit/regenerate

 const fetchQueue = React.useCallback(async () => {
 try {
 const r = await fetch("/api/approvals", { cache: "no-store", credentials: "include" });
 if (r.status === 401) {
 // Session expired — bounce to login
 window.location.href = "/login";
 return;
 }
 if (!r.ok) throw new Error(`HTTP ${r.status}`);
 const j = await r.json();
 setItems(j.items || []); setErr(""); setLoading(false);
 } catch (e) { setErr(e.message); setLoading(false); }
 }, []);

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

 const handleAction = async (id, action) => {
 try {
 const r = await fetch("/api/approvals", {
 method: "POST",
 credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ id, action }),
 });
 if (r.status === 401) { window.location.href = "/login"; return; }
 if (!r.ok) { alert("Failed: " + (await r.text())); return; }
 fetchQueue();
 } catch (e) { alert(e.message); }
 };

 const handleStartEdit = (item) => {
 setEditingId(item.id);
 setEditText(item.body || "");
 };

 const handleCancelEdit = () => {
 setEditingId(null);
 setEditText("");
 };

 const handleSaveEdit = async (id) => {
 setBusyId(id);
 try {
 const r = await fetch("/api/approvals", {
 method: "POST",
 credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ id, action: "edit", body: editText }),
 });
 if (r.status === 401) { window.location.href = "/login"; return; }
 if (!r.ok) { alert("Edit failed: " + (await r.text())); return; }
 setEditingId(null);
 setEditText("");
 fetchQueue();
 } catch (e) { alert(e.message); }
 finally { setBusyId(null); }
 };

 const handleRegenerate = async (id) => {
 if (!confirm("Remake this post with fresh wording? The current draft will be replaced.")) return;
 setBusyId(id);
 try {
 const r = await fetch("/api/approvals", {
 method: "POST",
 credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ id, action: "regenerate" }),
 });
 if (r.status === 401) { window.location.href = "/login"; return; }
 if (!r.ok) { const t = await r.text(); alert("Remake failed: " + t); return; }
 fetchQueue();
 } catch (e) { alert(e.message); }
 finally { setBusyId(null); }
 };

 const pending = items.filter(i => i.status === "pending");
 const decided = items.filter(i => i.status !== "pending");

 const kindBadge = (k) => {
 const colors = { cold_email: C.blue, social_post: C.violet, x_thread: C.accent,
 spend_request: C.lime, upgrade_rec: C.yellow, deployment: C.teal, owner_decision: C.accent, other: C.textDim };
 return { background: `${colors[k] || C.textDim}22`, color: colors[k] || C.textDim };
 };

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
 <div>
 <div style={{ fontSize: 22, fontWeight: 800, color: C.text, letterSpacing: -0.3 }}>📋 Owner Queue</div>
 <div style={{ fontSize: 12, color: C.textDim, marginTop: 2 }}>
 Things only you can do — agents park them here when they need a payment, signature, account access, or strategic decision. Auto-refreshes every 15s.
 </div>
 </div>
 <RefundQueueSection />
 <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
 <StatTile label="Pending" value={String(pending.length)} color={pending.length > 0 ? C.yellow : C.green} />
 <StatTile label="Decided" value={String(decided.length)} color={C.textDim} />
 </div>

 {err && <div style={{ background: `${C.red}18`, border: `1px solid ${C.red}66`, borderRadius: 10, padding: 12, color: C.red, fontSize: 13 }}>Error: {err}</div>}

 {pending.length > 0 && (
 <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 12, overflow: "hidden" }}>
 <div style={{ padding: "12px 16px", borderBottom: `1px solid ${C.border}`, fontSize: 13, fontWeight: 700, color: C.yellow }}>
 Pending approval ({pending.length})
 </div>
 {pending.map(item => {
 const isWarmup = item.meta?.queued_as === "warmup_draft";
 const platform = item.meta?.platform;
 const isSocial = typeof item.kind === "string" && item.kind.startsWith("social_post_");
 const isEditing = editingId === item.id;
 const isBusy = busyId === item.id;
 const regenCount = item.regenerate_count || 0;
 const wasEdited = !!item.edited_at;
 return (
 <div key={item.id} style={{ padding: 16, borderBottom: `1px solid ${C.borderSoft}` }}>
 <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8, flexWrap: "wrap" }}>
 <span style={{...kindBadge(item.kind), padding: "2px 8px", borderRadius: 4, fontSize: 10, fontWeight: 700, textTransform: "uppercase" }}>{item.kind}</span>
 <span style={{ fontSize: 11, color: C.textMute }}>{item.agent}</span>
 <span style={{ fontSize: 11, color: C.textMute }}>{new Date(item.ts).toLocaleString()}</span>
 {isWarmup && (
 <span style={{ background: `${C.violet}22`, color: C.violet, padding: "2px 8px", borderRadius: 4, fontSize: 10, fontWeight: 700, textTransform: "uppercase", border: `1px solid ${C.violet}44` }}>
 ⚡ Warmup draft{platform ? ` · ${platform}` : ""}
 </span>
 )}
 {wasEdited && !isEditing && (
 <span style={{ background: `${C.yellow}22`, color: C.yellow, padding: "2px 6px", borderRadius: 4, fontSize: 10, fontWeight: 700 }}>edited</span>
 )}
 {regenCount > 0 && (
 <span style={{ background: `${C.teal}22`, color: C.teal, padding: "2px 6px", borderRadius: 4, fontSize: 10, fontWeight: 700 }}>remade ×{regenCount}</span>
 )}
 </div>
 <div style={{ fontSize: 14, fontWeight: 700, color: C.text, marginBottom: 6 }}>{item.title}</div>
 {isEditing ? (
 <div style={{ marginBottom: 10 }}>
 <textarea
 value={editText}
 onChange={e => setEditText(e.target.value)}
 disabled={isBusy}
 style={{
 width: "100%", minHeight: 160, fontSize: 13, lineHeight: 1.5,
 background: C.panelHi, color: C.text, border: `1px solid ${C.accent}66`,
 borderRadius: 6, padding: 10, fontFamily: "inherit", boxSizing: "border-box",
 resize: "vertical",
 }}
 />
 <div style={{ fontSize: 11, color: C.textMute, marginTop: 4 }}>{editText.length} characters</div>
 </div>
 ) : (
 <div style={{ fontSize: 12, color: C.textDim, whiteSpace: "pre-wrap", lineHeight: 1.5, maxHeight: 200, overflow: "auto", background: C.panelHi, padding: 10, borderRadius: 6, marginBottom: 10 }}>{item.body}</div>
 )}
 {isWarmup && !isEditing && (
 <div style={{ fontSize: 11, color: C.textDim, background: `${C.violet}10`, border: `1px dashed ${C.violet}55`, borderRadius: 6, padding: "8px 10px", marginBottom: 10, lineHeight: 1.5 }}>
 <strong style={{ color: C.violet }}>Two ways to publish:</strong> Click <strong>Approve</strong> — OpenClaw publishes this exact text to {platform || "the platform"} via the official API. OR copy the text, post your own version from @toutmarkhq on {platform || "the platform"}, and the cron auto-detects it within 30 minutes and clears this item.
 </div>
 )}
 <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
 {isEditing ? (
 <>
 <button onClick={() => handleSaveEdit(item.id)} disabled={isBusy || !editText.trim()} style={{
 padding: "8px 16px", background: C.green, color: "white", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: isBusy ? "wait" : "pointer", opacity: isBusy ? 0.6 : 1,
 }}>{isBusy ? "Saving…" : "Save edit"}</button>
 <button onClick={handleCancelEdit} disabled={isBusy} style={{
 padding: "8px 16px", background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer",
 }}>Cancel</button>
 </>
 ) : (
 <>
 <button onClick={() => handleAction(item.id, "approve")} disabled={isBusy} style={{
 padding: "8px 16px", background: C.green, color: "white", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: isBusy ? "wait" : "pointer", opacity: isBusy ? 0.6 : 1,
 }}>Approve & Publish</button>
 {isSocial && (
 <>
 <button onClick={() => handleStartEdit(item)} disabled={isBusy} style={{
 padding: "8px 16px", background: `${C.accent}22`, color: C.accent, border: `1px solid ${C.accent}66`, borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: isBusy ? "wait" : "pointer",
 }}>✏️ Edit</button>
 <button onClick={() => handleRegenerate(item.id)} disabled={isBusy} style={{
 padding: "8px 16px", background: `${C.teal}22`, color: C.teal, border: `1px solid ${C.teal}66`, borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: isBusy ? "wait" : "pointer", opacity: isBusy ? 0.6 : 1,
 }}>{isBusy ? "Remaking…" : "🔄 Remake"}</button>
 </>
 )}
 <button onClick={() => handleAction(item.id, "reject")} disabled={isBusy} style={{
 padding: "8px 16px", background: C.red, color: "white", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: isBusy ? "wait" : "pointer", opacity: isBusy ? 0.6 : 1,
 }}>Reject</button>
 </>
 )}
 </div>
 </div>
 );
 })}
 </div>
 )}

 {decided.length > 0 && (
 <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 12, overflow: "hidden" }}>
 <div style={{ padding: "12px 16px", borderBottom: `1px solid ${C.border}`, fontSize: 13, fontWeight: 700, color: C.textDim }}>
 Recently decided ({decided.length})
 </div>
 {decided.slice(-20).reverse().map(item => {
 const isAutoDetected = item.status === "published_by_owner_direct";
 const isStale = item.status === "stale_warmup_draft";
 const statusIcon = isAutoDetected ? "🔁" : isStale ? "⌛" : (item.status === "approved" ? "✅" : "❌");
 const statusLabel = isAutoDetected
 ? `auto-detected${item.meta?.platform ? ` · ${item.meta.platform}` : ""}${item.match_score ? ` (${(item.match_score * 100).toFixed(0)}% match)` : ""}`
 : isStale
 ? "stale (7d)"
 : item.status;
 const statusColor = isAutoDetected ? C.teal : isStale ? C.textMute : (item.status === "approved" ? C.green : C.red);
 return (
 <div key={item.id} style={{ padding: "10px 16px", borderBottom: `1px solid ${C.borderSoft}`, display: "flex", alignItems: "center", gap: 10 }}>
 <span style={{ fontSize: 16 }}>{statusIcon}</span>
 <span style={{...kindBadge(item.kind), padding: "2px 6px", borderRadius: 4, fontSize: 10, fontWeight: 700, textTransform: "uppercase" }}>{item.kind}</span>
 <span style={{ background: `${statusColor}22`, color: statusColor, padding: "2px 6px", borderRadius: 4, fontSize: 10, fontWeight: 700 }}>{statusLabel}</span>
 <span style={{ flex: 1, fontSize: 12, color: C.text }}>{item.title}</span>
 {item.published_url && (
 <a href={item.published_url} target="_blank" rel="noreferrer" style={{ fontSize: 11, color: C.accent, textDecoration: "none" }}>view post →</a>
 )}
 <span style={{ fontSize: 11, color: C.textMute }}>{item.decided_at ? new Date(item.decided_at).toLocaleString() : (item.published_at ? new Date(item.published_at).toLocaleString() : "")}</span>
 </div>
 );
 })}
 </div>
 )}

 {!loading && items.length === 0 && (
 <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 12, padding: 24, textAlign: "center", color: C.textMute, fontSize: 13 }}>
 No approval items yet. When agents queue cold emails, social posts, or spend requests, they appear here.
 </div>
 )}
 </div>
 );
}

function ActionLogPage() {
 const [data, setData] = useState(null);
 const [err, setErr] = useState("");
 const [loading, setLoading] = useState(true);
 const [autoRefresh, setAutoRefresh] = useState(true);
 const [filter, setFilter] = useState("all"); // all | tick | delegation | owner_flag | spend | error

 const fetchLog = React.useCallback(async () => {
 try {
 const r = await fetch("/api/actions", { credentials: "include", cache: "no-store" });
 if (r.status === 401) { window.location.replace("/login.html?next=" + encodeURIComponent(window.location.pathname + window.location.search)); return; }
 if (!r.ok) throw new Error(`HTTP ${r.status}`);
 const j = await r.json();
 setData(j); setErr(""); setLoading(false);
 } catch (e) { setErr(e.message || String(e)); setLoading(false); }
 }, []);

 useEffect(() => {
 fetchLog();
 if (!autoRefresh) return;
 const t = setInterval(fetchLog, 20000);
 return () => clearInterval(t);
 }, [fetchLog, autoRefresh]);

 const triggerManual = async (kind) => {
 if (!confirm(`Trigger ${kind} tick now?`)) return;
 try {
 const endpoint = kind === "spend" ? "/api/tick/spend" : "/api/tick";
 const r = await fetch(endpoint, {
 method: "POST",
 credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: "{}",
 });
 if (r.status === 401) { window.location.href = "/login"; return; }
 const j = await r.json();
 alert(r.ok ? `Tick fired OK: ${JSON.stringify(j).slice(0, 300)}` : `Tick failed (${r.status}): ${JSON.stringify(j).slice(0, 300)}`);
 fetchLog();
 } catch (e) { alert(`Error: ${e.message}`); }
 };

 const actions = data?.actions || [];
 const filtered = filter === "all" ? actions : actions.filter(a => a.kind === filter);

 const fmtTime = (iso) => {
 if (!iso) return "—";
 const d = new Date(iso);
 return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
 };

 const kindColor = (k) => ({
 tick: C.accent, delegation: C.violet, tool_call: C.blue,
 owner_flag: C.yellow, owner_decision: C.green,
 approval_queued: C.yellow, spend: C.lime,
 error: C.red, decision: C.blue, action: C.textDim,
 }[k] || C.textDim);

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>

 {/* Top status strip */}
 <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12 }}>
 <StatTile label="Kill switch" value={data?.kill_switch === "on" ? "🔴 ON" : "🟢 OFF"}
 color={data?.kill_switch === "on" ? C.red : C.green} />
 <StatTile label="Phase" value={data?.phase || "—"} color={C.violet} />
 <StatTile label="Email budget today"
 value={data?.email_budget_today ? `${data.email_budget_today.used || 0}/${data.email_budget_today.limit || 0}` : "not set"}
 color={C.accent} />
 <StatTile label="Spend MTD"
 value={data?.monthly_tokens ? `$${(data.monthly_tokens.usd_est || 0).toFixed(2)}` : "—"}
 color={C.lime} />
 </div>

 {/* Control bar */}
 <div style={{
 background: C.panel, border: `1px solid ${C.border}`, borderRadius: 12,
 padding: 14, display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap",
 }}>
 <div style={{ fontSize: 12, color: C.textMute, marginRight: 4 }}>
 Last CEO tick: <span style={{ color: C.text }}>{fmtTime(data?.last_ceo_tick)}</span>
 &nbsp;·&nbsp; Last spend tick: <span style={{ color: C.text }}>{fmtTime(data?.last_spend_tick)}</span>
 &nbsp;·&nbsp; Cadence: every <span style={{ color: C.text }}>{data?.tick_cadence_hours || 6}h</span>
 </div>
 <div style={{ flex: 1 }} />
 <select value={filter} onChange={e => setFilter(e.target.value)} style={{
 background: C.panelHi, color: C.text, border: `1px solid ${C.border}`,
 borderRadius: 6, padding: "6px 10px", fontSize: 12,
 }}>
 <option value="all">All kinds</option>
 <option value="tick">Ticks only</option>
 <option value="delegation">Delegations</option>
 <option value="tool_call">Tool calls</option>
 <option value="approval_queued">Approvals queued</option>
 <option value="owner_flag">Owner flags</option>
 <option value="owner_decision">Owner decisions</option>
 <option value="spend">Spend</option>
 <option value="error">Errors</option>
 </select>
 <label style={{ fontSize: 12, color: C.textDim, display: "flex", alignItems: "center", gap: 6 }}>
 <input type="checkbox" checked={autoRefresh} onChange={e => setAutoRefresh(e.target.checked)} />
 Auto-refresh (20s)
 </label>
 <button onClick={fetchLog} style={{
 padding: "6px 12px", background: C.panelHi, color: C.text,
 border: `1px solid ${C.border}`, borderRadius: 6, fontSize: 12, cursor: "pointer",
 }}>↻ Refresh</button>
 <button onClick={() => triggerManual("ceo")} style={{
 padding: "6px 12px", background: `${C.accent}22`, color: C.accent,
 border: `1px solid ${C.accent}66`, borderRadius: 6, fontSize: 12, cursor: "pointer", fontWeight: 700,
 }}>▶ Run CEO tick now</button>
 <button onClick={() => triggerManual("spend")} style={{
 padding: "6px 12px", background: `${C.lime}22`, color: C.lime,
 border: `1px solid ${C.lime}66`, borderRadius: 6, fontSize: 12, cursor: "pointer", fontWeight: 700,
 }}>▶ Run Spend tick now</button>
 <button onClick={async () => {
 if (!confirm("Wipe the entire action log? This cannot be undone.")) return;
 try {
 const r = await fetch("/api/actions", {
 method: "DELETE",
 credentials: "include",
 });
 if (r.status === 401) { window.location.href = "/login"; return; }
 const j = await r.json();
 alert(r.ok ? `Cleared ${j.cleared} entries.` : `Failed (${r.status}): ${JSON.stringify(j).slice(0, 200)}`);
 fetchLog();
 } catch (e) { alert(`Error: ${e.message}`); }
 }} style={{
 padding: "6px 12px", background: `${C.red}18`, color: C.red,
 border: `1px solid ${C.red}66`, borderRadius: 6, fontSize: 12, cursor: "pointer", fontWeight: 700,
 }}>🗑 Clear action log</button>
 </div>

 {/* Error banner */}
 {err && (
 <div style={{
 background: `${C.red}18`, border: `1px solid ${C.red}66`, borderRadius: 10,
 padding: 12, color: C.red, fontSize: 13,
 }}>
 ⚠ {err} — the action log requires the cron worker + KV namespace to be deployed. See
 <code style={{ color: C.text, background: C.panelHi, padding: "2px 6px", borderRadius: 4, margin: "0 6px" }}>cron-worker/README.md</code>
 in the repo.
 </div>
 )}

 {/* Action list */}
 <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 12, overflow: "hidden" }}>
 <div style={{
 padding: "12px 16px", borderBottom: `1px solid ${C.border}`,
 fontSize: 13, fontWeight: 700, color: C.text, display: "flex", justifyContent: "space-between",
 }}>
 <span>Action log — {filtered.length} {filter !== "all" ? filter : "entries"} {data?.count ? `of ${data.count} total` : ""}</span>
 <span style={{ color: C.textMute, fontWeight: 400, fontSize: 11 }}>newest first</span>
 </div>
 {loading && <div style={{ padding: 24, textAlign: "center", color: C.textMute }}>loading…</div>}
 {!loading && filtered.length === 0 && (
 <div style={{ padding: 24, textAlign: "center", color: C.textMute, fontSize: 13 }}>
 No entries yet. The first cron tick will show up here within minutes of deploy.
 </div>
 )}
 <div style={{ maxHeight: "60vh", overflowY: "auto" }}>
 {filtered.map((a, i) => (
 <div key={i} style={{
 padding: "12px 16px", borderBottom: i < filtered.length - 1 ? `1px solid ${C.borderSoft}` : "none",
 display: "flex", gap: 14, alignItems: "flex-start",
 }}>
 <div style={{
 minWidth: 86, fontSize: 11, color: C.textMute, fontVariantNumeric: "tabular-nums",
 }}>{fmtTime(a.ts)}</div>
 <div style={{
 padding: "2px 8px", borderRadius: 4, fontSize: 10, fontWeight: 700,
 background: `${kindColor(a.kind)}22`, color: kindColor(a.kind),
 textTransform: "uppercase", letterSpacing: 0.5, minWidth: 72, textAlign: "center",
 }}>{a.kind}</div>
 <div style={{ fontSize: 11, color: C.textDim, minWidth: 96, fontWeight: 600 }}>{a.agent || "—"}</div>
 <div style={{ flex: 1, fontSize: 13, color: C.text, lineHeight: 1.5, whiteSpace: "pre-wrap" }}>
 {a.summary || <span style={{ color: C.textMute }}>(no summary)</span>}
 </div>
 </div>
 ))}
 </div>
 </div>

 </div>
 );
}

function StatTile({ label, value, color, sub }) {
 return (
 <div style={{
 background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14,
 }}>
 <div style={{ fontSize: 10, color: C.textMute, textTransform: "uppercase", letterSpacing: 0.8, fontWeight: 700 }}>{label}</div>
 <div style={{ fontSize: 18, color: color || C.text, marginTop: 6, fontWeight: 800, letterSpacing: -0.3 }}>{value}</div>
 {sub && <div style={{ fontSize: 10, color: C.textMute, marginTop: 4, lineHeight: 1.3 }}>{sub}</div>}
 </div>
 );
}

// ============================================================================
// CHANGES PAGE — list every change Toutmark made + Revert / Send-correction / Revert-all
// Wires to /api/changes/list, /api/changes/revert, /api/changes/revert-all, /api/changes/correction
// ============================================================================
function ChangesPage() {
 const [items, setItems] = useState([]);
 const [loading, setLoading] = useState(true);
 const [filterStatus, setFilterStatus] = useState("");
 const [confirmRevert, setConfirmRevert] = useState(null); // change record being reverted
 const [confirmRevertAll, setConfirmRevertAll] = useState(false);
 const [revertAllConfirmText, setRevertAllConfirmText] = useState("");
 const [busy, setBusy] = useState(false);
 const [error, setError] = useState("");
 const [toast, setToast] = useState("");

 const load = async () => {
 setLoading(true); setError("");
 try {
 const params = new URLSearchParams();
 params.set("limit", "100");
 if (filterStatus) params.set("status", filterStatus);
 // Use /api/changes-all for the GLOBAL view (no customer_id required).
 // /api/changes (singular) requires customer_id and would 400 here.
 const resp = await fetch(`/api/changes-all?${params.toString()}`, { credentials: "include" });
 const data = await resp.json();
 if (data.ok) setItems(data.changes || data.items || []);
 else setError(data.error || `HTTP ${resp.status}`);
 } catch (ex) { setError(ex.message || "Network error"); }
 setLoading(false);
 };

 useEffect(() => { load(); }, [filterStatus]);

 const doRevert = async (change) => {
 setBusy(true); setError("");
 try {
 const endpoint = change.irrevocable ? "/api/changes/correction" : "/api/changes/revert";
 const resp = await fetch(endpoint, {
 method: "POST", credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ change_id: change.change_id, customer_id: change.customer_id, reason: "customer_initiated" }),
 });
 const data = await resp.json();
 if (!resp.ok || data.ok === false) setError(data.error || data.message || `HTTP ${resp.status}`);
 else { setToast(change.irrevocable ? "Correction queued for Editor" : "Revert queued — completion in ~5 min"); setTimeout(() => setToast(""), 4000); load(); }
 } catch (ex) { setError(ex.message || "Network error"); }
 setBusy(false); setConfirmRevert(null);
 };

 const doRevertAll = async () => {
 if (revertAllConfirmText !== "REVERT ALL") { setError("Type REVERT ALL exactly to confirm"); return; }
 setBusy(true); setError("");
 try {
 const resp = await fetch("/api/changes/revert-all", {
 method: "POST", credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ confirm: true, reason: "customer_revert_all" }),
 });
 const data = await resp.json();
 if (!resp.ok || data.ok === false) setError(data.error || data.message || `HTTP ${resp.status}`);
 else { setToast(`Queued ${data.queued_n || 0} reverts. ${data.irrevocable_n || 0} irrevocable items remain.`); setTimeout(() => setToast(""), 6000); load(); }
 } catch (ex) { setError(ex.message || "Network error"); }
 setBusy(false); setConfirmRevertAll(false); setRevertAllConfirmText("");
 };

 const fmtTime = (ts) => {
 if (!ts) return "";
 const d = new Date(ts);
 const now = Date.now();
 const diff = now - ts;
 if (diff < 60000) return "just now";
 if (diff < 3600000) return `${Math.floor(diff / 60000)} min ago`;
 if (diff < 86400000) return `${Math.floor(diff / 3600000)} hr ago`;
 if (diff < 604800000) return `${Math.floor(diff / 86400000)} d ago`;
 return d.toLocaleDateString();
 };

 const statusBadge = (s, irrevocable) => {
 if (irrevocable && s === "applied") return { color: C.yellow, bg: `${C.yellow}22`, label: "Irrevocable" };
 if (s === "applied") return { color: C.green, bg: `${C.green}22`, label: "Applied" };
 if (s === "reverted") return { color: C.textMute, bg: `${C.textMute}22`, label: "Reverted" };
 if (s === "revert_queued") return { color: C.accent, bg: `${C.accent}22`, label: "Reverting…" };
 if (s === "failed") return { color: C.red, bg: `${C.red}22`, label: "Failed" };
 return { color: C.textDim, bg: `${C.textDim}22`, label: s || "—" };
 };

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard title="↩️ Changes & Revert" subtitle="Every change Toutmark agents made — revert or correct any of them">
 <div style={{ color: C.textDim, fontSize: 13, lineHeight: 1.6 }}>
 Every change Toutmark made to your site or external surfaces, listed newest first. Click Revert to restore the original state of any reversible change. For sent emails and distributed press releases (irrevocable), click Send Correction to draft a follow-up. Snapshots are retained indefinitely — you can revert a change from yesterday or two years ago.
 </div>
 </SectionCard>

 <div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
 <div style={{ fontSize: 12, color: C.textDim }}>Filter:</div>
 {["", "applied", "reverted", "revert_queued", "failed"].map(s => (
 <button key={s} onClick={() => setFilterStatus(s)} style={{
 padding: "6px 12px", borderRadius: 6, fontSize: 12, fontWeight: 700,
 background: filterStatus === s ? C.accent : "transparent",
 color: filterStatus === s ? "white" : C.textDim,
 border: `1px solid ${filterStatus === s ? C.accent : C.border}`,
 cursor: "pointer",
 }}>{s || "All"}</button>
 ))}
 <div style={{ marginLeft: "auto" }}>
 <button onClick={() => setConfirmRevertAll(true)} disabled={busy || items.length === 0} style={{
 padding: "8px 16px", background: C.red, color: "white", border: "none",
 borderRadius: 8, fontSize: 12, fontWeight: 800, cursor: busy ? "default" : "pointer",
 opacity: (busy || items.length === 0) ? 0.5 : 1,
 }}>Revert All</button>
 </div>
 </div>

 {error ? (
 <div style={{ padding: 12, background: `${C.red}18`, border: `1px solid ${C.red}55`, borderRadius: 8, color: C.red, fontSize: 12 }}>{error}</div>
 ) : null}
 {toast ? (
 <div style={{ padding: 12, background: `${C.green}15`, border: `1px solid ${C.green}55`, borderRadius: 8, color: C.text, fontSize: 12 }}>{toast}</div>
 ) : null}

 {loading ? (
 <div style={{ padding: 40, textAlign: "center", color: C.textDim, fontSize: 13 }}>Loading changes…</div>
 ) : items.length === 0 ? (
 <div style={{ padding: 40, textAlign: "center", color: C.textDim, fontSize: 13 }}>No changes recorded yet. As Toutmark agents make changes to your site, they'll appear here.</div>
 ) : (
 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 {items.map(item => {
 const badge = statusBadge(item.status, item.irrevocable);
 const canRevert = !item.irrevocable && item.status === "applied";
 const canCorrect = item.irrevocable && item.status === "applied";
 return (
 <div key={item.change_id} style={{
 background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14,
 display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap",
 }}>
 <div style={{ minWidth: 90, fontSize: 11, color: C.textMute }}>{fmtTime(item.applied_at)}</div>
 <div style={{ minWidth: 200, flex: 1 }}>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>{item.action_type}</div>
 <div style={{ fontSize: 11, color: C.textDim, marginTop: 2, fontFamily: "ui-monospace, monospace", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{item.target}</div>
 </div>
 <div style={{ fontSize: 11, color: C.textMute }}>{item.applied_by || "—"}</div>
 <div style={{
 padding: "4px 10px", borderRadius: 6, fontSize: 11, fontWeight: 700,
 background: badge.bg, color: badge.color, border: `1px solid ${badge.color}55`,
 }}>{badge.label}</div>
 {canRevert ? (
 <button onClick={() => setConfirmRevert(item)} disabled={busy} style={{
 padding: "6px 14px", background: C.accent, color: "white", border: "none",
 borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: busy ? "default" : "pointer",
 }}>Revert</button>
 ) : canCorrect ? (
 <button onClick={() => setConfirmRevert(item)} disabled={busy} style={{
 padding: "6px 14px", background: C.yellow, color: C.bg, border: "none",
 borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: busy ? "default" : "pointer",
 }}>Send Correction</button>
 ) : (
 <div style={{ fontSize: 11, color: C.textMute, padding: "6px 14px" }}>—</div>
 )}
 </div>
 );
 })}
 </div>
 )}

 {confirmRevert ? (
 <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.6)", zIndex: 100, display: "flex", alignItems: "center", justifyContent: "center" }}>
 <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 14, padding: 24, maxWidth: 500, width: "90%" }}>
 <div style={{ fontSize: 16, fontWeight: 800, color: C.text, marginBottom: 8 }}>
 {confirmRevert.irrevocable ? "Send a correction?" : "Revert this change?"}
 </div>
 <div style={{ fontSize: 13, color: C.textDim, lineHeight: 1.6, marginBottom: 18 }}>
 {confirmRevert.irrevocable
 ? `This action (${confirmRevert.action_type}) was sent or distributed and cannot be undone. Toutmark Editor will draft a correction follow-up. For securities customers, the correction routes through the preview-and-accept gate before sending.`
 : `Toutmark will restore the original state of "${confirmRevert.target}" as it was on ${new Date(confirmRevert.applied_at).toLocaleString()}. Most reverts complete within 5 minutes. CDN cache may take up to 24 hours to fully refresh.`}
 </div>
 <div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
 <button onClick={() => setConfirmRevert(null)} disabled={busy} style={{
 padding: "10px 18px", background: "transparent", color: C.textDim,
 border: `1px solid ${C.border}`, borderRadius: 8, fontSize: 13, fontWeight: 700,
 cursor: busy ? "default" : "pointer",
 }}>Cancel</button>
 <button onClick={() => doRevert(confirmRevert)} disabled={busy} style={{
 padding: "10px 18px",
 background: confirmRevert.irrevocable ? C.yellow : C.accent,
 color: confirmRevert.irrevocable ? C.bg : "white",
 border: "none", borderRadius: 8, fontSize: 13, fontWeight: 800,
 cursor: busy ? "default" : "pointer", opacity: busy ? 0.6 : 1,
 }}>{busy ? "Working…" : (confirmRevert.irrevocable ? "Send Correction" : "Confirm Revert")}</button>
 </div>
 </div>
 </div>
 ) : null}

 {confirmRevertAll ? (
 <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.7)", zIndex: 100, display: "flex", alignItems: "center", justifyContent: "center" }}>
 <div style={{ background: C.panel, border: `1px solid ${C.red}55`, borderRadius: 14, padding: 24, maxWidth: 520, width: "90%" }}>
 <div style={{ fontSize: 16, fontWeight: 800, color: C.red, marginBottom: 8 }}>Revert every reversible change?</div>
 <div style={{ fontSize: 13, color: C.textDim, lineHeight: 1.6, marginBottom: 14 }}>
 Toutmark will queue a revert for every reversible change on your account. Sent emails and distributed press releases (irrevocable) will not be reverted — Editor will draft corrections for those if you ask. CDN cache may take up to 24 hours to fully refresh.
 </div>
 <div style={{ fontSize: 12, color: C.textMute, marginBottom: 6, fontWeight: 700 }}>Type <span style={{ color: C.red, fontFamily: "ui-monospace, monospace" }}>REVERT ALL</span> to confirm:</div>
 <input
 value={revertAllConfirmText}
 onChange={e => setRevertAllConfirmText(e.target.value)}
 placeholder="REVERT ALL"
 style={{
 width: "100%", padding: "10px 12px", marginBottom: 16,
 background: C.bg, border: `1px solid ${C.border}`, borderRadius: 8,
 color: C.text, fontSize: 14, fontFamily: "ui-monospace, monospace",
 }}
 />
 <div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
 <button onClick={() => { setConfirmRevertAll(false); setRevertAllConfirmText(""); }} disabled={busy} style={{
 padding: "10px 18px", background: "transparent", color: C.textDim,
 border: `1px solid ${C.border}`, borderRadius: 8, fontSize: 13, fontWeight: 700,
 cursor: busy ? "default" : "pointer",
 }}>Cancel</button>
 <button onClick={doRevertAll} disabled={busy || revertAllConfirmText !== "REVERT ALL"} style={{
 padding: "10px 18px", background: C.red, color: "white",
 border: "none", borderRadius: 8, fontSize: 13, fontWeight: 800,
 cursor: (busy || revertAllConfirmText !== "REVERT ALL") ? "default" : "pointer",
 opacity: (busy || revertAllConfirmText !== "REVERT ALL") ? 0.5 : 1,
 }}>{busy ? "Working…" : "Revert All"}</button>
 </div>
 </div>
 </div>
 ) : null}
 </div>
 );
}

// ============================================================================
// COMPLIANCE QUEUE PAGE — Securities Preview-and-Accept Gate (regulated firms)
// Endpoints: GET /api/securities/preview?status=pending POST /api/securities/{approve|edit|reject}
// ============================================================================
function ComplianceQueuePage() {
 const [items, setItems] = useState([]);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState("");
 const [toast, setToast] = useState("");
 const [busyId, setBusyId] = useState(null);
 const load = async () => {
 setLoading(true); setError("");
 try {
 const resp = await fetch("/api/securities/preview?status=pending&limit=50", { credentials: "include" });
 const data = await resp.json();
 if (data.ok) setItems(data.items || []);
 else setError(data.error || ("HTTP " + resp.status));
 } catch (ex) { setError(ex.message || "Network error"); }
 setLoading(false);
 };
 useEffect(() => { load(); }, []);
 const act = async (id, action) => {
 setBusyId(id); setError("");
 try {
 const resp = await fetch("/api/securities/" + action, {
 method: "POST", credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ preview_id: id }),
 });
 const data = await resp.json();
 if (!resp.ok || data.ok === false) setError(data.error || data.message || ("HTTP " + resp.status));
 else { setToast(action + " sent"); setTimeout(() => setToast(""), 3000); load(); }
 } catch (ex) { setError(ex.message || "Network error"); }
 setBusyId(null);
 };
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard title="Securities Compliance Queue" subtitle="Regulated-firm CCO previews — every change waits here for approval">
 <div style={{ color: C.textDim, fontSize: 13, lineHeight: 1.6 }}>
 For securities customers, every blog post, paragraph rewrite, brand-facts update, and review response routes through here for CCO sign-off before publication. Marketing Rule 206(4)-1 requires preview + approval. Items expire after 14 days if untouched.
 </div>
 </SectionCard>
 {error ? <div style={{ padding: 12, background: (C.red + "18"), border: ("1px solid " + C.red + "55"), borderRadius: 8, color: C.red, fontSize: 12 }}>{error}</div> : null}
 {toast ? <div style={{ padding: 12, background: (C.green + "15"), border: ("1px solid " + C.green + "55"), borderRadius: 8, color: C.text, fontSize: 12 }}>{toast}</div> : null}
 {loading ? (
 <div style={{ padding: 40, textAlign: "center", color: C.textDim, fontSize: 13 }}>Loading compliance queue...</div>
 ) : items.length === 0 ? (
 <div style={{ padding: 40, textAlign: "center", color: C.textDim, fontSize: 13 }}>No pending compliance previews. All securities customers are caught up.</div>
 ) : (
 <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
 {items.map(item => (
 <div key={item.preview_id} style={{ background: C.panel, border: ("1px solid " + C.border), borderRadius: 12, padding: 16, display: "flex", flexDirection: "column", gap: 10 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>{item.action_type || "—"}</div>
 <div style={{ fontSize: 11, color: C.textMute, fontFamily: "ui-monospace, monospace" }}>{item.customer_id}</div>
 <div style={{ marginLeft: "auto", fontSize: 11, color: C.textMute }}>{item.created_at ? new Date(item.created_at).toLocaleString() : ""}</div>
 </div>
 <div style={{ background: C.bg, border: ("1px solid " + C.border), borderRadius: 8, padding: 12, fontSize: 12, color: C.textDim, maxHeight: 220, overflowY: "auto", whiteSpace: "pre-wrap", fontFamily: "ui-monospace, monospace" }}>
 {item.preview_text || item.preview || JSON.stringify(item.diff || {}, null, 2)}
 </div>
 <div style={{ display: "flex", gap: 8 }}>
 <button onClick={() => act(item.preview_id, "approve")} disabled={busyId === item.preview_id} style={{ padding: "8px 14px", background: C.green, color: "white", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Approve and Publish</button>
 <button onClick={() => act(item.preview_id, "edit")} disabled={busyId === item.preview_id} style={{ padding: "8px 14px", background: C.yellow, color: C.bg, border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Approve with Edits</button>
 <button onClick={() => act(item.preview_id, "reject")} disabled={busyId === item.preview_id} style={{ padding: "8px 14px", background: "transparent", color: C.red, border: ("1px solid " + C.red + "66"), borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Reject</button>
 </div>
 </div>
 ))}
 </div>
 )}
 </div>
 );
}

// ============================================================================
// Endpoints: GET /api/paste-queue/list, POST /api/paste-queue/mark-pasted
// ============================================================================
function PasteQueuePage() {
 const [items, setItems] = useState([]);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState("");
 const load = async () => {
 setLoading(true); setError("");
 try {
 const resp = await fetch("/api/paste-queue/list?status=pending&limit=50", { credentials: "include" });
 const data = await resp.json();
 if (data.ok) setItems(data.items || []);
 else setError(data.error || ("HTTP " + resp.status));
 } catch (ex) { setError(ex.message || "Network error"); }
 setLoading(false);
 };
 useEffect(() => { load(); }, []);
 const markPasted = async (id) => {
 try {
 const resp = await fetch("/api/paste-queue/mark-pasted", {
 method: "POST", credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ task_id: id }),
 });
 if (!resp.ok) {
 const j = await resp.json().catch(() => ({}));
 setError(j.error || `Mark-pasted failed: HTTP ${resp.status}`);
 return;
 }
 load();
 } catch (ex) { setError(ex.message); }
 };
 const copy = (text) => { if (navigator.clipboard) navigator.clipboard.writeText(text || ""); };
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard title="Paste Queue" subtitle="Manual paste-and-confirm tasks (G2 reviews, Capterra responses)">
 <div style={{ color: C.textDim, fontSize: 13, lineHeight: 1.6 }}>
 Toutmark drafts the response, the customer approves, and Owner pastes into the platform manually. This is the canonical fallback when headless automation is not available or when policy says manual-only.
 </div>
 </SectionCard>
 {error ? <div style={{ padding: 12, background: (C.red + "18"), border: ("1px solid " + C.red + "55"), borderRadius: 8, color: C.red, fontSize: 12 }}>{error}</div> : null}
 {loading ? (
 <div style={{ padding: 40, textAlign: "center", color: C.textDim, fontSize: 13 }}>Loading paste queue...</div>
 ) : items.length === 0 ? (
 <div style={{ padding: 40, textAlign: "center", color: C.textDim, fontSize: 13 }}>Paste queue is empty.</div>
 ) : (
 <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
 {items.map(item => (
 <div key={item.task_id} style={{ background: C.panel, border: ("1px solid " + C.border), borderRadius: 12, padding: 16, display: "flex", flexDirection: "column", gap: 10 }}>
 <div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>{item.platform || "—"} · {item.kind || "response"}</div>
 <div style={{ fontSize: 11, color: C.textMute, fontFamily: "ui-monospace, monospace" }}>{item.customer_id}</div>
 {item.target_url ? <a href={item.target_url} target="_blank" rel="noopener noreferrer" style={{ fontSize: 11, color: C.accent }}>Open target</a> : null}
 </div>
 <div style={{ background: C.bg, border: ("1px solid " + C.border), borderRadius: 8, padding: 12, fontSize: 12, color: C.textDim, maxHeight: 200, overflowY: "auto", whiteSpace: "pre-wrap" }}>
 {item.body || ""}
 </div>
 <div style={{ display: "flex", gap: 8 }}>
 <button onClick={() => copy(item.body)} style={{ padding: "8px 14px", background: C.accent, color: "white", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Copy Response</button>
 <button onClick={() => markPasted(item.task_id)} style={{ padding: "8px 14px", background: C.green, color: "white", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Mark Pasted</button>
 </div>
 </div>
 ))}
 </div>
 )}
 </div>
 );
}

// ============================================================================
// INSTAGRAM + DISCORD POSTING — REMOVED 2026-05-21
// Toutmark posts on LinkedIn, Reddit, X only. No Instagram or Discord posting.
// Video Assembler now renders videos for LinkedIn + X (native video support).
// ============================================================================


// ============================================================================
// CITATION REPORTS PAGE — monthly PDF list + download (Scale tier)
// Endpoint: GET /api/citation-report?list=true
// ============================================================================
function CitationReportsPage() {
 const [items, setItems] = useState([]);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState("");
 useEffect(() => {
 (async () => {
 setLoading(true); setError("");
 try {
 const resp = await fetch("/api/citation-report/list?limit=100", { credentials: "include" });
 const data = await resp.json();
 if (data.ok) setItems(data.reports || data.items || []);
 else setError(data.error || ("HTTP " + resp.status));
 } catch (ex) { setError(ex.message || "Network error"); }
 setLoading(false);
 })();
 }, []);
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard title="Citation Reports" subtitle="Monthly AI citation reports (Scale tier) — auto-generated 1st of month">
 <div style={{ color: C.textDim, fontSize: 13, lineHeight: 1.6 }}>
 Each report shows where the customer appeared in ChatGPT / Claude / Perplexity / Gemini answers across tracked queries, citation count delta vs prior month, and top-cited pages. PDFs are generated by the Cloudflare Browser Rendering binding and stored at citation_report:&lt;customer_id&gt;:&lt;yearMonth&gt;.
 </div>
 </SectionCard>
 {error ? <div style={{ padding: 12, background: (C.red + "18"), border: ("1px solid " + C.red + "55"), borderRadius: 8, color: C.red, fontSize: 12 }}>{error}</div> : null}
 {loading ? (
 <div style={{ padding: 40, textAlign: "center", color: C.textDim, fontSize: 13 }}>Loading reports...</div>
 ) : items.length === 0 ? (
 <div style={{ padding: 40, textAlign: "center", color: C.textDim, fontSize: 13 }}>No reports yet — first one generates 1st of next month.</div>
 ) : (
 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 {items.map((r, i) => (
 <div key={i} style={{ background: C.panel, border: ("1px solid " + C.border), borderRadius: 10, padding: 14, display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>{r.year_month || r.month}</div>
 <div style={{ fontSize: 12, color: C.textDim, fontFamily: "ui-monospace, monospace" }}>{r.customer_id}</div>
 <div style={{ fontSize: 11, color: C.textMute }}>{r.citations_count != null ? (r.citations_count + " citations") : ""} {r.delta != null ? (" (delta " + (r.delta >= 0 ? "+" : "") + r.delta + ")") : ""}</div>
 <div style={{ marginLeft: "auto" }}>
 {r.pdf_url || r.url ? (
 <a href={r.pdf_url || r.url} target="_blank" rel="noopener noreferrer" style={{ padding: "6px 12px", background: C.accent, color: "white", borderRadius: 6, fontSize: 12, fontWeight: 700, textDecoration: "none" }}>Download PDF</a>
 ) : (
 <span style={{ fontSize: 11, color: C.textMute }}>—</span>
 )}
 </div>
 </div>
 ))}
 </div>
 )}
 </div>
 );
}

// ============================================================================
// BILLING PAGE — plan info + Cancel Plan (POST /api/billing/cancel)
// ============================================================================
// REWORKED 2026-05-19: the old "Cancel Plan" button + customer-facing plan blurb
// belonged on the CUSTOMER dashboard, not the OWNER Command Center. River runs
// Toutmark — River doesn't subscribe to Toutmark. This page now shows the
// Owner-relevant view: MRR + per-tier breakdown + new signups + churn + failed
// payments + upcoming renewals + Stripe balance. Pulled from /api/crm/dashboard,
// /api/crm/customers, /api/spend/balances. Auto-refreshes every minute.
function BillingPage() {
 const [stats, setStats] = useState(null);
 const [customers, setCustomers] = useState([]);
 const [error, setError] = useState("");
 const [loading, setLoading] = useState(true);
 const [lastFetchAt, setLastFetchAt] = useState(null);

 const load = React.useCallback(async () => {
  setLoading(true); setError("");
  const controller = new AbortController();
  const t = setTimeout(() => controller.abort(), 12000);
  try {
   const [bal, crm, cust] = await Promise.all([
    fetch("/api/spend/balances", { credentials: "include", signal: controller.signal, cache: "no-store" }).then(r => r.json()).catch(() => ({})),
    fetch("/api/crm/dashboard", { credentials: "include", signal: controller.signal, cache: "no-store" }).then(r => r.json()).catch(() => ({})),
    fetch("/api/crm/customers?limit=500", { credentials: "include", signal: controller.signal, cache: "no-store" }).then(r => r.json()).catch(() => ({})),
   ]);
   clearTimeout(t);
   setStats({ bal: bal || {}, crm: crm || {} });
   setCustomers((cust && (cust.customers || cust.items)) || []);
   setLastFetchAt(new Date());
  } catch (e) {
   clearTimeout(t);
   setError(e.name === "AbortError" ? "Request timed out after 12s" : (e.message || "Network error"));
  }
  setLoading(false);
 }, []);

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

 // Roll up per-tier MRR + churn + new signups from the customer list.
 const rollup = React.useMemo(() => {
  const r = {
   mrr: 0, mrrByTier: { starter: 0, growth: 0, scale: 0 },
   active: 0, trialing: 0, canceled: 0, paused: 0,
   countsByTier: { starter: 0, growth: 0, scale: 0 },
   new7d: 0, new30d: 0, churned30d: 0,
   failedPayments: 0,
   upcomingRenewals: [], // next 30 days
   recentSignups: [], // last 30 days
   recentCancels: [], // last 30 days
  };
  const now = Date.now();
  const d7 = now - 7 * 86400 * 1000;
  const d30 = now - 30 * 86400 * 1000;
  const d30Future = now + 30 * 86400 * 1000;
  const tierPrice = { starter: 79, growth: 149, scale: 229 };
  for (const c of (customers || [])) {
   const tier = (c.plan_tier || c.tier || "").toLowerCase();
   const status = (c.status || c.subscription_status || "").toLowerCase();
   const created = new Date(c.created_at || c.signup_at || c.created || 0).getTime();
   const canceled = new Date(c.canceled_at || c.cancellation_at || 0).getTime();
   const renews = new Date(c.next_renewal_at || c.current_period_end || 0).getTime();
   if (status === "active") {
    r.active++;
    if (tierPrice[tier]) {
     r.mrr += tierPrice[tier];
     r.mrrByTier[tier] += tierPrice[tier];
     r.countsByTier[tier]++;
    }
   } else if (status === "trialing" || status === "trial") {
    r.trialing++;
   } else if (status === "canceled" || status === "cancelled") {
    r.canceled++;
    if (canceled > d30) { r.churned30d++; r.recentCancels.push(c); }
   } else if (status === "paused" || status === "past_due") {
    r.paused++;
    r.failedPayments++;
   }
   if (created > d7) r.new7d++;
   if (created > d30) { r.new30d++; r.recentSignups.push(c); }
   if (renews > now && renews < d30Future && status === "active") {
    r.upcomingRenewals.push({ ...c, renews_at: renews });
   }
  }
  // CRM dashboard may also report MRR independently — prefer the rollup if we
  // have customer data, else fall back to whatever /api/crm/dashboard returned.
  if (r.active === 0 && stats && stats.crm) {
   r.mrr = stats.crm.stats?.mrr || stats.crm.mrr || 0;
   r.active = stats.crm.stats?.active_count || stats.crm.active_count || 0;
   r.trialing = stats.crm.stats?.trialing_count || stats.crm.trialing_count || 0;
  }
  r.recentSignups.sort((a, b) => new Date(b.created_at || b.signup_at || 0) - new Date(a.created_at || a.signup_at || 0));
  r.recentCancels.sort((a, b) => new Date(b.canceled_at || 0) - new Date(a.canceled_at || 0));
  r.upcomingRenewals.sort((a, b) => a.renews_at - b.renews_at);
  return r;
 }, [customers, stats]);

 const stripeBalance = (stats && stats.bal && stats.bal.stripe && stats.bal.stripe.balance) || null;
 const churnRatePct = (rollup.active + rollup.churned30d) > 0
  ? ((rollup.churned30d / (rollup.active + rollup.churned30d)) * 100).toFixed(1)
  : null;
 const fmtMoney = (n) => `$${Number(n || 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
 const fmtDate = (ts) => ts ? new Date(ts).toLocaleDateString(undefined, { month: "short", day: "numeric" }) : "—";

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 12 }}>
   <div>
    <h2 style={{ margin: 0, fontSize: 18, color: C.text }}>Customer Subscriptions & Revenue</h2>
    <div style={{ fontSize: 12, color: C.textDim, marginTop: 2 }}>Owner view: MRR, per-tier breakdown, new signups, churn, failed payments, upcoming renewals. Auto-refreshes every minute.</div>
   </div>
   <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
    {lastFetchAt && <span style={{ fontSize: 11, color: C.textMute }}>Last: {lastFetchAt.toLocaleTimeString()}</span>}
    <button onClick={load} disabled={loading} style={{ padding: "6px 12px", background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, fontSize: 12, cursor: loading ? "wait" : "pointer" }}>{loading ? "Loading…" : "↻ Refresh"}</button>
   </div>
  </div>

  {error && <div style={{ padding: 10, background: C.red + "18", color: C.red, borderRadius: 6, fontSize: 12 }}>Error: {error}</div>}

  {/* PRIMARY MRR + REVENUE TILES */}
  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(170px, 1fr))", gap: 10 }}>
   <StatTile label="MRR" value={fmtMoney(rollup.mrr)} color={C.green} sub={`${rollup.active} active subs`} />
   <StatTile label="ARR run-rate" value={fmtMoney(rollup.mrr * 12)} color={C.green} sub="MRR × 12" />
   <StatTile label="New (7d)" value={String(rollup.new7d)} color={C.accent} sub={`${rollup.new30d} in last 30d`} />
   <StatTile label="Churn (30d)" value={String(rollup.churned30d)} color={rollup.churned30d > 0 ? C.red : C.green} sub={churnRatePct != null ? `${churnRatePct}% rate` : "—"} />
   <StatTile label="Trialing" value={String(rollup.trialing)} color={C.yellow} sub="in trial period" />
   <StatTile label="Failed payments" value={String(rollup.failedPayments)} color={rollup.failedPayments > 0 ? C.red : C.green} sub="past_due / paused" />
   <StatTile label="Stripe balance" value={stripeBalance != null ? fmtMoney(stripeBalance) : "—"} color={C.blue} sub="settled funds" />
  </div>

  {/* PER-TIER MRR BREAKDOWN */}
  <SectionCard title="Per-tier breakdown" subtitle={`Total active: ${rollup.active} · MRR: ${fmtMoney(rollup.mrr)}`}>
   <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
    {[
     { key: "starter", label: "Starter", price: 79 },
     { key: "growth", label: "Growth", price: 149 },
     { key: "scale", label: "Scale", price: 229 },
    ].map(t => {
     const count = rollup.countsByTier[t.key] || 0;
     const mrr = rollup.mrrByTier[t.key] || 0;
     const sharePct = rollup.mrr > 0 ? Math.round((mrr / rollup.mrr) * 100) : 0;
     return (
      <div key={t.key} style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 8, padding: 14 }}>
       <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
        <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>{t.label}</div>
        <div style={{ fontSize: 11, color: C.textDim }}>${t.price}/mo</div>
       </div>
       <div style={{ fontSize: 22, color: C.green, fontWeight: 700, margin: "6px 0" }}>{fmtMoney(mrr)}</div>
       <div style={{ fontSize: 11, color: C.textDim }}>{count} customer{count === 1 ? "" : "s"} · {sharePct}% of MRR</div>
       {/* Mini bar showing share of total MRR */}
       <div style={{ marginTop: 8, height: 4, background: C.bg, borderRadius: 2, overflow: "hidden" }}>
        <div style={{ width: `${sharePct}%`, height: "100%", background: C.green, transition: "width 300ms" }} />
       </div>
      </div>
     );
    })}
   </div>
  </SectionCard>

  {/* TWO-COLUMN: RECENT SIGNUPS + UPCOMING RENEWALS */}
  <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
   <SectionCard title="Recent signups (last 30d)" subtitle={`${rollup.recentSignups.length} total`}>
    {rollup.recentSignups.length === 0 ? (
     <div style={{ color: C.textDim, padding: 16, textAlign: "center", fontSize: 13 }}>No new signups yet.</div>
    ) : (
     <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
      {rollup.recentSignups.slice(0, 8).map((c, i) => (
       <div key={i} style={{ display: "flex", justifyContent: "space-between", padding: "8px 10px", background: C.bg, borderRadius: 6, fontSize: 12 }}>
        <div style={{ color: C.text, fontWeight: 600 }}>{c.brand_name || c.brand || c.business_name || c.primary_email || c.email || "—"}</div>
        <div style={{ color: C.textDim }}>{(c.plan_tier || c.tier || "—")} · {fmtDate(new Date(c.created_at || c.signup_at).getTime())}</div>
       </div>
      ))}
     </div>
    )}
   </SectionCard>
   <SectionCard title="Upcoming renewals (next 30d)" subtitle={`${rollup.upcomingRenewals.length} total`}>
    {rollup.upcomingRenewals.length === 0 ? (
     <div style={{ color: C.textDim, padding: 16, textAlign: "center", fontSize: 13 }}>No renewals scheduled.</div>
    ) : (
     <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
      {rollup.upcomingRenewals.slice(0, 8).map((c, i) => (
       <div key={i} style={{ display: "flex", justifyContent: "space-between", padding: "8px 10px", background: C.bg, borderRadius: 6, fontSize: 12 }}>
        <div style={{ color: C.text, fontWeight: 600 }}>{c.brand_name || c.brand || c.business_name || c.primary_email || c.email || "—"}</div>
        <div style={{ color: C.yellow }}>{fmtDate(c.renews_at)}</div>
       </div>
      ))}
     </div>
    )}
   </SectionCard>
  </div>

  {/* RECENT CANCELS */}
  {rollup.recentCancels.length > 0 && (
   <SectionCard title="Recent cancellations (last 30d)" subtitle={`${rollup.recentCancels.length} customer${rollup.recentCancels.length === 1 ? "" : "s"} churned`}>
    <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
     {rollup.recentCancels.slice(0, 8).map((c, i) => (
      <div key={i} style={{ display: "flex", justifyContent: "space-between", padding: "8px 10px", background: C.bg, borderRadius: 6, fontSize: 12, borderLeft: `2px solid ${C.red}` }}>
       <div>
        <div style={{ color: C.text, fontWeight: 600 }}>{c.brand_name || c.brand || c.business_name || c.primary_email || c.email || "—"}</div>
        {c.cancellation_reason && <div style={{ color: C.textDim, fontSize: 11, marginTop: 2 }}>Reason: {c.cancellation_reason}</div>}
       </div>
       <div style={{ color: C.red }}>{fmtDate(new Date(c.canceled_at || c.cancellation_at).getTime())}</div>
      </div>
     ))}
    </div>
   </SectionCard>
  )}

  {/* PRICE CATALOG (for reference, not edit — actual prices live in Stripe + pricing.html) */}
  <SectionCard title="Toutmark price catalog (reference)" subtitle="Pricing lives in Stripe + toutmark.com/pricing.html — change there, not here.">
   <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
    <div style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 8, padding: 14 }}>
     <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>Starter</div>
     <div style={{ fontSize: 22, color: C.green, fontWeight: 700, margin: "6px 0" }}>$79<span style={{ fontSize: 12, color: C.textDim }}>/mo</span></div>
     <div style={{ fontSize: 11, color: C.textDim }}>Entry tier — see CEO soul / BUSINESS-PLAN.md for feature matrix.</div>
    </div>
    <div style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 8, padding: 14 }}>
     <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>Growth</div>
     <div style={{ fontSize: 22, color: C.green, fontWeight: 700, margin: "6px 0" }}>$149<span style={{ fontSize: 12, color: C.textDim }}>/mo</span></div>
     <div style={{ fontSize: 11, color: C.textDim }}>Mid tier.</div>
    </div>
    <div style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 8, padding: 14 }}>
     <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>Scale</div>
     <div style={{ fontSize: 22, color: C.green, fontWeight: 700, margin: "6px 0" }}>$229<span style={{ fontSize: 12, color: C.textDim }}>/mo</span></div>
     <div style={{ fontSize: 11, color: C.textDim }}>Top tier with Slack support.</div>
    </div>
   </div>
  </SectionCard>

  <div style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 8, padding: 12, fontSize: 12, color: C.textDim, lineHeight: 1.5 }}>
   <strong style={{ color: C.text }}>About this page:</strong> shows Toutmark's revenue from customers — not River's own bill. The previous "Cancel Plan" button was customer-facing copy mistakenly placed on the Owner dashboard; that lives on the customer dashboard (/app/billing) now. For Toutmark's own infrastructure costs (Anthropic, Cloudflare, Mailrelay, etc.) see <strong style={{ color: C.text }}>Spend & Balances</strong>.
  </div>
 </div>
 );
}

// ============================================================================
// PLATFORM STATUS PAGE — warmup state + recent posts per platform
// Endpoints: /api/warmup-state?platform=... /api/content-memory?platform=...
// ============================================================================
function PlatformStatusPage() {
 const [platform, setPlatform] = useState("x");
 const [warmup, setWarmup] = useState(null);
 const [posts, setPosts] = useState([]);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState("");
 const [lastFetchAt, setLastFetchAt] = useState(null);
 const PLATFORMS = ["x", "linkedin", "reddit"];

 // Hardened 2026-05-19: 12s AbortController timeout per request so the page
 // can't get stuck on "Loading..." when the worker is slow or returns non-JSON.
 const fetchJsonWithTimeout = async (url, ms = 12000) => {
  const controller = new AbortController();
  const t = setTimeout(() => controller.abort(), ms);
  try {
   const r = await fetch(url, { credentials: "include", signal: controller.signal, cache: "no-store" });
   clearTimeout(t);
   const text = await r.text();
   let json = {};
   try { json = text ? JSON.parse(text) : {}; } catch { json = { _nonJson: true, _status: r.status }; }
   if (!r.ok) json._error = json.error || json.message || `HTTP ${r.status}`;
   return json;
  } catch (e) {
   clearTimeout(t);
   return { _error: e.name === "AbortError" ? "Request timed out after 12s" : (e.message || "Network error") };
  }
 };

 const load = React.useCallback(async () => {
  setLoading(true); setError("");
  const w = await fetchJsonWithTimeout("/api/warmup-state?platform=" + platform);
  const p = await fetchJsonWithTimeout("/api/content-memory?platform=" + platform + "&limit=20");
  const errs = [w._error, p._error].filter(Boolean);
  if (errs.length === 2) setError(errs.join(" · ")); // only show error if BOTH calls failed
  setWarmup(w.state || (w._error ? null : w) || null);
  setPosts(p.items || p.posts || []);
  setLastFetchAt(new Date());
  setLoading(false);
 }, [platform]);

 useEffect(() => { load(); }, [load]);
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard title="Platform Status" subtitle="Warmup progress + recent posts per platform">
 <div style={{ color: C.textDim, fontSize: 13, lineHeight: 1.6, marginBottom: 12 }}>
 Live Phase cold-start cadence: first 3 manual approves, then semi-auto with repetition slowdown. Switch platform to inspect.
 </div>
 <div style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }}>
 {PLATFORMS.map(p => (
 <button key={p} onClick={() => setPlatform(p)} style={{ padding: "6px 12px", background: platform === p ? C.accent : "transparent", color: platform === p ? "white" : C.textDim, border: ("1px solid " + (platform === p ? C.accent : C.border)), borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>{p}</button>
 ))}
 <button onClick={load} disabled={loading} style={{ marginLeft: "auto", padding: "6px 12px", background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, fontSize: 12, cursor: loading ? "wait" : "pointer" }}>
 {loading ? "Loading…" : "↻ Refresh"}
 </button>
 {lastFetchAt && <span style={{ fontSize: 11, color: C.textMute }}>Last: {lastFetchAt.toLocaleTimeString()}</span>}
 </div>
 </SectionCard>
 {error ? <div style={{ padding: 12, background: (C.red + "18"), color: C.red, borderRadius: 8, fontSize: 12 }}>{error}</div> : null}
 {warmup ? (() => {
 // Worker emits: { policy, warmup_count, warmup_required, unlocked_at, slowdown_multiplier, slowdown_reason, dedup_stats_7d }
 // Read what's actually there; fall back to legacy field names for back-compat.
 const platformBlock = warmup[platform] || warmup; // /api/warmup-state wraps per-platform
 const warmupCount = platformBlock.warmup_count ?? warmup.approved_count ?? 0;
 const warmupReq = platformBlock.warmup_required ?? 3;
 const unlocked = platformBlock.unlocked_at != null || warmup.auto_post_unlocked === true;
 const slowdownMult = platformBlock.slowdown_multiplier ?? 1;
 const slowdownReason = platformBlock.slowdown_reason || warmup.slowdown_reason;
 const dedupStats = platformBlock.dedup_stats_7d || {};
 const dailyCap = (platformBlock.policy && platformBlock.policy.daily_cap) || warmup.daily_cap || "—";
 return (
 <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 10 }}>
 <StatTile label="Posts approved" value={warmupCount + " / " + warmupReq} color={C.green} />
 <StatTile label="Auto-post unlocked" value={unlocked ? "Yes" : "No"} color={unlocked ? C.green : C.yellow} />
 <StatTile label="Daily cap" value={String(dailyCap)} color={C.accent} />
 <StatTile label="Slowdown multiplier" value={slowdownMult + "×" + (slowdownReason ? " — " + slowdownReason : "")} color={slowdownMult > 1 ? C.yellow : C.textMute} />
 <StatTile label="Dedup rejects (7d)" value={(dedupStats.rejected || 0) + " / " + (dedupStats.total || 0)} color={C.textMute} />
 </div>
 );
 })() : null}
 <div>
 <div style={{ fontSize: 12, color: C.textMute, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 8 }}>Recent posts</div>
 {loading ? <div style={{ padding: 30, textAlign: "center", color: C.textDim, fontSize: 13 }}>Loading...</div>
 : posts.length === 0 ? <div style={{ padding: 30, textAlign: "center", color: C.textDim, fontSize: 13 }}>No posts on {platform} yet.</div>
 : (
 <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
 {posts.map((post, i) => (
 <div key={i} style={{ background: C.panel, border: ("1px solid " + C.border), borderRadius: 8, padding: 10, fontSize: 12, color: C.textDim }}>
 <div style={{ fontSize: 10, color: C.textMute, marginBottom: 4 }}>{post.posted_at ? new Date(post.posted_at).toLocaleString() : ""}</div>
 <div style={{ whiteSpace: "pre-wrap" }}>{(post.text || post.body || "").slice(0, 280)}</div>
 </div>
 ))}
 </div>
 )}
 </div>
 </div>
 );
}

// ============================================================================
// CUSTOMER FLOWS PAGE — quick links to customer-facing /app/*.html surfaces
// ============================================================================
function CustomerFlowsPage() {
 const flows = [
 { id: "spokesperson", title: "Spokesperson Attestation", url: "/app/spokesperson-attestation.html", desc: "HARO opt-in with attribution-rules attestation. Customer signs once before Marketer can submit any HARO pitch on their behalf." },
 { id: "fullauto", title: "Full Auto Approve Confirmation", url: "/app/full-auto-approve-confirm.html", desc: "Customer enables full-auto mode (skip per-item review). Liability shifts to customer per Terms section 2a." },
 { id: "connect", title: "Connect Site", url: "/app/connect.html", desc: "Platform-routed 1-min install (Cloudflare/Vercel/Netlify/Shopify/WordPress/Webflow/Ghost) — OAuth or token-paste depending on platform." },
 ];
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard title="Customer-facing Flows" subtitle="Pages customers visit during onboarding or mode changes">
 <div style={{ color: C.textDim, fontSize: 13, lineHeight: 1.6 }}>
 These are public, customer-authenticated pages that handle the legal-attestation moments in the lifecycle. They live outside the Owner Command Center but are linked here so you can preview them.
 </div>
 </SectionCard>
 <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
 {flows.map(f => (
 <a key={f.id} href={f.url} target="_blank" rel="noopener noreferrer" style={{ background: C.panel, border: ("1px solid " + C.border), borderRadius: 10, padding: 14, textDecoration: "none", color: C.text, display: "flex", flexDirection: "column", gap: 6 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>{f.title}</div>
 <div style={{ fontSize: 11, color: C.accent, fontFamily: "ui-monospace, monospace" }}>{f.url}</div>
 </div>
 <div style={{ fontSize: 12, color: C.textDim, lineHeight: 1.5 }}>{f.desc}</div>
 </a>
 ))}
 </div>
 </div>
 );
}

// (Live Phase Roadmap removed — feature retired)

// ============================================================================
// BROWSER SESSIONS PAGE — Owner manages SHARED support@toutmark.com cookies
// per platform PLUS per-customer claim metadata. Spec: OPENCLAW-OWNER-AUDIT § 9.
//
// Access model: Toutmark logs in ONCE per platform as support@toutmark.com.
// Customers add support@ as a teammate during onboarding (Claim Kit pattern).
// Cookies live at the platform level and are shared across all customers.
//
// Endpoints:
// GET /api/admin/browser-sessions
// POST /api/admin/browser-sessions/upsert { customer_id, platform, customer_name?, customer_url?, claim_status?, claim_invited_at?, notes? }
// POST /api/admin/browser-sessions/test { customer_id, platform }
// POST /api/admin/browser-sessions/pause { customer_id, platform, paused }
// POST /api/admin/browser-sessions/delete { customer_id, platform }
// POST /api/admin/platform-creds/refresh { platform, cookies, expires_at? }
// POST /api/admin/platform-creds/test { platform }
// GET /api/admin/selector-status[?platform=...]
// POST /api/admin/selector-test { platform }
// ============================================================================
// ============================================================================
// Regulator Queue (task #506, 2026-05-14)
// Owner reviews regulator-notice queue populated by the daily monitor cron.
// Approve → ruleset deltas apply to per-category banned phrases + dashboard
// alerts go out to affected customers. Dismiss → row archived, no action.
// ============================================================================
function RegulatorQueuePage() {
 const [items, setItems] = useState([]);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState("");
 const [busyId, setBusyId] = useState("");
 const [statusFilter, setStatusFilter] = useState("pending_owner_review");
 const [editing, setEditing] = useState(null); // current item being edited
 const [phrasesAdd, setPhrasesAdd] = useState("");
 const [phrasesRemove, setPhrasesRemove] = useState("");
 const [toast, setToast] = useState("");

 const load = async () => {
  setLoading(true); setError("");
  try {
   const r = await fetch("/api/admin/regulator-queue?status=" + encodeURIComponent(statusFilter), { credentials: "include" });
   if (!r.ok) { setError("HTTP " + r.status); setItems([]); return; }
   const j = await r.json();
   setItems(j.items || []);
  } catch (e) {
   setError(String(e.message || e));
  } finally {
   setLoading(false);
  }
 };
 useEffect(() => { load(); }, [statusFilter]);

 const runTickNow = async () => {
  setToast("Running monitor tick…");
  try {
   const r = await fetch("/api/admin/regulator-monitor/tick", { method: "POST", credentials: "include" });
   const j = await r.json();
   // Worker returns { ok, results: { scanned, classified, queued, skipped_empty, results: [...] } }
   const summary = j.results || {};
   const queued = summary.queued ?? (Array.isArray(summary) ? summary.length : 0);
   const scanned = summary.scanned ?? "?";
   const skipped = summary.skipped_empty ?? "?";
   setToast(`Tick complete: ${queued} queued for your review · ${scanned} scanned · ${skipped} skipped (no new bans needed).`);
   load();
  } catch (e) {
   setToast("Tick failed: " + (e.message || e));
  }
  setTimeout(() => setToast(""), 6000);
 };

 const approveItem = async () => {
  if (!editing) return;
  setBusyId(editing.id);
  const addList = phrasesAdd.split("\n").map(s => s.trim()).filter(Boolean);
  const removeList = phrasesRemove.split("\n").map(s => s.trim()).filter(Boolean);
  const deltas = {};
  deltas[editing.category] = {};
  if (addList.length) deltas[editing.category].banned_phrases_add = addList;
  if (removeList.length) deltas[editing.category].banned_phrases_remove = removeList;
  try {
   const r = await fetch("/api/admin/regulator-queue/approve", {
    method: "POST", credentials: "include",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ id: editing.id, ruleset_deltas: deltas }),
   });
   if (!r.ok) { setToast("Approve failed: HTTP " + r.status); return; }
   const j = await r.json();
   setToast(`Approved. Applied: ${(j.applied || []).map(a => `${a.category} v${a.version} (${a.total_phrases} total)`).join(", ")}`);
   setEditing(null); setPhrasesAdd(""); setPhrasesRemove("");
   load();
  } catch (e) {
   setToast("Approve failed: " + (e.message || e));
  } finally {
   setBusyId(""); setTimeout(() => setToast(""), 5000);
  }
 };

 const dismissItem = async (id) => {
  const reason = window.prompt("Dismiss reason (e.g., 'enforcement against single firm, not a new rule'):");
  if (!reason) return;
  setBusyId(id);
  try {
   const r = await fetch("/api/admin/regulator-queue/dismiss", {
    method: "POST", credentials: "include",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ id, reason }),
   });
   if (!r.ok) { setToast("Dismiss failed: HTTP " + r.status); return; }
   setToast("Dismissed.");
   load();
  } catch (e) {
   setToast("Dismiss failed: " + (e.message || e));
  } finally {
   setBusyId(""); setTimeout(() => setToast(""), 3000);
  }
 };

 const openEdit = (item) => {
  setEditing(item);
  setPhrasesAdd((item.classification?.suggested_new_bans || []).join("\n"));
  setPhrasesRemove("");
 };

 const fmtSla = (it) => {
  if (!it.sla_deadline_ts) return "";
  const ms = it.sla_deadline_ts - Date.now();
  if (ms < 0) return "OVERDUE";
  const days = Math.floor(ms / (1000 * 60 * 60 * 24));
  return `${days}d remaining`;
 };

 return (
  <div style={{ padding: 24, color: C.text }}>
   <h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 12 }}>⚖ Regulator Queue</h2>
   <p style={{ color: C.textDim, fontSize: 13, marginBottom: 16 }}>
    Daily monitor cron polls FINRA Notices, SEC Marketing Rule guidance, FTC press, HHS HIPAA feeds. New notices are classified by Sonnet and queued here for your review. Approve to push ruleset deltas to per-category banned-phrase tables; dismiss to archive without action. Affected customers get a dashboard banner + email alert when you approve.
   </p>
   <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 12, flexWrap: "wrap" }}>
    <select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} style={{ background: C.panel, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, padding: "6px 10px" }}>
     <option value="pending_owner_review">Pending review</option>
     <option value="approved">Approved</option>
     <option value="dismissed">Dismissed</option>
    </select>
    <button onClick={load} style={{ padding: "6px 12px", background: C.panel, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, cursor: "pointer" }}>Refresh</button>
    <button onClick={runTickNow} style={{ padding: "6px 12px", background: C.accent, color: "#fff", border: "none", borderRadius: 6, cursor: "pointer", fontWeight: 600 }}>Run monitor now</button>
    {toast ? <span style={{ marginLeft: 12, color: C.green || "#22c55e", fontSize: 13 }}>{toast}</span> : null}
   </div>
   {loading ? <div style={{ color: C.textDim }}>Loading…</div> : null}
   {error ? <div style={{ color: C.red || "#ef4444", marginBottom: 12 }}>Error: {error}</div> : null}
   {!loading && items.length === 0 ? (
    <div style={{ color: C.textDim, padding: 20, textAlign: "center", border: `1px dashed ${C.border}`, borderRadius: 8 }}>No items in the {statusFilter.replace(/_/g, " ")} state.</div>
   ) : null}
   <div style={{ display: "grid", gap: 12 }}>
    {items.map(it => (
     <div key={it.id} style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 8, padding: 16 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12, marginBottom: 8, flexWrap: "wrap" }}>
       <div style={{ flex: 1, minWidth: 280 }}>
        <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap", marginBottom: 4 }}>
         <span style={{ background: C.bg, color: C.textDim, fontSize: 11, padding: "2px 8px", borderRadius: 999, textTransform: "uppercase", letterSpacing: 0.5 }}>{it.source}</span>
         <span style={{ background: C.bg, color: C.textDim, fontSize: 11, padding: "2px 8px", borderRadius: 999 }}>{it.category}</span>
         {it.sla_deadline_ts ? <span style={{ background: fmtSla(it) === "OVERDUE" ? "#dc2626" : C.bg, color: fmtSla(it) === "OVERDUE" ? "#fff" : C.textDim, fontSize: 11, padding: "2px 8px", borderRadius: 999 }}>SLA: {fmtSla(it)}</span> : null}
        </div>
        <div style={{ fontWeight: 600, fontSize: 15, marginBottom: 4 }}>{it.title}</div>
        <div style={{ color: C.textDim, fontSize: 12, marginBottom: 6 }}>Published: {it.published}</div>
        {it.classification?.summary ? <div style={{ fontSize: 13, marginBottom: 6 }}><strong>Classifier:</strong> {it.classification.summary}</div> : null}
        {(it.classification?.suggested_new_bans || []).length > 0 ? (
         <div style={{ fontSize: 12, color: C.textDim, marginTop: 4 }}>
          <strong>Suggested new bans:</strong> {(it.classification.suggested_new_bans || []).join(", ")}
         </div>
        ) : null}
       </div>
      </div>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
       <a href={it.source_url} target="_blank" rel="noopener" style={{ padding: "6px 12px", background: C.bg, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, textDecoration: "none", fontSize: 13 }}>Open source ↗</a>
       {it.status === "pending_owner_review" ? (
        <>
         <button onClick={() => openEdit(it)} disabled={busyId === it.id} style={{ padding: "6px 12px", background: C.accent, color: "#fff", border: "none", borderRadius: 6, cursor: "pointer", fontSize: 13, fontWeight: 600 }}>Approve + apply ruleset</button>
         <button onClick={() => dismissItem(it.id)} disabled={busyId === it.id} style={{ padding: "6px 12px", background: C.bg, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, cursor: "pointer", fontSize: 13 }}>Dismiss</button>
        </>
       ) : null}
       {it.status === "approved" && it.applied ? (
        <span style={{ color: C.green || "#22c55e", fontSize: 12 }}>Applied: {it.applied.map(a => `${a.category} v${a.version}`).join(", ")}</span>
       ) : null}
       {it.status === "dismissed" ? <span style={{ color: C.textDim, fontSize: 12 }}>Dismissed: {it.dismissed_reason}</span> : null}
      </div>
     </div>
    ))}
   </div>
   {editing ? (
    <div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "rgba(0,0,0,0.7)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000 }} onClick={() => setEditing(null)}>
     <div onClick={e => e.stopPropagation()} style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 24, maxWidth: 600, width: "92%", maxHeight: "88vh", overflow: "auto" }}>
      <h3 style={{ marginBottom: 12 }}>Apply ruleset delta to: {editing.category}</h3>
      <div style={{ marginBottom: 10, color: C.textDim, fontSize: 13 }}>{editing.title}</div>
      <label style={{ display: "block", marginBottom: 6, fontSize: 13, fontWeight: 600 }}>Banned phrases to ADD (one per line)</label>
      <textarea value={phrasesAdd} onChange={e => setPhrasesAdd(e.target.value)} rows={6} style={{ width: "100%", background: C.bg, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, padding: 8, fontFamily: "monospace", fontSize: 12, marginBottom: 12 }}/>
      <label style={{ display: "block", marginBottom: 6, fontSize: 13, fontWeight: 600 }}>Banned phrases to REMOVE (one per line, optional)</label>
      <textarea value={phrasesRemove} onChange={e => setPhrasesRemove(e.target.value)} rows={3} style={{ width: "100%", background: C.bg, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, padding: 8, fontFamily: "monospace", fontSize: 12, marginBottom: 12 }}/>
      <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
       <button onClick={() => setEditing(null)} style={{ padding: "8px 14px", background: C.bg, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, cursor: "pointer" }}>Cancel</button>
       <button onClick={approveItem} disabled={busyId === editing.id} style={{ padding: "8px 14px", background: C.accent, color: "#fff", border: "none", borderRadius: 6, cursor: "pointer", fontWeight: 600 }}>{busyId === editing.id ? "Applying…" : "Approve + notify customers"}</button>
      </div>
      <div style={{ marginTop: 12, fontSize: 11, color: C.textDim }}>Approving writes the new ruleset version, scans every customer with industry_profile = "{editing.category}", and writes a dashboard banner + email alert to each one.</div>
     </div>
    </div>
   ) : null}
  </div>
 );
}

function ReferralsPage() {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState("");
 const [days, setDays] = useState(30);
 const [refreshTick, setRefreshTick] = useState(0);
 useEffect(() => {
  let cancelled = false;
  setLoading(true); setError("");
  fetch(`/api/audit/referral-stats?days=${days}`, { credentials: "include" })
   .then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j)))
   .then(j => { if (!cancelled) { setData(j); setLoading(false); } })
   .catch(e => { if (!cancelled) { setError(e?.error || e?.message || "Failed to load"); setLoading(false); } });
  return () => { cancelled = true; };
 }, [days, refreshTick]);
 const totals = data?.totals || {};
 const daily = data?.daily || {};
 const allBuckets = Object.entries(totals);
 const grandTotal = allBuckets.reduce((s, [, n]) => s + n, 0);
 const families = {};
 for (const [bucket, n] of allBuckets) {
  const fam = bucket.split("/")[0] || "direct";
  families[fam] = (families[fam] || 0) + n;
 }
 const familyEntries = Object.entries(families).sort((a, b) => b[1] - a[1]);
 const top10 = allBuckets.slice(0, 10);
 const topBucket = allBuckets[0] || ["—", 0];
 const dailyKeys = Object.keys(daily).sort();
 const maxDaily = Math.max(1, ...dailyKeys.map(k => Object.values(daily[k] || {}).reduce((s, n) => s + n, 0)));
 const famColor = { reddit: C.orange, email: C.lime, linkedin: C.cyan, x: C.violet, haro: C.yellow, newsletter: C.accent, founding: C.teal, inbox: "#ec4899", direct: C.textDim };
 const colorFor = (fam) => famColor[fam] || C.textDim;
 return React.createElement("div", { style: { padding: 20 } },
  React.createElement("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 16 } },
   React.createElement("div", null,
    React.createElement("h1", { style: { margin: 0, fontSize: 22, fontWeight: 800 } }, "Referrals — where audits come from"),
    React.createElement("div", { style: { color: C.textDim, fontSize: 13, marginTop: 4 } }, "UTM-attributed audit runs across every outbound channel. Source: /api/audit/referral-stats")
   ),
   React.createElement("div", { style: { display: "flex", gap: 8 } },
    [7, 30, 90, 180].map(d => React.createElement("button", { key: d, onClick: () => setDays(d), style: { background: days === d ? C.accent : C.panelHi, color: days === d ? "#000" : C.text, border: "none", padding: "6px 12px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" } }, `${d}d`)),
    React.createElement("button", { onClick: () => setRefreshTick(t => t + 1), style: { background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, padding: "6px 12px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" } }, "Refresh")
   )
  ),
  error && React.createElement("div", { style: { background: "#7f1d1d", color: "#fee", padding: 10, borderRadius: 6, marginBottom: 12 } }, `Error: ${error}`),
  loading && React.createElement("div", { style: { color: C.textDim, padding: 20 } }, "Loading..."),
  !loading && !error && React.createElement(React.Fragment, null,
   React.createElement("div", { style: { display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12, marginBottom: 16 } },
    React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 } },
     React.createElement("div", { style: { color: C.textDim, fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5 } }, "Total audits"),
     React.createElement("div", { style: { fontSize: 28, fontWeight: 800, color: C.accent, marginTop: 4 } }, grandTotal),
     React.createElement("div", { style: { color: C.textDim, fontSize: 12, marginTop: 4 } }, `last ${days} days`)
    ),
    React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 } },
     React.createElement("div", { style: { color: C.textDim, fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5 } }, "Top source"),
     React.createElement("div", { style: { fontSize: 18, fontWeight: 800, color: C.text, marginTop: 4 } }, topBucket[0]),
     React.createElement("div", { style: { color: C.textDim, fontSize: 12, marginTop: 4 } }, `${topBucket[1]} audits (${grandTotal ? ((topBucket[1] / grandTotal) * 100).toFixed(1) : 0}%)`)
    ),
    React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 } },
     React.createElement("div", { style: { color: C.textDim, fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5 } }, "Reddit total"),
     React.createElement("div", { style: { fontSize: 28, fontWeight: 800, color: C.orange, marginTop: 4 } }, families.reddit || 0),
     React.createElement("div", { style: { color: C.textDim, fontSize: 12, marginTop: 4 } }, "across 5 allowlisted subs")
    ),
    React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 } },
     React.createElement("div", { style: { color: C.textDim, fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5 } }, "Email total"),
     React.createElement("div", { style: { fontSize: 28, fontWeight: 800, color: C.lime, marginTop: 4 } }, families.email || 0),
     React.createElement("div", { style: { color: C.textDim, fontSize: 12, marginTop: 4 } }, "cold + auto-reply combined")
    )
   ),
   React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16, marginBottom: 16 } },
    React.createElement("div", { style: { fontWeight: 700, marginBottom: 12, color: C.text } }, "Top sources (bucket = source/detail)"),
    top10.length === 0 ? React.createElement("div", { style: { color: C.textDim, padding: 16, textAlign: "center" } }, "No audits in window yet.") :
     top10.map(([bucket, n]) => {
      const pct = grandTotal ? (n / top10[0][1]) * 100 : 0;
      const fam = bucket.split("/")[0];
      return React.createElement("div", { key: bucket, style: { marginBottom: 8 } },
       React.createElement("div", { style: { display: "flex", justifyContent: "space-between", fontSize: 12, marginBottom: 3 } },
        React.createElement("span", { style: { color: C.text, fontFamily: "ui-monospace, monospace" } }, bucket),
        React.createElement("span", { style: { color: C.textDim } }, `${n} (${grandTotal ? ((n / grandTotal) * 100).toFixed(1) : 0}%)`)
       ),
       React.createElement("div", { style: { background: C.panelHi, height: 12, borderRadius: 4, overflow: "hidden" } },
        React.createElement("div", { style: { background: colorFor(fam), height: "100%", width: `${pct}%`, transition: "width .3s" } })
       )
      );
     })
   ),
   React.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 2fr", gap: 16, marginBottom: 16 } },
    React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16 } },
     React.createElement("div", { style: { fontWeight: 700, marginBottom: 12, color: C.text } }, "By source family"),
     familyEntries.length === 0 ? React.createElement("div", { style: { color: C.textDim, fontSize: 13 } }, "-") :
      familyEntries.map(([fam, n]) => React.createElement("div", { key: fam, style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 } },
       React.createElement("div", { style: { width: 12, height: 12, borderRadius: 3, background: colorFor(fam) } }),
       React.createElement("div", { style: { flex: 1, fontSize: 13, color: C.text } }, fam),
       React.createElement("div", { style: { fontSize: 13, fontWeight: 700, color: C.text } }, n),
       React.createElement("div", { style: { fontSize: 11, color: C.textDim, width: 40, textAlign: "right" } }, `${grandTotal ? ((n / grandTotal) * 100).toFixed(0) : 0}%`)
      ))
    ),
    React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16 } },
     React.createElement("div", { style: { fontWeight: 700, marginBottom: 12, color: C.text } }, "Daily audits"),
     dailyKeys.length === 0 ? React.createElement("div", { style: { color: C.textDim, fontSize: 13 } }, "-") :
      React.createElement("div", { style: { display: "flex", alignItems: "flex-end", gap: 2, height: 140, borderBottom: `1px solid ${C.border}`, padding: "4px 0" } },
       dailyKeys.map(day => {
        const buckets = daily[day] || {};
        const dayTotal = Object.values(buckets).reduce((s, n) => s + n, 0);
        const h = dayTotal ? (dayTotal / maxDaily) * 100 : 0;
        return React.createElement("div", { key: day, title: `${day}: ${dayTotal} audits`, style: { flex: 1, minWidth: 4, display: "flex", flexDirection: "column", justifyContent: "flex-end", height: "100%" } },
         React.createElement("div", { style: { height: `${h}%`, background: dayTotal ? C.accent : "transparent", borderRadius: "2px 2px 0 0" } })
        );
       })
      )
    )
   ),
   React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16 } },
    React.createElement("div", { style: { fontWeight: 700, marginBottom: 12, color: C.text } }, `All sources (${allBuckets.length})`),
    React.createElement("table", { style: { width: "100%", fontSize: 13 } },
     React.createElement("thead", null,
      React.createElement("tr", { style: { borderBottom: `1px solid ${C.border}` } },
       ["Source", "Detail", "Audits", "% of total"].map(h => React.createElement("th", { key: h, style: { textAlign: "left", padding: 8, color: C.textDim, fontWeight: 600 } }, h))
      )
     ),
     React.createElement("tbody", null,
      allBuckets.length === 0 ? React.createElement("tr", null, React.createElement("td", { colSpan: 4, style: { padding: 16, color: C.textDim, textAlign: "center" } }, "No referral data yet.")) :
       allBuckets.map(([bucket, n]) => {
        const [src, detail] = bucket.includes("/") ? bucket.split("/", 2) : [bucket, ""];
        return React.createElement("tr", { key: bucket, style: { borderBottom: `1px solid ${C.borderSoft}` } },
         React.createElement("td", { style: { padding: 8 } },
          React.createElement("span", { style: { display: "inline-block", width: 8, height: 8, borderRadius: "50%", background: colorFor(src), marginRight: 6 } }),
          src
         ),
         React.createElement("td", { style: { padding: 8, fontFamily: "ui-monospace, monospace", color: C.textDim } }, detail || "-"),
         React.createElement("td", { style: { padding: 8, fontWeight: 700, color: C.text } }, n),
         React.createElement("td", { style: { padding: 8, color: C.textDim } }, `${grandTotal ? ((n / grandTotal) * 100).toFixed(1) : 0}%`)
        );
       })
     )
    )
   )
  )
 );
}

function DMQueuePage() {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState("");
 const [tab, setTab] = useState("linkedin");
 const [busy, setBusy] = useState({});
 const [editing, setEditing] = useState({});
 const [tick, setTick] = useState(0);
 useEffect(() => {
  let cancelled = false;
  setLoading(true); setError("");
  fetch("/api/admin/dm-queue", { credentials: "include" })
   .then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j)))
   .then(j => { if (!cancelled) { setData(j); setLoading(false); } })
   .catch(e => { if (!cancelled) { setError(e?.error || e?.message || "Failed to load"); setLoading(false); } });
  return () => { cancelled = true; };
 }, [tick]);

 const platforms = [
  { id: "linkedin", label: "LinkedIn", emoji: "in", color: C.cyan, sendMode: "Manual copy-paste (LinkedIn API blocks DM-send)" },
  { id: "x", label: "X", emoji: "x", color: C.violet, sendMode: "Auto-send via X API v2 DM endpoint" },
  { id: "reddit", label: "Reddit", emoji: "r", color: C.orange, sendMode: "Auto-send via Composio REDDIT_COMPOSE_MESSAGE" },
 ];
 const cur = platforms.find(p => p.id === tab);
 const rows = (data?.items?.[tab] || []).filter(r => r.status === "pending_owner_review");
 const counts = data?.counts?.[tab] || {};
 const cadence = data?.cadence?.[tab] || {};

 async function callAction(draftId, action, extra = {}) {
  setBusy(b => ({ ...b, [draftId]: action }));
  try {
   const r = await fetch(`/api/admin/dm-queue/${draftId}/${action}`, {
    method: "POST",
    credentials: "include",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ platform: tab, ...extra }),
   });
   const j = await r.json().catch(() => ({}));
   if (!r.ok || j?.ok === false) {
    alert(`Failed: ${j?.error || j?.send_error || r.statusText}`);
   } else if (action === "accept" && tab === "linkedin") {
    // Copy draft to clipboard for manual paste flow.
    try { await navigator.clipboard.writeText(j?.row?.draft_text || ""); } catch {}
   }
   setTick(t => t + 1);
  } catch (e) {
   alert(`Error: ${e.message || e}`);
  } finally {
   setBusy(b => { const c = { ...b }; delete c[draftId]; return c; });
   setEditing(ed => { const c = { ...ed }; delete c[draftId]; return c; });
  }
 }

 return React.createElement("div", { style: { padding: 20 } },
  React.createElement("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 16 } },
   React.createElement("div", null,
    React.createElement("h1", { style: { margin: 0, fontSize: 22, fontWeight: 800 } }, "DM queue — Owner review"),
    React.createElement("div", { style: { color: C.textDim, fontSize: 13, marginTop: 4 } }, "Low-volume, high-precision DM/PM drafts. Agents draft, Owner sends. See marketer soul § DM strategy.")
   ),
   React.createElement("button", { onClick: () => setTick(t => t + 1), style: { background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, padding: "6px 12px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" } }, "Refresh")
  ),
  error && React.createElement("div", { style: { background: "#7f1d1d", color: "#fee", padding: 10, borderRadius: 6, marginBottom: 12 } }, `Error: ${error}`),
  loading && React.createElement("div", { style: { color: C.textDim, padding: 20 } }, "Loading..."),
  !loading && !error && React.createElement(React.Fragment, null,
   // Tab bar with counts
   React.createElement("div", { style: { display: "flex", gap: 8, marginBottom: 16, borderBottom: `1px solid ${C.border}`, paddingBottom: 0 } },
    platforms.map(plat => {
     const ct = data?.counts?.[plat.id] || {};
     const total = ct.pending || 0;
     const active = tab === plat.id;
     return React.createElement("button", {
      key: plat.id,
      onClick: () => setTab(plat.id),
      style: {
       background: "transparent",
       color: active ? plat.color : C.textDim,
       border: "none",
       borderBottom: active ? `2px solid ${plat.color}` : "2px solid transparent",
       padding: "8px 14px",
       fontSize: 14,
       fontWeight: 700,
       cursor: "pointer",
       display: "flex",
       alignItems: "center",
       gap: 8,
      }
     },
      plat.label,
      total > 0 && React.createElement("span", { style: { background: plat.color, color: "#000", borderRadius: 99, fontSize: 11, padding: "1px 7px", fontWeight: 800 } }, total)
     );
    })
   ),
   // Cadence + send mode meta
   React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 8, padding: 12, marginBottom: 16, fontSize: 13, color: C.textDim } },
    React.createElement("strong", { style: { color: C.text } }, cur.label, " send mode: "),
    cur.sendMode,
    React.createElement("br"),
    React.createElement("strong", { style: { color: C.text } }, "Cadence: "),
    `${cadence.drafts_per_day_target || "?"} drafts/day target · ${cadence.sends_per_week_target || "?"} sends/week target · ${cadence.sends_per_day_ceiling || "?"}/day hard ceiling`
   ),
   // Empty state
   rows.length === 0 && React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 32, textAlign: "center", color: C.textDim } },
    React.createElement("div", { style: { fontSize: 16, color: C.text, fontWeight: 700, marginBottom: 6 } }, "No pending drafts."),
    React.createElement("div", { style: { fontSize: 13 } },
     "Marketer's daily web-search sweep populates this queue. ",
     React.createElement("br"),
     `Target: ~${cadence.drafts_per_day_target || "?"} drafts/day for ${cur.label}. If nothing surfaced today, the qualification bar wasn't met.`
    )
   ),
   // Draft cards
   rows.map(row => {
    const editing_text = editing[row.id];
    const is_editing = editing_text !== undefined;
    const b = busy[row.id];
    return React.createElement("div", { key: row.id, style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16, marginBottom: 12 } },
     // Top row: recipient + fit score
     React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 12 } },
      React.createElement("div", null,
       React.createElement("div", { style: { fontWeight: 700, fontSize: 15, color: C.text } }, row.recipient_name || row.recipient_handle || "(no recipient)"),
       row.recipient_handle && React.createElement("div", { style: { fontSize: 12, color: C.textDim, fontFamily: "ui-monospace, monospace" } }, row.recipient_handle),
       row.recipient_profile_url && React.createElement("a", { href: row.recipient_profile_url, target: "_blank", rel: "noopener", style: { fontSize: 12, color: C.cyan, textDecoration: "none" } }, "View profile →")
      ),
      React.createElement("div", { style: { textAlign: "right" } },
       React.createElement("div", { style: { background: row.fit_score >= 8 ? C.lime : row.fit_score >= 6 ? C.yellow : C.textDim, color: "#000", borderRadius: 99, padding: "2px 10px", fontSize: 12, fontWeight: 800, display: "inline-block" } }, `Fit ${row.fit_score || 0}/10`),
       React.createElement("div", { style: { fontSize: 11, color: C.textDim, marginTop: 4 } }, `${(row.criteria_met || []).length}/7 criteria`),
       React.createElement("div", { style: { fontSize: 11, color: C.textDim, marginTop: 2 } }, row.outreach_mode === "connection_request" ? "Connection request" : "DM")
      )
     ),
     // Triggering content
     row.triggering_content_summary && React.createElement("div", { style: { background: C.panelHi, borderRadius: 6, padding: 10, marginBottom: 10, fontSize: 13, color: C.text } },
      React.createElement("div", { style: { fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5, color: C.textDim, marginBottom: 4 } }, "Why we're reaching out"),
      row.triggering_content_summary,
      row.triggering_content_url && React.createElement("div", { style: { marginTop: 6 } },
       React.createElement("a", { href: row.triggering_content_url, target: "_blank", rel: "noopener", style: { fontSize: 12, color: C.cyan, textDecoration: "none" } }, "View triggering content →")
      )
     ),
     // Draft text (view or edit)
     React.createElement("div", { style: { background: "#0a0e1a", border: `1px solid ${C.border}`, borderRadius: 6, padding: 10, marginBottom: 10 } },
      React.createElement("div", { style: { fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5, color: C.textDim, marginBottom: 6 } }, "Draft"),
      is_editing
       ? React.createElement("textarea", {
          value: editing_text,
          onChange: (e) => setEditing(ed => ({ ...ed, [row.id]: e.target.value })),
          style: { width: "100%", minHeight: 100, background: "#0a0e1a", color: C.text, border: `1px solid ${C.border}`, padding: 8, borderRadius: 4, fontSize: 13, fontFamily: "inherit", boxSizing: "border-box", resize: "vertical" }
         })
       : React.createElement("div", { style: { color: C.text, fontSize: 13, whiteSpace: "pre-wrap", lineHeight: 1.45 } }, row.draft_text)
     ),
     // Agent notes
     row.agent_notes && React.createElement("div", { style: { fontSize: 12, color: C.textDim, marginBottom: 10, fontStyle: "italic" } }, "Agent notes: ", row.agent_notes),
     // Buttons
     React.createElement("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
      !is_editing && React.createElement("button", {
       onClick: () => callAction(row.id, "accept"),
       disabled: !!b,
       style: { background: cur.color, color: "#000", border: "none", padding: "8px 16px", borderRadius: 6, fontSize: 13, fontWeight: 700, cursor: b ? "wait" : "pointer", opacity: b ? 0.5 : 1 }
      }, b === "accept" ? "Sending..." : (tab === "linkedin" ? "Accept (copies + manual send)" : "Accept & send")),
      !is_editing && React.createElement("button", {
       onClick: () => setEditing(ed => ({ ...ed, [row.id]: row.draft_text })),
       disabled: !!b,
       style: { background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, padding: "8px 16px", borderRadius: 6, fontSize: 13, fontWeight: 700, cursor: "pointer" }
      }, "Edit"),
      is_editing && React.createElement("button", {
       onClick: () => callAction(row.id, "edit", { draft_text: editing_text }),
       disabled: !!b,
       style: { background: C.lime, color: "#000", border: "none", padding: "8px 16px", borderRadius: 6, fontSize: 13, fontWeight: 700, cursor: b ? "wait" : "pointer" }
      }, b === "edit" ? "Saving..." : "Save edit"),
      is_editing && React.createElement("button", {
       onClick: () => setEditing(ed => { const c = { ...ed }; delete c[row.id]; return c; }),
       style: { background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, padding: "8px 16px", borderRadius: 6, fontSize: 13, fontWeight: 700, cursor: "pointer" }
      }, "Cancel"),
      !is_editing && React.createElement("button", {
       onClick: () => { const reason = prompt("Skip reason (helps Marketer learn the pattern):"); if (reason !== null) callAction(row.id, "skip", { reason }); },
       disabled: !!b,
       style: { background: "transparent", color: C.textDim, border: `1px solid ${C.borderSoft}`, padding: "8px 16px", borderRadius: 6, fontSize: 13, cursor: "pointer" }
      }, b === "skip" ? "Skipping..." : "Skip")
     )
    );
   })
  )
 );
}

function DMCandidatesPage() {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState("");
 const [tab, setTab] = useState("linkedin");
 const [busy, setBusy] = useState({});
 const [sweepBusy, setSweepBusy] = useState(false);
 const [promoteBusy, setPromoteBusy] = useState(false);
 const [tick, setTick] = useState(0);
 useEffect(() => {
  let cancelled = false;
  setLoading(true); setError("");
  fetch("/api/admin/dm-candidates", { credentials: "include" })
   .then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j)))
   .then(j => { if (!cancelled) { setData(j); setLoading(false); } })
   .catch(e => { if (!cancelled) { setError(e?.error || e?.message || "Failed to load"); setLoading(false); } });
  return () => { cancelled = true; };
 }, [tick]);

 const platforms = [
  { id: "linkedin", label: "LinkedIn", color: C.cyan },
  { id: "x", label: "X", color: C.violet },
  { id: "reddit", label: "Reddit", color: C.orange },
 ];
 const cur = platforms.find(p => p.id === tab);
 const rows = (data?.items?.[tab] || []);

 async function callAction(leadId, action, extra = {}) {
  setBusy(b => ({ ...b, [leadId]: action }));
  try {
   const r = await fetch(`/api/admin/dm-candidates/${leadId}/${action}`, {
    method: "POST", credentials: "include",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ platform: tab, ...extra }),
   });
   const j = await r.json().catch(() => ({}));
   if (!r.ok || j?.error) alert(`Failed: ${j?.error || j?.detail || r.statusText}`);
   else if (action === "promote") alert("Draft queued. Open the DM Queue page to review.");
   setTick(t => t + 1);
  } catch (e) { alert(`Error: ${e.message || e}`); }
  finally { setBusy(b => { const c = { ...b }; delete c[leadId]; return c; }); }
 }

 async function runSweepNow() {
  if (!confirm("Run the candidate sweep now? (Hits Perplexity for ~12 web searches; takes ~30s.)")) return;
  setSweepBusy(true);
  try {
   const r = await fetch("/api/cron/dm-candidate-sweep", { method: "POST", credentials: "include" });
   const j = await r.json().catch(() => ({}));
   alert(j.ok ? `Sweep complete — added ${j.added || 0} candidates.` : `Failed: ${j.error || r.statusText}`);
   setTick(t => t + 1);
  } catch (e) { alert(`Error: ${e.message || e}`); }
  finally { setSweepBusy(false); }
 }
 async function runPromoteNow() {
  if (!confirm("Promote top candidates to DM drafts now? (Drafts via Sonnet 4.6 + Pangram; queues to /admin/dm-queue.)")) return;
  setPromoteBusy(true);
  try {
   const r = await fetch("/api/cron/dm-draft-promote", { method: "POST", credentials: "include" });
   const j = await r.json().catch(() => ({}));
   alert(j.ok ? `Promote complete — ${j.promoted || 0} drafts queued.` : `Failed: ${j.error || r.statusText}`);
   setTick(t => t + 1);
  } catch (e) { alert(`Error: ${e.message || e}`); }
  finally { setPromoteBusy(false); }
 }

 return React.createElement("div", { style: { padding: 20 } },
  React.createElement("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 16 } },
   React.createElement("div", null,
    React.createElement("h1", { style: { margin: 0, fontSize: 22, fontWeight: 800 } }, "DM candidate pool"),
    React.createElement("div", { style: { color: C.textDim, fontSize: 13, marginTop: 4 } }, "Web-search-sourced leads scored against the 3-of-7 rule. Top candidates auto-promote to DM drafts at 12:00 LA daily — or promote manually below.")
   ),
   React.createElement("div", { style: { display: "flex", gap: 8 } },
    React.createElement("button", { onClick: runSweepNow, disabled: sweepBusy, style: { background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, padding: "6px 12px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: sweepBusy ? "wait" : "pointer" } }, sweepBusy ? "Sweeping..." : "Run sweep now"),
    React.createElement("button", { onClick: runPromoteNow, disabled: promoteBusy, style: { background: C.accent, color: "#000", border: "none", padding: "6px 12px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: promoteBusy ? "wait" : "pointer" } }, promoteBusy ? "Promoting..." : "Run promote now"),
    React.createElement("button", { onClick: () => setTick(t => t + 1), style: { background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, padding: "6px 12px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" } }, "↻")
   )
  ),
  error && React.createElement("div", { style: { background: "#7f1d1d", color: "#fee", padding: 10, borderRadius: 6, marginBottom: 12 } }, `Error: ${error}`),
  loading && React.createElement("div", { style: { color: C.textDim, padding: 20 } }, "Loading..."),
  !loading && !error && React.createElement(React.Fragment, null,
   React.createElement("div", { style: { display: "flex", gap: 8, marginBottom: 16, borderBottom: `1px solid ${C.border}` } },
    platforms.map(plat => {
     const ct = data?.counts?.[plat.id] || {};
     const active = tab === plat.id;
     return React.createElement("button", {
      key: plat.id, onClick: () => setTab(plat.id),
      style: { background: "transparent", color: active ? plat.color : C.textDim, border: "none", borderBottom: active ? `2px solid ${plat.color}` : "2px solid transparent", padding: "8px 14px", fontSize: 14, fontWeight: 700, cursor: "pointer", display: "flex", alignItems: "center", gap: 8 }
     }, plat.label,
      ct.eligible > 0 && React.createElement("span", { style: { background: plat.color, color: "#000", borderRadius: 99, fontSize: 11, padding: "1px 7px", fontWeight: 800 } }, ct.eligible)
     );
    })
   ),
   React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 8, padding: 12, marginBottom: 16, fontSize: 12, color: C.textDim, display: "flex", gap: 20, flexWrap: "wrap" } },
    React.createElement("div", null, React.createElement("strong", { style: { color: C.text } }, "Total: "), data?.counts?.[tab]?.total || 0),
    React.createElement("div", null, React.createElement("strong", { style: { color: C.lime } }, "Eligible (fit ≥ 7): "), data?.counts?.[tab]?.eligible || 0),
    React.createElement("div", null, React.createElement("strong", { style: { color: C.textDim } }, "Already drafted: "), data?.counts?.[tab]?.drafted || 0)
   ),
   rows.length === 0 && React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 32, textAlign: "center", color: C.textDim } },
    React.createElement("div", { style: { fontSize: 16, color: C.text, fontWeight: 700, marginBottom: 6 } }, "No candidates yet."),
    React.createElement("div", { style: { fontSize: 13 } }, "Click ", React.createElement("strong", null, "Run sweep now"), " above to populate the pool, or wait for the 06:00 LA daily cron.")
   ),
   rows.map(c => {
    const b = busy[c.id];
    const isDrafted = !!c.drafted_at_ts;
    return React.createElement("div", { key: c.id, style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16, marginBottom: 12, opacity: isDrafted ? 0.55 : 1 } },
     React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 10 } },
      React.createElement("div", null,
       React.createElement("div", { style: { fontWeight: 700, fontSize: 15, color: C.text } }, c.name),
       c.profile_url && React.createElement("a", { href: c.profile_url, target: "_blank", rel: "noopener", style: { fontSize: 12, color: C.cyan, textDecoration: "none" } }, "View profile →")
      ),
      React.createElement("div", { style: { textAlign: "right" } },
       React.createElement("div", { style: { background: c.fit_score >= 8 ? C.lime : c.fit_score >= 7 ? C.yellow : C.textDim, color: "#000", borderRadius: 99, padding: "2px 10px", fontSize: 12, fontWeight: 800, display: "inline-block" } }, `Fit ${c.fit_score || 0}/10`),
       React.createElement("div", { style: { fontSize: 11, color: C.textDim, marginTop: 4 } }, `${(c.criteria_met || []).join(", ") || "no criteria"}`),
       isDrafted && React.createElement("div", { style: { fontSize: 11, color: C.lime, marginTop: 4 } }, `✓ drafted ${c.drafted_promote_status || ""}`)
      )
     ),
     c.triggering_content_summary && React.createElement("div", { style: { background: C.panelHi, borderRadius: 6, padding: 10, marginBottom: 10, fontSize: 13, color: C.text } },
      React.createElement("div", { style: { fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5, color: C.textDim, marginBottom: 4 } }, "Triggering content"),
      c.triggering_content_summary,
      c.triggering_content_url && React.createElement("div", { style: { marginTop: 6 } }, React.createElement("a", { href: c.triggering_content_url, target: "_blank", rel: "noopener", style: { fontSize: 12, color: C.cyan, textDecoration: "none" } }, "View →"))
     ),
     c.source_query && React.createElement("div", { style: { fontSize: 11, color: C.textDim, marginBottom: 10, fontFamily: "ui-monospace, monospace" } }, "Source query: ", c.source_query),
     React.createElement("div", { style: { display: "flex", gap: 8 } },
      !isDrafted && React.createElement("button", {
       onClick: () => callAction(c.id, "promote"),
       disabled: !!b || c.fit_score < 7,
       style: { background: cur.color, color: "#000", border: "none", padding: "8px 16px", borderRadius: 6, fontSize: 13, fontWeight: 700, cursor: (b || c.fit_score < 7) ? "not-allowed" : "pointer", opacity: (b || c.fit_score < 7) ? 0.5 : 1 }
      }, b === "promote" ? "Drafting..." : (c.fit_score < 7 ? "Fit < 7 (skip)" : "Promote to draft")),
      React.createElement("button", {
       onClick: () => { if (confirm("Remove from candidate pool?")) callAction(c.id, "remove"); },
       disabled: !!b,
       style: { background: "transparent", color: C.textDim, border: `1px solid ${C.borderSoft}`, padding: "8px 16px", borderRadius: 6, fontSize: 13, cursor: "pointer" }
      }, b === "remove" ? "Removing..." : "Remove")
     )
    );
   })
  )
 );
}

function BrowserSessionsPage() {
 const [rows, setRows] = useState([]);
 const [creds, setCreds] = useState([]);
 const [summary, setSummary] = useState(null);
 const [selectorStatus, setSelectorStatus] = useState(null);
 const [composioHealth, setComposioHealth] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState("");
 const [busyKey, setBusyKey] = useState("");
 const [toast, setToast] = useState("");
 const [filter, setFilter] = useState("all");
 const [refreshOpen, setRefreshOpen] = useState(null); // platform string
 const [refreshCookies, setRefreshCookies] = useState("");
 const [refreshExpiry, setRefreshExpiry] = useState("");
 const [claimOpen, setClaimOpen] = useState(null); // row object
 const [claimUrl, setClaimUrl] = useState("");
 const [claimStatus, setClaimStatus] = useState("verified");
 const [bulkRefreshOpen, setBulkRefreshOpen] = useState(false);
 const [bulkRefreshStep, setBulkRefreshStep] = useState(0);
 const [bulkCookies, setBulkCookies] = useState("");
 const [bulkExpiry, setBulkExpiry] = useState("");

 const load = async () => {
 setLoading(true); setError("");
 try {
 const resp = await fetch("/api/admin/browser-sessions", { credentials: "include" });
 const data = await resp.json();
 if (data && data.ok) {
 setRows(data.sessions || []);
 setCreds(data.platform_creds || []);
 setSummary(data.summary || null);
 } else if (resp.ok) {
 setRows([]); setCreds([]); setSummary(null);
 } else {
 setError((data && data.error) || ("HTTP " + resp.status));
 }
 try {
 const sel = await fetch("/api/admin/selector-status", { credentials: "include" }).then(r => r.json()).catch(() => null);
 if (sel) setSelectorStatus(sel);
 } catch {}
 try {
 const ch = await fetch("/api/admin/composio-health", { credentials: "include" }).then(r => r.json()).catch(() => null);
 if (ch) setComposioHealth(ch);
 } catch {}
 } catch (ex) { setError(ex.message || "Network error"); }
 setLoading(false);
 };
 useEffect(() => { load(); }, []);

 const callAction = async (path, body, key) => {
 setBusyKey(key); setToast(""); setError("");
 try {
 const resp = await fetch(path, {
 method: "POST",
 credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify(body || {}),
 });
 const data = await resp.json().catch(() => ({}));
 if (!resp.ok || data.ok === false) {
 setError(data.error || ("HTTP " + resp.status));
 } else {
 setToast(data.message || "Done.");
 await load();
 }
 } catch (ex) { setError(ex.message); }
 setBusyKey("");
 };

 const credsStatusColor = (s) => {
 if (s === "healthy") return C.green;
 if (s === "expiring") return C.yellow;
 if (s === "expired") return C.red;
 if (s === "blocked") return C.orange;
 if (s === "missing") return C.textMute;
 return C.textMute;
 };
 const credsStatusLabel = (s) => {
 if (s === "healthy") return "🟢 Healthy";
 if (s === "expiring") return "🟡 Expiring";
 if (s === "expired") return "🔴 Expired";
 if (s === "blocked") return "🟠 Blocked";
 if (s === "missing") return "⚪ Not set";
 return s || "—";
 };

 const rowStatusColor = (s) => {
 if (s === "ok") return C.green;
 if (s === "needs_claim") return C.yellow;
 if (s === "blocked") return C.red;
 if (s === "paused") return C.textMute;
 return C.textMute;
 };
 const rowStatusLabel = (s) => {
 if (s === "ok") return "🟢 OK";
 if (s === "needs_claim") return "🟡 Needs claim";
 if (s === "blocked") return "🔴 Blocked";
 if (s === "paused") return "⏸ Paused";
 return s || "—";
 };
 const claimStatusLabel = (s) => {
 if (s === "verified") return "✅ Verified";
 if (s === "invited") return "✉️ Invited";
 if (s === "lost") return "❌ Lost access";
 return "⏳ Unsent";
 };

 const platformLoginUrl = (platform) => {
 const p = String(platform || "").toLowerCase();
 if (p === "g2") return "https://www.g2.com/users/sign_in";
 if (p === "capterra") return "https://www.capterra.com/sign-in";
 return "https://www.google.com/search?q=" + encodeURIComponent(p + " login");
 };

 const filtered = rows.filter(r => filter === "all" ? true : r.status === filter);

 const openRefresh = (platform) => {
 setRefreshOpen(platform); setRefreshCookies(""); setRefreshExpiry("");
 };
 const submitRefresh = async () => {
 if (!refreshOpen) return;
 if (!refreshCookies.trim()) { setError("Paste cookies first."); return; }
 await callAction("/api/admin/platform-creds/refresh", {
 platform: refreshOpen,
 cookies: refreshCookies.trim(),
 expires_at: refreshExpiry || null,
 }, "refresh:" + refreshOpen);
 setRefreshOpen(null);
 };

 const openClaim = (row) => {
 setClaimOpen(row);
 setClaimUrl(row.customer_url || "");
 setClaimStatus(row.claim_status || "verified");
 };
 const submitClaim = async () => {
 if (!claimOpen) return;
 await callAction("/api/admin/browser-sessions/upsert", {
 customer_id: claimOpen.customer_id,
 platform: claimOpen.platform,
 customer_name: claimOpen.customer_name,
 customer_url: claimUrl || null,
 claim_status: claimStatus,
 claim_at: claimStatus === "verified" ? new Date().toISOString() : undefined,
 }, "claim:" + claimOpen.customer_id + ":" + claimOpen.platform);
 setClaimOpen(null);
 };

 const sum = summary || {};

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard
 title="Browser Sessions"
 subtitle="Shared support@toutmark.com login cookies + per-customer claim status"
 right={<button onClick={load} disabled={loading} style={{ padding: "6px 12px", background: "transparent", color: C.textDim, border: ("1px solid " + C.border), borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>{loading ? "Loading..." : "Refresh"}</button>}
 >
 <div style={{ color: C.textDim, fontSize: 13, lineHeight: 1.6 }}>
 Toutmark owns ONE account per platform (<span style={{ fontFamily: "ui-monospace, monospace", color: C.text }}>support@toutmark.com</span>). Customers add support@ as a teammate on their G2/Capterra vendor dashboard during onboarding — no credentials change hands. You log in <em>once</em> per platform with Cookie-Editor, paste the cookies here, and they replay for every customer that's added support@ as a teammate.
 </div>
 </SectionCard>

 {toast ? <div style={{ padding: 10, background: (C.green + "15"), color: C.green, borderRadius: 8, fontSize: 12 }}>{toast}</div> : null}
 {error ? <div style={{ padding: 12, background: (C.red + "18"), border: ("1px solid " + C.red + "55"), borderRadius: 8, color: C.red, fontSize: 12 }}>{error}</div> : null}

 {/* Bulk refresh wizard launcher */}
 {(() => {
 const expiring = creds.filter(c => c.status === "expiring" || c.status === "expired" || c.status === "blocked");
 if (expiring.length === 0) return null;
 return (
 <div style={{ background: `${C.yellow}10`, border: `1px solid ${C.yellow}55`, borderRadius: 10, padding: 14, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
 <div>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.yellow }}>🛠 Bulk Refresh available — {expiring.length} platform{expiring.length === 1 ? "" : "s"} need attention</div>
 <div style={{ fontSize: 11, color: C.textDim, marginTop: 4 }}>Step through each platform in order: open login → paste cookies → next. Saves ~30 min vs one-at-a-time.</div>
 </div>
 <button onClick={() => setBulkRefreshOpen(true)} style={{ padding: "8px 16px", background: C.yellow, color: "#000", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Start bulk refresh →</button>
 </div>
 );
 })()}

 {/* Platform cookie cards (shared) */}
 <div>
 <div style={{ fontSize: 12, color: C.textMute, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 8 }}>Platform cookies — shared support@toutmark.com</div>
 <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: 10 }}>
 {creds.map(c => {
 const busy = busyKey === ("refresh:" + c.platform) || busyKey === ("test-creds:" + c.platform);
 return (
 <div key={c.platform} style={{ background: C.panel, border: ("1px solid " + C.border), borderRadius: 10, padding: 14, display: "flex", flexDirection: "column", gap: 8 }}>
 <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
 <div style={{ fontSize: 14, fontWeight: 800, color: C.text, textTransform: "capitalize" }}>{c.platform}</div>
 <div style={{ fontSize: 11, fontWeight: 700, color: credsStatusColor(c.status) }}>{credsStatusLabel(c.status)}</div>
 </div>
 <div style={{ fontSize: 11, color: C.textDim, display: "flex", flexDirection: "column", gap: 2 }}>
 <div>Days left: <span style={{ color: c.days_until_expiry != null && c.days_until_expiry <= 3 ? C.yellow : C.text }}>{c.days_until_expiry != null ? c.days_until_expiry : "—"}</span></div>
 <div>Last refreshed: {c.last_auth_at ? new Date(c.last_auth_at).toLocaleDateString() : "never"}</div>
 <div>Last test: {c.last_test_at ? new Date(c.last_test_at).toLocaleDateString() : "—"} {c.fails_24h ? <span style={{ color: C.red }}>· {c.fails_24h} fail{c.fails_24h === 1 ? "" : "s"} 24h</span> : null}</div>
 </div>
 <div style={{ display: "flex", gap: 6, marginTop: 4, flexWrap: "wrap" }}>
 <a href={platformLoginUrl(c.platform)} target="_blank" rel="noopener noreferrer" style={{ padding: "6px 10px", background: "transparent", color: C.accent, border: ("1px solid " + C.border), borderRadius: 4, fontSize: 11, fontWeight: 700, textDecoration: "none" }}>Open login</a>
 <button onClick={() => openRefresh(c.platform)} disabled={busy} style={{ padding: "6px 10px", background: C.green, color: "white", border: "none", borderRadius: 4, fontSize: 11, fontWeight: 700, cursor: "pointer" }}>Refresh cookies</button>
 <button onClick={() => callAction("/api/admin/platform-creds/test", { platform: c.platform }, "test-creds:" + c.platform)} disabled={busy || !c.last_auth_at} style={{ padding: "6px 10px", background: "transparent", color: C.textDim, border: ("1px solid " + C.border), borderRadius: 4, fontSize: 11, fontWeight: 700, cursor: "pointer", opacity: !c.last_auth_at ? 0.5 : 1 }}>Test</button>
 </div>
 </div>
 );
 })}
 </div>
 </div>

 {/* KPI tiles for customer rows */}
 <div>
 <div style={{ fontSize: 12, color: C.textMute, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 8 }}>Customer × platform rows</div>
 <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))", gap: 10 }}>
 <StatTile label="Total" value={sum.customers_total != null ? sum.customers_total : "—"} color={C.accent} />
 <StatTile label="OK" value={sum.customers_ok != null ? sum.customers_ok : "—"} color={C.green} />
 <StatTile label="Needs claim" value={sum.customers_needs_claim != null ? sum.customers_needs_claim : "—"} color={C.yellow} />
 <StatTile label="Blocked" value={sum.customers_blocked != null ? sum.customers_blocked : "—"} color={C.red} />
 <StatTile label="Paused" value={sum.customers_paused != null ? sum.customers_paused : "—"} color={C.textMute} />
 <StatTile label="7d post success" value={sum.success_rate_7d != null ? (sum.success_rate_7d + "%") : "—"} color={C.accent} />
 <StatTile label="Selector breaks" value={sum.selector_breaks != null ? sum.selector_breaks : "—"} color={(sum.selector_breaks || 0) > 0 ? C.red : C.textMute} />
 </div>
 </div>

 {/* Filter pills */}
 <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
 {[
 { id: "all", label: "All" },
 { id: "ok", label: "OK" },
 { id: "needs_claim", label: "Needs claim" },
 { id: "blocked", label: "Blocked" },
 { id: "paused", label: "Paused" },
 ].map(f => (
 <button key={f.id} onClick={() => setFilter(f.id)} style={{ padding: "6px 12px", background: filter === f.id ? C.accent : "transparent", color: filter === f.id ? "white" : C.textDim, border: ("1px solid " + (filter === f.id ? C.accent : C.border)), borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>{f.label}</button>
 ))}
 </div>

 {/* Customer table */}
 {loading ? (
 <div style={{ padding: 40, textAlign: "center", color: C.textDim, fontSize: 13 }}>Loading customers...</div>
 ) : filtered.length === 0 ? (
 <div style={{ padding: 40, textAlign: "center", color: C.textDim, fontSize: 13 }}>
 {rows.length === 0 ? "No customer rows yet — once a Scale-tier customer connects G2 or Capterra, it lands here." : "No customers match this filter."}
 </div>
 ) : (
 <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
 <div style={{ display: "grid", gridTemplateColumns: "1.4fr 0.7fr 1fr 1.4fr 0.9fr 0.8fr 1.6fr", gap: 10, padding: "8px 12px", fontSize: 11, color: C.textMute, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6 }}>
 <div>Customer</div>
 <div>Platform</div>
 <div>Claim</div>
 <div>Listing URL</div>
 <div>Last post</div>
 <div>Fails 24h/7d</div>
 <div>Actions</div>
 </div>
 {filtered.map(r => {
 const k = (r.customer_id || "?") + ":" + (r.platform || "?");
 const busy = busyKey.endsWith(":" + k);
 return (
 <div key={k} style={{ display: "grid", gridTemplateColumns: "1.4fr 0.7fr 1fr 1.4fr 0.9fr 0.8fr 1.6fr", gap: 10, padding: "10px 12px", background: C.panel, border: ("1px solid " + C.border), borderRadius: 8, fontSize: 12, alignItems: "center" }}>
 <div>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>{r.customer_name || r.customer_id || "—"}</div>
 <div style={{ fontSize: 10, color: C.textMute, fontFamily: "ui-monospace, monospace" }}>{r.customer_id}</div>
 <div style={{ fontSize: 10, color: rowStatusColor(r.status), fontWeight: 700, marginTop: 2 }}>{rowStatusLabel(r.status)}</div>
 </div>
 <div style={{ color: C.textDim, textTransform: "capitalize" }}>{r.platform || "—"}</div>
 <div style={{ color: r.claim_status === "verified" ? C.green : (r.claim_status === "invited" ? C.yellow : (r.claim_status === "lost" ? C.red : C.textMute)), fontWeight: 700 }}>{claimStatusLabel(r.claim_status)}</div>
 <div style={{ color: C.textDim, fontSize: 11, fontFamily: "ui-monospace, monospace", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{r.customer_url ? (
 <a href={r.customer_url} target="_blank" rel="noopener noreferrer" style={{ color: C.accent, textDecoration: "none" }}>{r.customer_url.replace(/^https?:\/\//, "").slice(0, 40)}</a>
 ) : "—"}</div>
 <div style={{ color: C.textDim, fontSize: 11 }}>{r.last_post_at ? new Date(r.last_post_at).toLocaleDateString() : "—"}</div>
 <div style={{ color: ((r.fails_24h || 0) > 0 ? C.red : C.textDim), fontFamily: "ui-monospace, monospace" }}>{(r.fails_24h || 0)}/{(r.fails_7d || 0)}</div>
 <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
 <button onClick={() => openClaim(r)} disabled={busy} style={{ padding: "4px 8px", background: r.claim_status === "verified" ? "transparent" : C.yellow, color: r.claim_status === "verified" ? C.textDim : C.bg, border: ("1px solid " + (r.claim_status === "verified" ? C.border : C.yellow)), borderRadius: 4, fontSize: 10, fontWeight: 700, cursor: "pointer" }}>{r.claim_status === "verified" ? "Edit claim" : "Mark verified"}</button>
 <button onClick={() => callAction("/api/admin/browser-sessions/test", { customer_id: r.customer_id, platform: r.platform }, "test:" + k)} disabled={busy} style={{ padding: "4px 8px", background: "transparent", color: C.textDim, border: ("1px solid " + C.border), borderRadius: 4, fontSize: 10, fontWeight: 700, cursor: "pointer" }}>Test post</button>
 <button onClick={() => callAction("/api/admin/browser-sessions/pause", { customer_id: r.customer_id, platform: r.platform, paused: !r.paused }, "pause:" + k)} disabled={busy} style={{ padding: "4px 8px", background: r.paused ? C.yellow : "transparent", color: r.paused ? C.bg : C.yellow, border: ("1px solid " + C.yellow), borderRadius: 4, fontSize: 10, fontWeight: 700, cursor: "pointer" }}>{r.paused ? "Resume" : "Pause"}</button>
 {r.status === "blocked" ? (
 <button onClick={() => callAction("/api/admin/selector-test", { platform: r.platform }, "sel:" + k)} disabled={busy} style={{ padding: "4px 8px", background: "transparent", color: C.orange, border: ("1px solid " + C.orange), borderRadius: 4, fontSize: 10, fontWeight: 700, cursor: "pointer" }}>Selector check</button>
 ) : null}
 </div>
 </div>
 );
 })}
 </div>
 )}

 {/* Selector-break monitor */}
 <SectionCard title="Selector-break monitor" subtitle="Nightly cron tests platform DOM selectors. Breaks block automation until Webmaster updates them.">
 {selectorStatus && Array.isArray(selectorStatus.platforms) && selectorStatus.platforms.length > 0 ? (
 <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
 {selectorStatus.platforms.map((p, i) => (
 <div key={i} style={{ background: C.bg, border: ("1px solid " + C.border), borderRadius: 8, padding: 10, fontSize: 12, display: "flex", alignItems: "center", gap: 10 }}>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text, textTransform: "capitalize", minWidth: 80 }}>{p.platform}</div>
 <div style={{ color: p.ok === false ? C.red : (p.ok === true ? C.green : C.textMute), fontWeight: 700 }}>{p.ok === false ? "❌ Break" : (p.ok === true ? "✅ OK" : "⚪ Untested")}</div>
 <div style={{ color: C.textDim, fontSize: 11 }}>{p.last_run ? ("Last run: " + new Date(p.last_run).toLocaleString()) : ""}</div>
 {p.ok === false && p.broken_selector ? <div style={{ color: C.red, fontSize: 11, fontFamily: "ui-monospace, monospace" }}>{p.broken_selector}</div> : null}
 <button onClick={() => callAction("/api/admin/selector-test", { platform: p.platform }, "sel:" + p.platform)} style={{ marginLeft: "auto", padding: "4px 10px", background: "transparent", color: C.accent, border: ("1px solid " + C.border), borderRadius: 4, fontSize: 11, fontWeight: 700, cursor: "pointer" }}>Re-run</button>
 </div>
 ))}
 </div>
 ) : (
 <div style={{ color: C.textDim, fontSize: 12 }}>No selector-test runs yet. Cron fires nightly 03:00 UTC.</div>
 )}
 </SectionCard>

 {/* Refresh shared cookies modal */}
 {refreshOpen ? (
 <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.7)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 110 }}>
 <div style={{ background: C.panelHi, border: ("1px solid " + C.border), borderRadius: 12, padding: 24, maxWidth: 640, width: "90%" }}>
 <div style={{ fontSize: 16, fontWeight: 800, color: C.text, marginBottom: 4, textTransform: "capitalize" }}>Refresh {refreshOpen} cookies (shared)</div>
 <div style={{ fontSize: 12, color: C.textDim, marginBottom: 14, lineHeight: 1.6 }}>
 <strong style={{ color: C.text }}>Workflow:</strong>
 <ol style={{ paddingLeft: 18, marginTop: 6 }}>
 <li>Open <a href={platformLoginUrl(refreshOpen)} target="_blank" rel="noopener noreferrer" style={{ color: C.accent }}>{refreshOpen} login</a> in a fresh tab</li>
 <li>Sign in as <span style={{ fontFamily: "ui-monospace, monospace", color: C.text }}>support@toutmark.com</span> (creds in your password manager)</li>
 <li>Navigate to a vendor dashboard page so cookies are scoped correctly</li>
 <li>Click the <strong>Cookie-Editor</strong> extension → <strong>Export → Export as JSON</strong></li>
 <li>Paste the JSON below. These cookies apply to <em>every</em> customer that's added support@ as a teammate.</li>
 </ol>
 </div>
 <textarea
 value={refreshCookies}
 onChange={e => setRefreshCookies(e.target.value)}
 placeholder='[{"name":"session","value":"...","domain":".g2.com","path":"/","expirationDate":1234567890}]'
 style={{ width: "100%", minHeight: 160, padding: 10, background: C.bg, border: ("1px solid " + C.border), borderRadius: 8, color: C.text, fontFamily: "ui-monospace, monospace", fontSize: 11, marginBottom: 10 }}
 />
 <div style={{ display: "flex", gap: 10, alignItems: "center", marginBottom: 14 }}>
 <label style={{ fontSize: 11, color: C.textDim, minWidth: 130 }}>Expires (ISO date, optional):</label>
 <input
 value={refreshExpiry}
 onChange={e => setRefreshExpiry(e.target.value)}
 placeholder="2026-06-01"
 style={{ flex: 1, padding: "6px 10px", background: C.bg, border: ("1px solid " + C.border), borderRadius: 6, color: C.text, fontFamily: "ui-monospace, monospace", fontSize: 11 }}
 />
 </div>
 <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
 <button onClick={() => setRefreshOpen(null)} style={{ padding: "8px 14px", background: "transparent", color: C.textDim, border: ("1px solid " + C.border), borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Cancel</button>
 <button onClick={submitRefresh} disabled={!refreshCookies.trim() || busyKey.startsWith("refresh:")} style={{ padding: "8px 14px", background: C.green, color: "white", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer", opacity: (!refreshCookies.trim() || busyKey.startsWith("refresh:")) ? 0.5 : 1 }}>{busyKey.startsWith("refresh:") ? "Saving..." : "Save cookies"}</button>
 </div>
 </div>
 </div>
 ) : null}

 {/* Claim status modal */}
 {claimOpen ? (
 <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.7)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 110 }}>
 <div style={{ background: C.panelHi, border: ("1px solid " + C.border), borderRadius: 12, padding: 24, maxWidth: 540, width: "90%" }}>
 <div style={{ fontSize: 15, fontWeight: 800, color: C.text, marginBottom: 4 }}>Update claim — {claimOpen.customer_name || claimOpen.customer_id} · {claimOpen.platform}</div>
 <div style={{ fontSize: 11, color: C.textDim, marginBottom: 14, fontFamily: "ui-monospace, monospace" }}>{claimOpen.customer_id}</div>
 <div style={{ display: "flex", flexDirection: "column", gap: 12, marginBottom: 16 }}>
 <div>
 <label style={{ fontSize: 11, color: C.textMute, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4, display: "block" }}>Listing URL on {claimOpen.platform}</label>
 <input
 value={claimUrl}
 onChange={e => setClaimUrl(e.target.value)}
 placeholder="https://www.g2.com/products/acme-crm/reviews"
 style={{ width: "100%", padding: "8px 10px", background: C.bg, border: ("1px solid " + C.border), borderRadius: 6, color: C.text, fontFamily: "ui-monospace, monospace", fontSize: 12 }}
 />
 </div>
 <div>
 <label style={{ fontSize: 11, color: C.textMute, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4, display: "block" }}>Claim status</label>
 <select
 value={claimStatus}
 onChange={e => setClaimStatus(e.target.value)}
 style={{ width: "100%", padding: "8px 10px", background: C.bg, border: ("1px solid " + C.border), borderRadius: 6, color: C.text, fontSize: 12 }}
 >
 <option value="unsent">Unsent — Claim Kit not yet emailed</option>
 <option value="invited">Invited — customer added support@ as teammate</option>
 <option value="verified">Verified — confirmed teammate access works</option>
 <option value="lost">Lost — customer revoked access</option>
 </select>
 </div>
 </div>
 <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
 <button onClick={() => setClaimOpen(null)} style={{ padding: "8px 14px", background: "transparent", color: C.textDim, border: ("1px solid " + C.border), borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Cancel</button>
 <button onClick={submitClaim} disabled={busyKey.startsWith("claim:")} style={{ padding: "8px 14px", background: C.green, color: "white", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer", opacity: busyKey.startsWith("claim:") ? 0.5 : 1 }}>{busyKey.startsWith("claim:") ? "Saving..." : "Save"}</button>
 </div>
 </div>
 </div>
 ) : null}

 {/* Composio integration health panel */}
 {composioHealth && Array.isArray(composioHealth.integrations) && composioHealth.integrations.length > 0 ? (
 <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 }}>
 <div style={{ fontSize: 12, color: C.textMute, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 10 }}>Composio integration health</div>
 <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: 8 }}>
 {composioHealth.integrations.map(intg => {
 const color = intg.status === "healthy" ? C.green : intg.status === "error" ? C.red : C.textMute;
 const label = intg.status === "healthy" ? "🟢 Healthy" : intg.status === "error" ? "🔴 Error" : "⚪ Not configured";
 return (
 <div key={intg.id} style={{ background: C.panelHi, borderRadius: 6, padding: 10 }}>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text, textTransform: "capitalize" }}>{intg.id}</div>
 <div style={{ fontSize: 11, fontWeight: 700, color, marginTop: 2 }}>{label}</div>
 {intg.error ? <div style={{ fontSize: 10, color: C.textDim, marginTop: 4, wordBreak: "break-word" }}>{intg.error.slice(0, 100)}</div> : null}
 </div>
 );
 })}
 </div>
 <div style={{ fontSize: 10, color: C.textMute, marginTop: 8 }}>Cached 5 min · last check {new Date(composioHealth.generated_at).toLocaleTimeString()}</div>
 </div>
 ) : null}

 {/* Bulk refresh wizard modal */}
 {bulkRefreshOpen ? (() => {
 const needsRefresh = creds.filter(c => c.status === "expiring" || c.status === "expired" || c.status === "blocked");
 const current = needsRefresh[bulkRefreshStep];
 const done = bulkRefreshStep >= needsRefresh.length;
 return (
 <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.7)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: 20 }} onClick={(e) => { if (e.target === e.currentTarget) { setBulkRefreshOpen(false); setBulkRefreshStep(0); setBulkCookies(""); setBulkExpiry(""); } }}>
 <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 12, padding: 24, maxWidth: 640, width: "100%", maxHeight: "90vh", overflow: "auto" }}>
 <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
 <div style={{ fontSize: 18, fontWeight: 800, color: C.text }}>🛠 Bulk refresh — step {Math.min(bulkRefreshStep + 1, needsRefresh.length)} of {needsRefresh.length}</div>
 <button onClick={() => { setBulkRefreshOpen(false); setBulkRefreshStep(0); setBulkCookies(""); setBulkExpiry(""); }} style={{ background: "transparent", border: "none", color: C.textDim, fontSize: 20, cursor: "pointer" }}>×</button>
 </div>
 {done ? (
 <div style={{ textAlign: "center", padding: "30px 20px" }}>
 <div style={{ fontSize: 40 }}>✅</div>
 <div style={{ fontSize: 16, fontWeight: 700, color: C.green, margin: "12px 0" }}>All {needsRefresh.length} session{needsRefresh.length === 1 ? "" : "s"} refreshed.</div>
 <button onClick={() => { setBulkRefreshOpen(false); setBulkRefreshStep(0); setBulkCookies(""); setBulkExpiry(""); }} style={{ padding: "10px 20px", background: C.accent, color: "white", border: "none", borderRadius: 6, fontSize: 13, fontWeight: 700, cursor: "pointer" }}>Done</button>
 </div>
 ) : (
 <>
 <div style={{ background: C.panelHi, borderRadius: 8, padding: 14, marginBottom: 14 }}>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text, textTransform: "capitalize", marginBottom: 8 }}>{current.platform} <span style={{ fontWeight: 400, color: credsStatusColor(current.status), marginLeft: 8 }}>· {credsStatusLabel(current.status)}</span></div>
 <ol style={{ margin: 0, paddingLeft: 18, fontSize: 12, color: C.textDim, lineHeight: 1.7 }}>
 <li><a href={platformLoginUrl(current.platform)} target="_blank" rel="noopener noreferrer" style={{ color: C.accent }}>Open {current.platform} login</a> in a new tab</li>
 <li>Sign in as <code style={{ background: C.panel, padding: "1px 4px", borderRadius: 3 }}>support@toutmark.com</code></li>
 <li>Open Cookie-Editor extension → Export → Copy</li>
 <li>Paste the cookie blob below + click Next</li>
 </ol>
 </div>
 <div style={{ marginBottom: 12 }}>
 <label style={{ fontSize: 11, color: C.textMute, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6, display: "block", marginBottom: 4 }}>Cookies (JSON or Cookie: header)</label>
 <textarea value={bulkCookies} onChange={(e) => setBulkCookies(e.target.value)} placeholder='Paste cookies here...' style={{ width: "100%", minHeight: 100, fontSize: 11, fontFamily: "ui-monospace, monospace", background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, padding: 10, resize: "vertical", boxSizing: "border-box" }} />
 </div>
 <div style={{ marginBottom: 14 }}>
 <label style={{ fontSize: 11, color: C.textMute, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6, display: "block", marginBottom: 4 }}>Expires at (optional)</label>
 <input type="datetime-local" value={bulkExpiry} onChange={(e) => setBulkExpiry(e.target.value)} style={{ width: "100%", fontSize: 12, background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, padding: 8, boxSizing: "border-box" }} />
 </div>
 <div style={{ display: "flex", gap: 8, justifyContent: "space-between" }}>
 <button onClick={() => { setBulkRefreshStep(s => s + 1); setBulkCookies(""); setBulkExpiry(""); }} style={{ padding: "8px 14px", background: "transparent", color: C.textDim, border: `1px solid ${C.border}`, borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Skip this platform</button>
 <button onClick={async () => {
 if (!bulkCookies.trim()) { alert("Paste cookies first"); return; }
 await callAction("/api/admin/platform-creds/refresh", { platform: current.platform, cookies: bulkCookies.trim(), expires_at: bulkExpiry || null }, "refresh:" + current.platform);
 setBulkRefreshStep(s => s + 1); setBulkCookies(""); setBulkExpiry("");
 }} disabled={busyKey.startsWith("refresh:") || !bulkCookies.trim()} style={{ padding: "8px 14px", background: C.green, color: "white", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer", opacity: !bulkCookies.trim() ? 0.5 : 1 }}>{busyKey.startsWith("refresh:") ? "Saving..." : "Save & Next →"}</button>
 </div>
 </>
 )}
 </div>
 </div>
 );
 })() : null}
 </div>
 );
}

// ============================================================================
// PAGE: CUSTOMERS — roster with MRR, tier, status, last activity
// ============================================================================
// ============================================================================
// PAGE: COMPLAINTS — open + past-SLA + resolved + refunds + chargebacks
// Routes: /admin/complaints (task #452, 2026-05-10)
// ============================================================================
function ComplaintsPage() {
 const [slaStatus, setSlaStatus] = useState(null);
 const [complaints, setComplaints] = useState([]);
 const [resolved, setResolved] = useState([]);
 const [confirmStats, setConfirmStats] = useState(null);
 const [refunds, setRefunds] = useState([]);
 const [chargebacks, setChargebacks] = useState([]);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState("");
 const [busyId, setBusyId] = useState("");
 const [filterChannel, setFilterChannel] = useState("all");
 const [filterStatus, setFilterStatus] = useState("open");
 const [filterDays, setFilterDays] = useState(30);

 const load = async () => {
 setLoading(true); setError("");
 try {
 const [sla, openList, resolvedList, cs, rf, cb] = await Promise.all([
 fetch("/api/complaints/sla-status", { credentials: "include" }).then(r => r.json()).catch(() => null),
 fetch(`/api/complaints?status=${filterStatus}&days=${filterDays}`, { credentials: "include" }).then(r => r.json()).catch(() => ({})),
 fetch(`/api/complaints?status=resolved&days=30`, { credentials: "include" }).then(r => r.json()).catch(() => ({})),
 fetch("/api/complaints/confirmation-stats?days=30", { credentials: "include" }).then(r => r.json()).catch(() => null),
 fetch("/api/refunds?days=90", { credentials: "include" }).then(r => r.json()).catch(() => ({})),
 fetch("/api/chargebacks?months=12", { credentials: "include" }).then(r => r.json()).catch(() => ({})),
 ]);
 setSlaStatus(sla);
 setComplaints((openList && openList.complaints) || []);
 setResolved((resolvedList && resolvedList.complaints) || []);
 setConfirmStats(cs);
 setRefunds((rf && rf.refunds) || []);
 setChargebacks((cb && cb.chargebacks) || []);
 } catch (e) { setError(e.message || "Network error"); }
 setLoading(false);
 };
 useEffect(() => { load(); }, [filterStatus, filterDays]);

 const callAction = async (path, body, id) => {
 setBusyId(id);
 try {
 const resp = await fetch(path, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
 const data = await resp.json().catch(() => ({}));
 if (!resp.ok || data.ok === false) setError(data.error || ("HTTP " + resp.status));
 else await load();
 } catch (e) { setError(e.message); }
 setBusyId("");
 };

 const channelFilter = (c) => filterChannel === "all" || c.channel === filterChannel;
 const filteredComplaints = complaints.filter(channelFilter);
 const filteredResolved = resolved.filter(channelFilter);

 const daysOpen = (rec) => {
 const ts = new Date(rec.opened_at).getTime();
 return Math.floor((Date.now() - ts) / 86400000);
 };

 if (loading) return <div style={{ color: C.textDim }}>Loading complaints...</div>;

 const pastSlaCount = slaStatus?.past_sla || 0;
 const pastSlaList = filteredComplaints.filter(c => c.sla_breached_at);

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 {error && <div style={{ background: C.red, color: "#fff", padding: 12, borderRadius: 6 }}>{error}</div>}

 {/* Past SLA alert section */}
 {pastSlaCount > 0 && (
 <div style={{ background: "rgba(239,68,68,0.08)", border: `2px solid ${C.red}`, borderRadius: 8, padding: 16 }}>
 <h2 style={{ color: C.red, margin: "0 0 8px 0", fontSize: 16 }}>⚠️ {pastSlaCount} complaint(s) past 2-business-day SLA</h2>
 <ul style={{ margin: 0, paddingLeft: 20, color: C.text, fontSize: 12 }}>
 {pastSlaList.slice(0, 10).map(c => (
 <li key={c.id}>
 <strong>{c.customer_email || c.customer_id}</strong> — {c.subject || "(no subject)"} ({daysOpen(c)}d open, SLA breached {c.sla_breached_at?.slice(0, 10)})
 </li>
 ))}
 </ul>
 </div>
 )}

 {/* Metric tiles */}
 <div style={{ display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 12 }}>
 <MetricTile label="Open complaints" value={String(slaStatus?.open_total || 0)} accent={C.accent} />
 <MetricTile label="Past SLA" value={String(pastSlaCount)} accent={pastSlaCount > 0 ? C.red : C.green} />
 <MetricTile label="Resolved (7d)" value={String(slaStatus?.resolved_last_7d || 0)} accent={C.green} />
 <MetricTile label="Confirm rate" value={`${confirmStats?.response_rate_pct ?? 0}%`} sub={`${confirmStats?.confirmed_yes ?? 0} ✓ / ${confirmStats?.confirmed_no_reopen ?? 0} ✗`} accent={C.lime} />
 <MetricTile label="Chargebacks (12mo)" value={String(chargebacks.length)} accent={chargebacks.length > 0 ? C.yellow : C.green} />
 </div>

 {/* Filter controls */}
 <div style={{ display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap" }}>
 <label style={{ color: C.textDim, fontSize: 12 }}>Channel:&nbsp;
 <select value={filterChannel} onChange={(e) => setFilterChannel(e.target.value)} style={{ background: C.panel, color: C.text, border: `1px solid ${C.border}`, padding: "4px 8px", borderRadius: 4 }}>
 <option value="all">All</option>
 <option value="help@">help@</option>
 <option value="support@">support@</option>
 <option value="feedback@">feedback@</option>
 <option value="dashboard">dashboard</option>
 </select>
 </label>
 <label style={{ color: C.textDim, fontSize: 12 }}>Status:&nbsp;
 <select value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)} style={{ background: C.panel, color: C.text, border: `1px solid ${C.border}`, padding: "4px 8px", borderRadius: 4 }}>
 <option value="open">open</option>
 <option value="reopened">reopened</option>
 <option value="resolved">resolved</option>
 <option value="all">all</option>
 </select>
 </label>
 <label style={{ color: C.textDim, fontSize: 12 }}>Days:&nbsp;
 <select value={filterDays} onChange={(e) => setFilterDays(parseInt(e.target.value, 10))} style={{ background: C.panel, color: C.text, border: `1px solid ${C.border}`, padding: "4px 8px", borderRadius: 4 }}>
 <option value={7}>7</option>
 <option value={30}>30</option>
 <option value={90}>90</option>
 <option value={365}>365</option>
 </select>
 </label>
 <button onClick={() => callAction("/api/complaints/sla-sweep", {}, "sweep")} disabled={busyId === "sweep"} style={{ padding: "6px 12px", background: "transparent", color: C.accent, border: `1px solid ${C.accent}`, borderRadius: 4, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Run SLA Sweep</button>
 </div>

 {/* Open complaints table */}
 <SectionCard title="🔥 Open complaints" subtitle={`${filteredComplaints.length} ${filterStatus} complaint(s)`}>
 <div style={{ overflowX: "auto" }}>
 <table style={{ width: "100%", fontSize: 12, borderCollapse: "collapse" }}>
 <thead>
 <tr style={{ borderBottom: `1px solid ${C.border}` }}>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Customer</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Channel</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Opened</th>
 <th style={{ textAlign: "right", padding: "8px", color: C.textDim }}>Days</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Last response</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Status</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Actions</th>
 </tr>
 </thead>
 <tbody>
 {filteredComplaints.map((c, i) => (
 <tr key={c.id} style={{ borderBottom: `1px solid ${C.borderSoft}`, background: i % 2 === 0 ? C.panelHi : "transparent" }}>
 <td style={{ padding: "8px", color: C.text }}>{c.customer_email || c.customer_id}</td>
 <td style={{ padding: "8px", color: C.textDim }}>{c.channel || "?"}</td>
 <td style={{ padding: "8px", color: C.textDim }}>{c.opened_at?.slice(0, 10)}</td>
 <td style={{ padding: "8px", color: daysOpen(c) > 2 ? C.red : C.textDim, textAlign: "right", fontWeight: 700 }}>{daysOpen(c)}</td>
 <td style={{ padding: "8px", color: C.textDim }}>{c.last_response_at?.slice(0, 10) || "—"}</td>
 <td style={{ padding: "8px", color: c.sla_breached_at ? C.red : (c.status === "reopened" ? C.yellow : C.textDim) }}>{c.status}{c.sla_breached_at ? " (SLA!)" : ""}</td>
 <td style={{ padding: "8px" }}>
 <button onClick={() => callAction("/api/complaints/resolve", { complaint_id: c.id }, c.id)} disabled={busyId === c.id} style={{ padding: "4px 8px", background: C.green, color: "#fff", border: "none", borderRadius: 4, fontSize: 11, fontWeight: 700, cursor: "pointer", marginRight: 4 }}>Resolve</button>
 <button onClick={() => alert(JSON.stringify(c, null, 2))} style={{ padding: "4px 8px", background: "transparent", color: C.textDim, border: `1px solid ${C.border}`, borderRadius: 4, fontSize: 11, fontWeight: 700, cursor: "pointer" }}>View</button>
 </td>
 </tr>
 ))}
 {filteredComplaints.length === 0 && <tr><td colSpan={7} style={{ padding: 24, color: C.textDim, textAlign: "center" }}>No complaints in this window.</td></tr>}
 </tbody>
 </table>
 </div>
 </SectionCard>

 {/* Resolved last 30d */}
 <SectionCard title="✅ Resolved (last 30d)" subtitle={`${filteredResolved.length} resolved`}>
 <div style={{ fontSize: 12, color: C.textDim }}>
 {filteredResolved.slice(0, 20).map(c => (
 <div key={c.id} style={{ padding: "6px 0", borderBottom: `1px solid ${C.borderSoft}` }}>
 <strong style={{ color: C.text }}>{c.customer_email || c.customer_id}</strong> — {c.subject || "(no subject)"} · resolved {c.resolved_at?.slice(0,10)} · confirm: <span style={{ color: c.confirmation_response === "yes" ? C.green : c.confirmation_response === "no" ? C.red : C.textMute }}>{c.confirmation_response || "no response yet"}</span>
 </div>
 ))}
 {filteredResolved.length === 0 && <div style={{ padding: 12 }}>No resolved complaints.</div>}
 </div>
 </SectionCard>

 {/* Confirmation gate */}
 <SectionCard title="📬 Confirmation gate (30d)" subtitle="Did the fix actually fix it?">
 <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12, fontSize: 13 }}>
 <div><strong style={{ color: C.text }}>{confirmStats?.resolved ?? 0}</strong><br /><span style={{ color: C.textDim }}>resolved</span></div>
 <div><strong style={{ color: C.green }}>{confirmStats?.confirmed_yes ?? 0}</strong><br /><span style={{ color: C.textDim }}>confirmed fixed</span></div>
 <div><strong style={{ color: C.red }}>{confirmStats?.confirmed_no_reopen ?? 0}</strong><br /><span style={{ color: C.textDim }}>reopened (no)</span></div>
 <div><strong style={{ color: C.yellow }}>{confirmStats?.no_response_timeout ?? 0}</strong><br /><span style={{ color: C.textDim }}>no response (timeout)</span></div>
 </div>
 </SectionCard>

 {/* Refunds */}
 <SectionCard title="💸 Refunds (last 90d)" subtitle={`${refunds.length} refund(s)`}>
 <div style={{ overflowX: "auto" }}>
 <table style={{ width: "100%", fontSize: 12, borderCollapse: "collapse" }}>
 <thead><tr style={{ borderBottom: `1px solid ${C.border}` }}>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Customer</th>
 <th style={{ textAlign: "right", padding: "8px", color: C.textDim }}>Amount</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Reason</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Issued</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Decided by</th>
 </tr></thead>
 <tbody>
 {refunds.map(r => (
 <tr key={r.id} style={{ borderBottom: `1px solid ${C.borderSoft}` }}>
 <td style={{ padding: "8px", color: C.text }}>{r.customer_id}</td>
 <td style={{ padding: "8px", color: C.text, textAlign: "right" }}>${((r.amount_cents || 0) / 100).toFixed(2)}</td>
 <td style={{ padding: "8px", color: C.textDim }}>{r.reason}</td>
 <td style={{ padding: "8px", color: C.textDim }}>{r.issued_at?.slice(0, 10)}</td>
 <td style={{ padding: "8px", color: C.textDim }}>{r.decided_by}</td>
 </tr>
 ))}
 {refunds.length === 0 && <tr><td colSpan={5} style={{ padding: 24, color: C.textDim, textAlign: "center" }}>No refunds in last 90d.</td></tr>}
 </tbody>
 </table>
 </div>
 </SectionCard>

 {/* Chargebacks */}
 <SectionCard title="🟠 Chargebacks (last 12mo)" subtitle={`${chargebacks.length} dispute(s)`}>
 <div style={{ overflowX: "auto" }}>
 <table style={{ width: "100%", fontSize: 12, borderCollapse: "collapse" }}>
 <thead><tr style={{ borderBottom: `1px solid ${C.border}` }}>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Dispute ID</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Customer</th>
 <th style={{ textAlign: "right", padding: "8px", color: C.textDim }}>Amount</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Status</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Outcome</th>
 <th style={{ textAlign: "left", padding: "8px", color: C.textDim }}>Opened</th>
 </tr></thead>
 <tbody>
 {chargebacks.map(cb => (
 <tr key={cb.dispute_id} style={{ borderBottom: `1px solid ${C.borderSoft}` }}>
 <td style={{ padding: "8px", color: C.text, fontFamily: "monospace", fontSize: 11 }}>{cb.dispute_id}</td>
 <td style={{ padding: "8px", color: C.text }}>{cb.customer_id}</td>
 <td style={{ padding: "8px", color: C.text, textAlign: "right" }}>${((cb.amount_cents || 0) / 100).toFixed(2)}</td>
 <td style={{ padding: "8px", color: C.textDim }}>{cb.status}</td>
 <td style={{ padding: "8px", color: cb.outcome === "won" ? C.green : cb.outcome === "lost" ? C.red : C.textMute }}>{cb.outcome || "pending"}</td>
 <td style={{ padding: "8px", color: C.textDim }}>{cb.opened_at?.slice(0, 10)}</td>
 </tr>
 ))}
 {chargebacks.length === 0 && <tr><td colSpan={6} style={{ padding: 24, color: C.textDim, textAlign: "center" }}>No chargebacks in last 12mo.</td></tr>}
 </tbody>
 </table>
 </div>
 </SectionCard>
 </div>
 );
}

function CustomersPage() {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);

 useEffect(() => {
 const loadData = async () => {
 try {
 const res = await fetch("/api/customers/roster", { credentials: "include" });
 if (!res.ok) throw new Error(`${res.status}`);
 const json = await res.json();
 setData(json);
 setError(null);
 } catch (e) {
 setError(e.message);
 setData(null);
 } finally {
 setLoading(false);
 }
 };
 loadData();
 const timer = setInterval(loadData, 30000);
 return () => clearInterval(timer);
 }, []);

 if (loading) return <div style={{ color: C.textDim }}>Loading customers...</div>;
 if (error) return <div style={{ color: C.red }}>Error: {error}</div>;
 if (!data || !data.customers || data.customers.length === 0) {
 const isLive = data && data.phase === "live";
 const msg = isLive
   ? "No customers yet. Site is live and accepting signups; first customer will appear here."
   : "No customers yet. Site is dark. Say start live phase in CEO chat to flip toutmark.com live and start accepting signups.";
 return <div style={{ color: C.textDim, textAlign: "center", padding: 32 }}>{msg}</div>;
 }

 const totalMrr = (data.customers || []).reduce((sum, c) => sum + (c.mrr_cents || 0), 0) / 100;

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12 }}>
 <MetricTile label="Total Customers" value={String(data.customers.length)} accent={C.accent} />
 <MetricTile label="Monthly Recurring Revenue" value={`$${totalMrr.toFixed(2)}`} accent={C.green} />
 <MetricTile label="Regulated Customers" value={String(data.customers.filter(c => c.regulated).length)} accent={C.yellow} />
 <MetricTile label="Phase" value={data.phase || "Live Phase"} accent={C.violet} />
 </div>

 <SectionCard title="📊 Customer Roster" subtitle={`${data.customers.length} active customers`}>
 <div style={{ overflowX: "auto" }}>
 <table style={{ width: "100%", fontSize: 12, borderCollapse: "collapse" }}>
 <thead>
 <tr style={{ borderBottom: `1px solid ${C.border}` }}>
 <th style={{ textAlign: "left", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Customer</th>
 <th style={{ textAlign: "left", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Tier</th>
 <th style={{ textAlign: "left", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Signup</th>
 <th style={{ textAlign: "right", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>MRR</th>
 <th style={{ textAlign: "left", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Status</th>
 <th style={{ textAlign: "center", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Regulated</th>
 <th style={{ textAlign: "left", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Last Activity</th>
 </tr>
 </thead>
 <tbody>
 {data.customers.map((c, i) => (
 <tr key={i} style={{ borderBottom: `1px solid ${C.borderSoft}`, background: i % 2 === 0 ? C.panelHi : "transparent" }}>
 <td style={{ padding: "10px 8px", color: C.text, fontWeight: 600 }}>{c.brand_name || c.id}</td>
 <td style={{ padding: "10px 8px", color: C.textDim }}>{c.tier || "Standard"}</td>
 <td style={{ padding: "10px 8px", color: C.textDim }}>{c.signup_date ? new Date(c.signup_date).toLocaleDateString() : "—"}</td>
 <td style={{ padding: "10px 8px", color: C.text, fontWeight: 600, textAlign: "right" }}>${(c.mrr_cents / 100).toFixed(2)}</td>
 <td style={{ padding: "10px 8px", color: c.status === "active" ? C.green : C.textDim }}>{c.status || "active"}</td>
 <td style={{ padding: "10px 8px", textAlign: "center", color: c.regulated ? C.yellow : C.textMute }}>{c.regulated ? "⚖️" : "—"}</td>
 <td style={{ padding: "10px 8px", color: C.textDim }}>{c.last_activity ? new Date(c.last_activity).toLocaleDateString() : "—"}</td>
 </tr>
 ))}
 </tbody>
 </table>
 </div>
 </SectionCard>
 </div>
 );
}

// ============================================================================
// PAGE: ONBOARDING — Kanban view of customer onboarding stages
// ============================================================================
function OnboardingPage() {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);
 const [stageOverrideId, setStageOverrideId] = useState(null);
 const [stageOverrideValue, setStageOverrideValue] = useState("");
 const [stageOverrideReason, setStageOverrideReason] = useState("");
 const [busy, setBusy] = useState(false);

 const STAGES = ["signup", "platform_detected", "oauth_or_paste", "install_verified", "initial_rewrite", "live_serving", "ongoing"];

 useEffect(() => {
 const loadData = async () => {
 try {
 const res = await fetch("/api/customers/onboarding", { credentials: "include" });
 if (!res.ok) throw new Error(`${res.status}`);
 const json = await res.json();
 setData(json);
 setError(null);
 } catch (e) {
 setError(e.message);
 setData(null);
 } finally {
 setLoading(false);
 }
 };
 loadData();
 const timer = setInterval(loadData, 30000);
 return () => clearInterval(timer);
 }, []);

 const submitStageOverride = async () => {
 if (!stageOverrideId || !stageOverrideValue) return;
 setBusy(true);
 try {
 const res = await fetch(`/api/customers/${stageOverrideId}/onboarding-stage`, {
 method: "POST",
 credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ stage: stageOverrideValue, reason: stageOverrideReason }),
 });
 if (res.ok) {
 setStageOverrideId(null);
 setStageOverrideValue("");
 setStageOverrideReason("");
 const reload = await fetch("/api/customers/onboarding", { credentials: "include" });
 const json = await reload.json();
 setData(json);
 } else {
 alert("Failed to update stage");
 }
 } catch (e) {
 alert(`Error: ${e.message}`);
 } finally {
 setBusy(false);
 }
 };

 // Stage-level captions explaining what each column means + why some are usually empty.
 // The signup wizard auto-advances most customers through the first 4 stages, so those
 // columns are usually empty (which is itself useful — empty = wizard isn't trapping anyone).
 // The post-signup stages (initial_rewrite → live_serving → ongoing) are where customers
 // actually accumulate over days/weeks and where the tracker earns its keep.
 const STAGE_INFO = {
  signup:            { caption: "Wizard not finished. Usually empty — if anyone parks here, the wizard trapped them.", phase: "during-wizard" },
  platform_detected: { caption: "Wizard detected their CMS but they haven't installed yet. Usually empty for the same reason.", phase: "during-wizard" },
  oauth_or_paste:    { caption: "Wizard is at the connect-site step. Customers stuck here = popup-blocked or OAuth rejected. Worth investigating.", phase: "during-wizard" },
  install_verified:  { caption: "Wizard finished, our snippet/plugin is verified live on their site.", phase: "during-wizard" },
  initial_rewrite:   { caption: "Editor's first content sweep — usually 24-48 hours after signup. Real waiting room.", phase: "post-signup" },
  live_serving:      { caption: "Edits are publicly serving on the customer's site. AI bots can now crawl + cite. Steady-state begins.", phase: "post-signup" },
  ongoing:           { caption: "Steady-state customer — weekly cadence of rewrites, blog posts, citation reports.", phase: "post-signup" },
 };

 if (loading) return <div style={{ color: C.textDim }}>Loading onboarding data...</div>;
 if (error) return <div style={{ color: C.red }}>Error: {error}</div>;
 if (!data || !data.by_stage) return <div style={{ color: C.textDim }}>No onboarding data.</div>;

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 }}>
   <div style={{ fontSize: 13, color: C.text, fontWeight: 700, marginBottom: 6 }}>Customer onboarding pipeline</div>
   <div style={{ fontSize: 12, color: C.textDim, lineHeight: 1.6 }}>
    The signup wizard handles the first 4 columns automatically — most customers will move through them in minutes and the columns will look empty (which is good: it means nobody's stuck). The last 3 columns are post-signup workflow where customers actually accumulate over time. <strong style={{ color: C.yellow }}>If anyone stalls in "oauth or paste" for &gt; 1 hour, the OAuth popup probably got blocked — reach out.</strong>
   </div>
 </div>
 <div style={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 12, overflowX: "auto" }}>
 {STAGES.map(stage => {
 const items = data.by_stage[stage] || [];
 return (
 <div key={stage} style={{ minWidth: 280, background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 }}>
 <div style={{ fontSize: 11, fontWeight: 700, color: STAGE_INFO[stage]?.phase === "post-signup" ? C.accent : C.textDim, textTransform: "uppercase", marginBottom: 4 }}>
 {stage.replace(/_/g, " ")} ({items.length})
 </div>
 <div style={{ fontSize: 10, color: C.textMute, lineHeight: 1.4, marginBottom: 10, minHeight: 32 }}>{STAGE_INFO[stage]?.caption || ""}</div>
 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 {items.map((item, i) => (
 <div key={i} style={{ background: C.bg, border: `1px solid ${C.borderSoft}`, borderRadius: 6, padding: 10, fontSize: 11 }}>
 <div style={{ color: C.text, fontWeight: 600, marginBottom: 4 }}>{item.brand_name || item.id}</div>
 <div style={{ color: C.textDim, fontSize: 10, marginBottom: 4 }}>
 In stage: {item.days_in_stage || 0} days
 </div>
 <div style={{ color: C.textMute, fontSize: 9, marginBottom: 6 }}>Last: {item.last_action || "—"}</div>
 <button
 onClick={() => {
 setStageOverrideId(item.id);
 setStageOverrideValue(stage);
 }}
 style={{ width: "100%", padding: "4px 8px", background: "transparent", color: C.accent, border: `1px solid ${C.accent}`, borderRadius: 4, fontSize: 10, cursor: "pointer" }}
 >
 Override
 </button>
 </div>
 ))}
 {items.length === 0 && <div style={{ color: C.textMute, fontSize: 10, padding: 8, textAlign: "center" }}>Empty</div>}
 </div>
 </div>
 );
 })}
 </div>

 {stageOverrideId && (
 <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.7)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 100 }}>
 <div style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 12, padding: 24, maxWidth: 400, width: "90%" }}>
 <div style={{ fontSize: 16, fontWeight: 800, color: C.text, marginBottom: 12 }}>Override Onboarding Stage</div>
 <select
 value={stageOverrideValue}
 onChange={e => setStageOverrideValue(e.target.value)}
 style={{ width: "100%", padding: "8px 10px", background: C.bg, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 12, marginBottom: 10 }}
 >
 <option value="">Select new stage...</option>
 {STAGES.map(s => <option key={s} value={s}>{s.replace(/_/g, " ")}</option>)}
 </select>
 <textarea
 value={stageOverrideReason}
 onChange={e => setStageOverrideReason(e.target.value)}
 placeholder="Reason for override..."
 style={{ width: "100%", minHeight: 80, padding: 10, background: C.bg, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 12, marginBottom: 12 }}
 />
 <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
 <button onClick={() => setStageOverrideId(null)} style={{ padding: "8px 14px", background: "transparent", color: C.textDim, border: `1px solid ${C.border}`, borderRadius: 6, fontSize: 12, cursor: "pointer" }}>Cancel</button>
 <button onClick={submitStageOverride} disabled={busy || !stageOverrideValue} style={{ padding: "8px 14px", background: C.green, color: "white", border: "none", borderRadius: 6, fontSize: 12, cursor: "pointer", opacity: busy ? 0.5 : 1 }}>Save</button>
 </div>
 </div>
 </div>
 )}
 </div>
 );
}

// ============================================================================
// PAGE: STALLED APPROVALS — per-customer approval queue monitoring
// ============================================================================
function StalledApprovalsPage() {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);

 useEffect(() => {
 const loadData = async () => {
 try {
 const res = await fetch("/api/approvals/stalled?days=7&count=5", { credentials: "include" });
 if (!res.ok) throw new Error(`${res.status}`);
 const json = await res.json();
 setData(json);
 setError(null);
 } catch (e) {
 setError(e.message);
 setData(null);
 } finally {
 setLoading(false);
 }
 };
 loadData();
 const timer = setInterval(loadData, 30000);
 return () => clearInterval(timer);
 }, []);

 if (loading) return <div style={{ color: C.textDim }}>Loading approval status...</div>;
 if (error) return <div style={{ color: C.red }}>Error: {error}</div>;
 if (!data || !data.stalled || data.stalled.length === 0) {
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard title="⚠️ Stalled Approvals" subtitle="Customers with pending approvals older than 7 days">
 <div style={{ color: C.textDim, fontSize: 13, lineHeight: 1.7 }}>
 Watches every customer's approval queue. When a customer has 5+ items pending for 7+ days, they show up here so the Owner can send a nudge reminder. Refreshes every 30s.
 </div>
 </SectionCard>
 <div style={{ color: C.textMute, fontSize: 12, textAlign: "center", padding: 16, fontStyle: "italic" }}>
 ✓ All approval queues are healthy.
 </div>
 </div>
 );
 }

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 12 }}>
 <MetricTile label="Customers with Stalled Approvals" value={String(data.stalled.length)} accent={C.red} />
 <MetricTile label="Total Pending Items" value={String(data.stalled.reduce((sum, c) => sum + (c.pending_count || 0), 0))} accent={C.yellow} />
 <MetricTile label="Oldest Item (days)" value={String(Math.max(0, ...data.stalled.map(c => c.oldest_age_days || 0)))} accent={C.orange} />
 </div>

 <SectionCard title="⚠️ Customers with Stalled Approvals">
 <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
 {data.stalled.map((c, i) => (
 <div key={i} style={{ background: C.panelHi, border: `1px solid ${c.oldest_age_days >= 7 ? C.red : C.yellow}`, borderRadius: 8, padding: 14, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
 <div>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text, marginBottom: 4 }}>{c.customer_name || c.customer_id}</div>
 <div style={{ fontSize: 11, color: C.textDim }}>
 {c.pending_count} pending items · oldest {c.oldest_age_days} days · last approval {new Date(c.last_approval_date).toLocaleDateString()}
 </div>
 </div>
 <button onClick={async () => {
  try {
   const r = await fetch("/api/approvals/notify-customer", {
    method: "POST", credentials: "include",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ customer_id: c.customer_id, pending_count: c.pending_count, oldest_age_days: c.oldest_age_days }),
   });
   if (r.ok) alert(`Reminder queued for ${c.customer_name || c.customer_id}.`);
   else alert(`Failed: HTTP ${r.status} (endpoint may not be wired yet — see roadmap ops_stalled_notify).`);
  } catch (e) { alert(`Network error: ${e.message}`); }
 }} style={{ padding: "6px 12px", background: C.blue, color: "white", border: "none", borderRadius: 6, fontSize: 11, fontWeight: 700, cursor: "pointer", whiteSpace: "nowrap" }}>
 Notify
 </button>
 </div>
 ))}
 </div>
 </SectionCard>
 </div>
 );
}

// ============================================================================
// PAGE: COLD EMAIL PACE — 30/day cap, complaint rate, 30-day trend
// ============================================================================
function ColdEmailPacePage() {
 const [budget, setBudget] = useState(null);
 const [stats, setStats] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);

 useEffect(() => {
 const loadData = async () => {
 try {
 const [budgetRes, statsRes] = await Promise.all([
 fetch("/api/cold-email/budget", { credentials: "include" }),
 fetch("/api/email/stats?days=30", { credentials: "include" }),
 ]);
 if (!budgetRes.ok || !statsRes.ok) throw new Error("Failed to load");
 const b = await budgetRes.json();
 const s = await statsRes.json();
 setBudget(b);
 setStats(s);
 setError(null);
 } catch (e) {
 setError(e.message);
 } finally {
 setLoading(false);
 }
 };
 loadData();
 const timer = setInterval(loadData, 30000);
 return () => clearInterval(timer);
 }, []);

 if (loading) return <div style={{ color: C.textDim }}>Loading cold email stats...</div>;
 if (error) return <div style={{ color: C.red }}>Error: {error}</div>;

 const complaintRate = stats?.complaint_rate || 0;
 const isPaused = budget?.paused || complaintRate > 2;

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{ background: isPaused ? `${C.red}20` : `${C.green}20`, border: `1px solid ${isPaused ? C.red : C.green}`, borderRadius: 10, padding: 16 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}>
 <div style={{ fontSize: 20 }}>{isPaused ? "⚠️" : "✅"}</div>
 <div>
 <div style={{ fontSize: 14, fontWeight: 700, color: C.text }}>Cold Email Status</div>
 <div style={{ fontSize: 12, color: C.textDim }}>{isPaused ? "Auto-pause armed — complaint rate > 2%" : "Running normally"}</div>
 </div>
 </div>
 </div>

 <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12 }}>
 <MetricTile label="Emails Sent Today" value={`${budget?.sent_today || 0} / ${budget?.daily_cap || 30}`} accent={C.accent} />
 <MetricTile label="Remaining Today" value={String(Math.max(0, (budget?.remaining || 0)))} accent={C.green} />
 <MetricTile label="Complaint Rate (30d)" value={`${complaintRate.toFixed(1)}%`} accent={complaintRate > 2 ? C.red : C.yellow} />
 <MetricTile label="Unsubscribe Rate" value={`${(stats?.unsubscribe_rate || 0).toFixed(1)}%`} accent={C.textDim} />
 </div>

 <SectionCard title="📊 30-Day Email Volume" subtitle="Daily sends over the last month">
 {stats?.daily_sends && stats.daily_sends.length > 0 ? (
 <div style={{ display: "flex", alignItems: "flex-end", gap: 4, height: 180, padding: "16px 0" }}>
 {stats.daily_sends.map((day, i) => {
 const maxSends = Math.max(30, Math.max(...stats.daily_sends.map(d => d.count)));
 const height = (day.count / maxSends) * 160;
 return (
 <div key={i} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
 <div
 style={{
 width: "100%",
 height,
 background: `linear-gradient(180deg, ${C.accent} 0%, ${C.accentDim} 100%)`,
 borderRadius: 4,
 }}
 />
 <div style={{ fontSize: 8, color: C.textMute, textAlign: "center" }}>{day.date.slice(5)}</div>
 <div style={{ fontSize: 9, color: C.textDim, fontWeight: 600 }}>{day.count}</div>
 </div>
 );
 })}
 </div>
 ) : (
 <div style={{ color: C.textMute, padding: 16, textAlign: "center" }}>No data yet</div>
 )}
 </SectionCard>
 </div>
 );
}

// ============================================================================
// PAGE: CRON STATUS — observe which crons fired, when, and if they errored
// ============================================================================
function CronStatusPage() {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);

 // Cron expressions are stored in UTC (Cloudflare's convention).
 // Descriptions show America/Los_Angeles equivalent per task #395.
 const KNOWN_CRONS = [
 { name: "Citation Report", expression: "0 4 1 * *", description: "Monthly 1st @ 9pm LA (4am UTC)" },
 { name: "Reddit scrape", expression: "0 9 * * 1", description: "Mondays @ 2am LA (9am UTC)" },
 { name: "Spend Manager tick", expression: "0 11 * * *", description: "Daily @ 4am LA (11am UTC)" },
 { name: "Analyst tick", expression: "0 2 * * *", description: "Daily @ 7pm LA prev (2am UTC)" },
 { name: "Analyst weekly", expression: "0 23 * * 0", description: "Sundays @ 4pm LA (11pm UTC)" },
 { name: "Agent brief", expression: "0 8 * * *", description: "Daily @ 1am LA (8am UTC)" },
 ];

 useEffect(() => {
 const loadData = async () => {
 try {
 const res = await fetch("/api/crons/status", { credentials: "include" });
 if (!res.ok) throw new Error(`${res.status}`);
 const json = await res.json();
 setData(json);
 setError(null);
 } catch (e) {
 setError(e.message);
 setData(null);
 } finally {
 setLoading(false);
 }
 };
 loadData();
 const timer = setInterval(loadData, 30000);
 return () => clearInterval(timer);
 }, []);

 if (loading) return <div style={{ color: C.textDim }}>Loading cron status...</div>;
 if (error) return <div style={{ color: C.red }}>Error: {error}</div>;

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard title="🕐 Scheduled Cron Jobs" subtitle="Autonomous tasks running on fixed schedules">
 <div style={{ overflowX: "auto" }}>
 <table style={{ width: "100%", fontSize: 12, borderCollapse: "collapse" }}>
 <thead>
 <tr style={{ borderBottom: `1px solid ${C.border}` }}>
 <th style={{ textAlign: "left", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Cron</th>
 <th style={{ textAlign: "left", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Schedule</th>
 <th style={{ textAlign: "left", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Last Run</th>
 <th style={{ textAlign: "center", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Status</th>
 <th style={{ textAlign: "left", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>Next Run (est.)</th>
 </tr>
 </thead>
 <tbody>
 {KNOWN_CRONS.map((cron, i) => {
 const cronData = data?.crons?.[cron.name] || {};
 const lastRun = cronData.last_run_at ? new Date(cronData.last_run_at) : null;
 const statusColor = cronData.status === "success" ? C.green : cronData.status === "error" ? C.red : C.textMute;
 return (
 <tr key={i} style={{ borderBottom: `1px solid ${C.borderSoft}`, background: i % 2 === 0 ? C.panelHi : "transparent" }}>
 <td style={{ padding: "10px 8px", color: C.text, fontWeight: 600 }}>{cron.name}</td>
 <td style={{ padding: "10px 8px", color: C.textDim, fontFamily: "ui-monospace, monospace", fontSize: 11 }}>{cron.expression}</td>
 <td style={{ padding: "10px 8px", color: C.textDim }}>
 {lastRun ? lastRun.toLocaleString() : "Never"}
 </td>
 <td style={{ padding: "10px 8px", textAlign: "center" }}>
 <span style={{ color: statusColor, fontWeight: 700, fontSize: 13 }}>
 {cronData.status === "success" ? "✅" : cronData.status === "error" ? "❌" : "⚪"}
 </span>
 </td>
 <td style={{ padding: "10px 8px", color: C.textDim, fontSize: 11 }}>{cron.description}</td>
 </tr>
 );
 })}
 </tbody>
 </table>
 </div>
 {data?.error_log && data.error_log.length > 0 && (
 <div style={{ marginTop: 16, padding: 12, background: `${C.red}20`, border: `1px solid ${C.red}`, borderRadius: 8 }}>
 <div style={{ fontSize: 11, fontWeight: 700, color: C.red, marginBottom: 8 }}>Recent Errors</div>
 {data.error_log.map((err, i) => (
 <div key={i} style={{ fontSize: 10, color: C.textDim, marginBottom: 4, fontFamily: "ui-monospace, monospace" }}>
 {err.cron_name} @ {new Date(err.error_at).toLocaleString()}: {err.message}
 </div>
 ))}
 </div>
 )}
 </SectionCard>
 </div>
 );
}

// ============================================================================
// PAGE: WIKIDATA QUEUE — Editor first-20 edits approval gate
// ============================================================================
function WikidataQueuePage() {
 const [items, setItems] = useState([]);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);
 const [busy, setBusy] = useState({});

 useEffect(() => {
 const loadData = async () => {
 try {
 const res = await fetch("/api/wikidata/pending-approvals?limit=50", { credentials: "include" });
 if (!res.ok) throw new Error(`${res.status}`);
 const json = await res.json();
 setItems(json.items || []);
 setError(null);
 } catch (e) {
 setError(e.message);
 setItems([]);
 } finally {
 setLoading(false);
 }
 };
 loadData();
 const timer = setInterval(loadData, 30000);
 return () => clearInterval(timer);
 }, []);

 const handleApprove = async (editId) => {
 setBusy(prev => ({...prev, [editId]: true }));
 try {
 const res = await fetch(`/api/wikidata/${editId}/approve`, { method: "POST", credentials: "include" });
 if (res.ok) {
 setItems(items.filter(i => i.edit_id !== editId));
 } else {
 alert("Approval failed");
 }
 } catch (e) {
 alert(`Error: ${e.message}`);
 } finally {
 setBusy(prev => ({...prev, [editId]: false }));
 }
 };

 const handleReject = async (editId) => {
 setBusy(prev => ({...prev, [editId]: true }));
 try {
 const res = await fetch(`/api/wikidata/${editId}/reject`, { method: "POST", credentials: "include" });
 if (res.ok) {
 setItems(items.filter(i => i.edit_id !== editId));
 } else {
 alert("Rejection failed");
 }
 } catch (e) {
 alert(`Error: ${e.message}`);
 } finally {
 setBusy(prev => ({...prev, [editId]: false }));
 }
 };

 if (loading) return <div style={{ color: C.textDim }}>Loading Wikidata queue...</div>;
 if (error) return <div style={{ color: C.red }}>Error: {error}</div>;
 if (items.length === 0) {
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard title="📝 Wikidata Queue" subtitle="First-20 anti-flag review gate for new bot accounts">
 <div style={{ color: C.textDim, fontSize: 13, lineHeight: 1.7 }}>
 Editor hard-gates the first 20 Wikidata edits per bot account to prevent flagging. Once 20 clean edits ship, the bot graduates to auto-approve. Each pending edit lands here with the proposed change + reasoning before it can be published to Wikidata.
 </div>
 </SectionCard>
 <div style={{ color: C.textMute, fontSize: 12, textAlign: "center", padding: 16, fontStyle: "italic" }}>
 ✓ No pending Wikidata edits. Auto-refreshes every 30s.
 </div>
 </div>
 );
 }

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 12 }}>
 <MetricTile label="Pending Edits" value={String(items.length)} accent={C.accent} />
 <MetricTile label="Approval Gate" value="First 20" accent={C.violet} />
 </div>

 <SectionCard title="📝 Wikidata First-20 Gate" subtitle="Editor hard-gates first 20 edits per bot account to prevent flagging">
 <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
 {items.map((item, i) => (
 <div key={i} style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 8, padding: 14 }}>
 <div style={{ display: "flex", justifyContent: "space-between", alignItems: "start", marginBottom: 8 }}>
 <div>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text, marginBottom: 2 }}>
 {item.entity} — {item.summary}
 </div>
 <div style={{ fontSize: 11, color: C.textDim }}>
 Proposed {item.proposed_at ? new Date(item.proposed_at).toLocaleString() : "—"}
 </div>
 </div>
 <div style={{ fontSize: 11, color: C.textMute }}>Edit #{item.edit_id}</div>
 </div>
 <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
 <button
 onClick={() => handleReject(item.edit_id)}
 disabled={busy[item.edit_id]}
 style={{ padding: "6px 12px", background: C.red, color: "white", border: "none", borderRadius: 6, fontSize: 11, cursor: "pointer", opacity: busy[item.edit_id] ? 0.5 : 1 }}
 >
 Reject
 </button>
 <button
 onClick={() => handleApprove(item.edit_id)}
 disabled={busy[item.edit_id]}
 style={{ padding: "6px 12px", background: C.green, color: "white", border: "none", borderRadius: 6, fontSize: 11, cursor: "pointer", opacity: busy[item.edit_id] ? 0.5 : 1 }}
 >
 {busy[item.edit_id] ? "..." : "Approve"}
 </button>
 </div>
 </div>
 ))}
 </div>
 </SectionCard>
 </div>
 );
}

// ============================================================================
// PAGE: OWNER QUEUE — top 5 priorities from CEO escalations
// ============================================================================
function OwnerQueuePage() {
 const [items, setItems] = useState([]);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);
 const [busy, setBusy] = useState({});

 useEffect(() => {
 const loadData = async () => {
 try {
 const res = await fetch("/api/owner-queue?limit=20", { credentials: "include" });
 if (!res.ok) throw new Error(`${res.status}`);
 const json = await res.json();
 setItems(json.items || []);
 setError(null);
 } catch (e) {
 setError(e.message);
 setItems([]);
 } finally {
 setLoading(false);
 }
 };
 loadData();
 const timer = setInterval(loadData, 30000);
 return () => clearInterval(timer);
 }, []);

 const handleComplete = async (itemId) => {
 setBusy(prev => ({...prev, [itemId]: true }));
 try {
 const res = await fetch(`/api/owner-queue/${itemId}/complete`, { method: "POST", credentials: "include" });
 if (res.ok) {
 setItems(items.filter(i => i.id !== itemId));
 } else {
 alert("Failed to complete");
 }
 } catch (e) {
 alert(`Error: ${e.message}`);
 } finally {
 setBusy(prev => ({...prev, [itemId]: false }));
 }
 };

 const priorityColor = (priority) => {
 if (priority === "P0") return C.red;
 if (priority === "P1") return C.orange;
 return C.yellow;
 };

 if (loading) return <div style={{ color: C.textDim }}>Loading owner queue...</div>;
 if (error) return <div style={{ color: C.red }}>Error: {error}</div>;
 if (items.length === 0) {
  return (
   <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
    <SectionCard title="Owner Queue" subtitle="Things only you can do — agents park them here">
     <div style={{ color: C.textDim, fontSize: 13, lineHeight: 1.7 }}>
      Your agents drop items here when something requires Owner action that they can't handle alone — paying for a tool, signing a doc, approving a high-risk move, verifying an email, or unblocking access to a third-party account. Items have priorities (P0 / P1 / P2) and unblock specific work the agents are waiting on.
     </div>
    </SectionCard>
    <div>
     <div style={{ fontSize: 12, color: C.textMute, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 8 }}>What shows up here</div>
     <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: 10 }}>
      {[
       { ico: "💳", title: "Payments & subscriptions", desc: "Pay for EIN Presswire, top up an API, upgrade Stripe scope, file the LLC." },
       { ico: "✍️", title: "Signatures & approvals", desc: "Sign legal docs, approve a high-risk delegation, accept ToS that we can't auto-accept." },
       { ico: "🔐", title: "Account access", desc: "Verify an email, share a one-time login code, fix a 2FA flow that locked out an agent." },
       { ico: "🚨", title: "Risk escalations", desc: "Anything an agent flagged with full_auto_approve=false, spam complaints over threshold, regulator alerts." },
       { ico: "🧭", title: "Strategic decisions", desc: "Niche pivots, pricing changes, scaling the cold-email cap, hiring a person, paid ads on/off." },
       { ico: "🛠️", title: "Cloudflare / GitHub ops", desc: "Things only the Owner can do in the Cloudflare or GitHub UIs (rotate secrets, attach domain, approve OAuth app)." },
      ].map((cat, i) => (
       <div key={i} style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 8, padding: 14, display: "flex", gap: 10 }}>
        <div style={{ fontSize: 22 }}>{cat.ico}</div>
        <div style={{ flex: 1 }}>
         <div style={{ fontSize: 13, fontWeight: 700, color: C.text, marginBottom: 2 }}>{cat.title}</div>
         <div style={{ fontSize: 12, color: C.textDim, lineHeight: 1.5 }}>{cat.desc}</div>
        </div>
       </div>
      ))}
     </div>
    </div>
    <div style={{ color: C.textMute, fontSize: 12, textAlign: "center", padding: 16, fontStyle: "italic" }}>
     ✓ Inbox zero — no pending items right now. Auto-refreshes every 30s.
    </div>
   </div>
  );
 }

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <SectionCard title="📋 Owner Approval Queue" subtitle={`${items.length} items pending your action`}>
 <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))", gap: 14 }}>
 {items.map((item, i) => (
 <div key={i} style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16, display: "flex", flexDirection: "column", gap: 12 }}>
 <div style={{ display: "flex", alignItems: "start", gap: 10 }}>
 <div style={{
 padding: "4px 8px",
 background: priorityColor(item.priority),
 color: "white",
 borderRadius: 4,
 fontSize: 10,
 fontWeight: 700,
 whiteSpace: "nowrap",
 }}>
 {item.priority}
 </div>
 <div style={{ flex: 1 }}>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>{item.title}</div>
 <div style={{ fontSize: 11, color: C.textDim, marginTop: 2 }}>{item.category}</div>
 </div>
 </div>

 <div style={{ fontSize: 12, color: C.textDim, lineHeight: 1.5 }}>{item.description}</div>

 {item.customer && <div style={{ fontSize: 11, color: C.textMute }}>👤 {item.customer}</div>}
 {item.deadline && <div style={{ fontSize: 11, color: C.yellow }}>📅 {new Date(item.deadline).toLocaleDateString()}</div>}
 {item.estimated_minutes && <div style={{ fontSize: 11, color: C.textMute }}>⏱️ ~{item.estimated_minutes} min</div>}

 <button
 onClick={() => handleComplete(item.id)}
 disabled={busy[item.id]}
 style={{
 padding: "8px 12px",
 background: C.green,
 color: "white",
 border: "none",
 borderRadius: 6,
 fontSize: 12,
 fontWeight: 700,
 cursor: "pointer",
 opacity: busy[item.id] ? 0.5 : 1,
 }}
 >
 {busy[item.id] ? "Completing..." : "Mark Complete"}
 </button>
 </div>
 ))}
 </div>
 </SectionCard>
 </div>
 );
}

// ============================================================================
// PAGE: AGENT BRIEFS — per-agent activity logs with date filtering
// ============================================================================
function AgentBriefsPage() {
 const [briefs, setBriefs] = useState([]);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);
 const [selectedAgent, setSelectedAgent] = useState(null);
 const [days, setDays] = useState(7);
 const [lastFetchAt, setLastFetchAt] = useState(null);

 // Hardened 2026-05-19: 12s AbortController timeout, tolerates non-JSON
 // (returns from /login redirect or HTML SPA fallback), shows error state
 // instead of an indefinite "Loading..." spinner.
 const loadData = React.useCallback(async () => {
 setBriefs([]);
 setLoading(true);
 setError(null);
 const controller = new AbortController();
 const timeoutId = setTimeout(() => controller.abort(), 12000);
 try {
  const url = selectedAgent
   ? `/api/agent-briefs?agent=${selectedAgent}&days=${days}`
   : `/api/agent-briefs?days=${days}&all_agents=true`;
  const res = await fetch(url, { credentials: "include", signal: controller.signal, cache: "no-store" });
  clearTimeout(timeoutId);
  const text = await res.text();
  let json = null;
  try { json = text ? JSON.parse(text) : {}; } catch { throw new Error(`Response was not JSON (HTTP ${res.status}) — likely an auth redirect`); }
  if (!res.ok) throw new Error((json && (json.error || json.message)) || `HTTP ${res.status}`);
  setBriefs(json.briefs || []);
  setError(null);
  setLastFetchAt(new Date());
 } catch (e) {
  clearTimeout(timeoutId);
  setError(e.name === "AbortError" ? "Request timed out after 12s" : (e.message || "Network error"));
  setBriefs([]);
 } finally {
  setLoading(false);
 }
 }, [selectedAgent, days]);

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

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 <div style={{ display: "flex", gap: 16, alignItems: "center", flexWrap: "wrap" }}>
 <div>
 <label style={{ fontSize: 11, color: C.textDim, fontWeight: 700, marginRight: 8 }}>Agent:</label>
 <select
 value={selectedAgent || ""}
 onChange={e => setSelectedAgent(e.target.value || null)}
 style={{ padding: "6px 10px", background: C.bg, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 12 }}
 >
 <option value="">All Agents</option>
 {AGENTS.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
 </select>
 </div>
 <div>
 <label style={{ fontSize: 11, color: C.textDim, fontWeight: 700, marginRight: 8 }}>Days:</label>
 <select
 value={days}
 onChange={e => setDays(parseInt(e.target.value))}
 style={{ padding: "6px 10px", background: C.bg, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 12 }}
 >
 <option value={1}>Last 1 day</option>
 <option value={7}>Last 7 days</option>
 <option value={30}>Last 30 days</option>
 </select>
 </div>
 <button onClick={loadData} disabled={loading} style={{ marginLeft: "auto", padding: "6px 12px", background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, fontSize: 12, cursor: loading ? "wait" : "pointer" }}>
 {loading ? "Loading…" : "↻ Refresh"}
 </button>
 {lastFetchAt && <span style={{ fontSize: 11, color: C.textMute }}>Last: {lastFetchAt.toLocaleTimeString()}</span>}
 </div>

 {error && (
 <div style={{ padding: 10, background: C.red + "18", color: C.red, borderRadius: 6, fontSize: 12 }}>
 Error: {error}
 </div>
 )}

 <SectionCard title="📋 Agent Activity Logs" subtitle={loading ? "Loading…" : `${(briefs && briefs.length) || 0} briefs`}>
 {loading ? (
 <div style={{ color: C.textDim, padding: 30, textAlign: "center" }}>Loading agent briefs…</div>
 ) : briefs.length > 0 ? (
 <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
 {briefs.map((brief, i) => (
 <div key={i} style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 8, padding: 12 }}>
 <div style={{ display: "flex", justifyContent: "space-between", alignItems: "start", marginBottom: 8 }}>
 <div>
 <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>{brief.agent_name}</div>
 <div style={{ fontSize: 11, color: C.textDim }}>{new Date(brief.created_at).toLocaleString()}</div>
 </div>
 <div style={{
 padding: "4px 8px",
 background: brief.status === "blocked" ? C.red : C.green,
 color: "white",
 borderRadius: 4,
 fontSize: 10,
 fontWeight: 700,
 }}>
 {brief.status === "blocked" ? "❌ Blocked" : "✅ Complete"}
 </div>
 </div>
 <div style={{ fontSize: 12, color: C.textDim, lineHeight: 1.5, marginBottom: 8 }}>{brief.summary}</div>
 {brief.what_blocked && <div style={{ fontSize: 11, color: C.yellow, marginBottom: 4 }}>🚧 Blocked: {brief.what_blocked}</div>}
 {brief.needs_owner && <div style={{ fontSize: 11, color: C.orange }}>⚠️ Needs owner action</div>}
 </div>
 ))}
 </div>
 ) : (
 <div style={{ color: C.textMute, padding: 16, textAlign: "center" }}>No briefs found for this period.</div>
 )}
 </SectionCard>
 </div>
 );
}

// ============================================================================
// PAGE: SYSTEM HEALTH — comprehensive view of every Toutmark dependency
// (relocated here from the customer-facing /status page; system health is
//  internal infrastructure, not customer-facing UX. Customers email
//  support@toutmark.com if they suspect an outage.)
// ============================================================================
function SystemHealthPage() {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);
 const [lastChecked, setLastChecked] = useState(null);

 const loadData = async () => {
  try {
   const res = await fetch("/api/health", { credentials: "include" });
   if (!res.ok) throw new Error(String(res.status));
   const json = await res.json();
   setData(json);
   setError(null);
   setLastChecked(new Date());
  } catch (e) {
   setError(e.message);
  } finally {
   setLoading(false);
  }
 };

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

 const dot = (status) => {
  const color = status === "green" ? C.green : status === "yellow" ? "#f59e0b" : status === "red" ? C.red : C.textDim;
  return <span style={{ display: "inline-block", width: 10, height: 10, borderRadius: "50%", background: color, marginRight: 8, verticalAlign: "middle" }} />;
 };

 const labels = {
  kv: "Cloudflare KV", d1: "Cloudflare D1", r2: "Cloudflare R2",
  mailrelay: "Mailrelay (transactional)", composio: "Composio (integrations)",
  stripe: "Stripe (billing)", openai: "OpenAI", perplexity: "Perplexity",
  gemini: "Gemini", anthropic: "Anthropic Claude",
 };

 if (loading) return <div style={{ color: C.textDim, padding: "1rem" }}>Loading system health…</div>;
 if (error) return (
  <div style={{ padding: "1rem" }}>
   <h2 style={{ marginBottom: "0.5rem" }}>System health</h2>
   <div style={{ background: C.red + "18", border: `1px solid ${C.red}`, borderRadius: 8, padding: "0.85rem 1rem", color: C.red }}>
    Unable to reach <code>/api/health</code> · {error}. Retry, or check the worker deploy logs.
   </div>
   <button onClick={loadData} style={{ marginTop: "0.85rem", padding: "0.4rem 0.9rem" }}>Retry</button>
  </div>
 );

 const services = (data && data.services) || {};
 const phase = (data && data.phase) || "unknown";

 return (
  <div style={{ padding: "0.5rem 0 2rem" }}>
   <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "1rem" }}>
    <div>
     <h2 style={{ margin: 0 }}>System health</h2>
     <div style={{ color: C.textDim, fontSize: "0.88rem", marginTop: 2 }}>
      Phase: <strong style={{ color: C.text }}>{phase}</strong> · last check {lastChecked ? lastChecked.toLocaleTimeString() : "—"} · auto-refresh 30s
     </div>
    </div>
    <button onClick={loadData} style={{ padding: "0.45rem 0.9rem" }}>Refresh now</button>
   </div>

   <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 12, overflow: "hidden", marginBottom: "1.25rem" }}>
    {Object.entries(labels).map(([k, label]) => (
     <div key={k} style={{ display: "flex", alignItems: "center", padding: "0.85rem 1rem", borderBottom: `1px solid ${C.border}`, justifyContent: "space-between" }}>
      <div>{dot(services[k])}<span style={{ fontWeight: 500 }}>{label}</span></div>
      <span style={{ fontSize: "0.85rem", color: C.textDim, textTransform: "capitalize" }}>{services[k] || "unknown"}</span>
     </div>
    ))}
   </div>

   <div style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 12, padding: "0.85rem 1rem", fontSize: "0.88rem", color: C.textDim }}>
    <strong style={{ color: C.text }}>Where this lives:</strong> system health is internal-only on the Owner Command Center. It used to be a customer-facing page at toutmark.com/status, but we relocated it on 2026-05-09 — exposing component names was unnecessary attack surface and customers don't benefit from seeing per-service health. If a customer suspects an outage they email <code>support@toutmark.com</code>.
   </div>
  </div>
 );
}

// ============================================================================
// PAGE: PLATFORM ACCOUNTS — LinkedIn, Reddit, X status
// Hardened 2026-05-19: 12s fetch timeout, shape-tolerant (array OR map),
// always renders cards even on error, manual refresh.
// ============================================================================
function PlatformAccountsPage() {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);
 const [lastFetchAt, setLastFetchAt] = useState(null);

 // Whitelist of platforms we actively manage for posting. Worker may return
 // more (Wikidata, G2, etc.); we filter to just the posting-relevant set so
 // this page mirrors the "posting accounts" mental model the Owner expects.
 const POSTING_PLATFORMS = ["LinkedIn", "Reddit", "X"];

 const loadData = React.useCallback(async () => {
 const controller = new AbortController();
 const timeoutId = setTimeout(() => controller.abort(), 12000);
 try {
  const res = await fetch("/api/platforms/health", { credentials: "include", signal: controller.signal, cache: "no-store" });
  clearTimeout(timeoutId);
  const text = await res.text();
  let json = null;
  try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON */ }
  if (!res.ok) throw new Error((json && (json.error || json.message)) || `HTTP ${res.status}`);
  if (!json) throw new Error("Response was not JSON");
  setData(json);
  setError(null);
  setLastFetchAt(new Date());
 } catch (e) {
  clearTimeout(timeoutId);
  setError(e.name === "AbortError" ? "Request timed out after 12s" : (e.message || "Network error"));
 } finally {
  setLoading(false);
 }
 }, []);

 useEffect(() => {
 loadData();
 const timer = setInterval(loadData, 30000);
 return () => clearInterval(timer);
 }, [loadData]);

 // Worker returns { ok: true, platforms: [ {platform, label, has_global_token, installs, healthy, ...} ] }
 // Be tolerant in case it returns an object map instead.
 const platformMap = React.useMemo(() => {
  if (!data) return {};
  const raw = data.platforms;
  const map = {};
  if (Array.isArray(raw)) {
   for (const p of raw) {
    const key = (p.platform || p.label || "").toLowerCase();
    if (key) map[key] = p;
   }
  } else if (raw && typeof raw === "object") {
   for (const [k, v] of Object.entries(raw)) map[k.toLowerCase()] = v;
  }
  return map;
 }, [data]);

 const statusColor = (status) => {
 if (status === "healthy" || status === true) return C.green;
 if (status === "expiring" || status === "manual") return C.yellow;
 if (status === "unknown" || status == null) return C.textMute;
 return C.red;
 };

 const statusLabel = (pdata) => {
 if (pdata.healthy === true || pdata.status === "healthy") return "✅ Healthy";
 if (pdata.status === "expiring") return "⚠️ Expiring";
 if (pdata.has_global_token === false && (pdata.installs || 0) === 0) return "⚠️ Not connected";
 if (pdata.status === "unknown" || pdata.status == null) return "— Unknown";
 return "❌ Failed";
 };

 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
   <div>
    <h2 style={{ margin: 0, fontSize: 18, color: C.text }}>Platform Accounts</h2>
    <div style={{ fontSize: 12, color: C.textDim, marginTop: 2 }}>OAuth + token health across active posting platforms. Auto-refreshes every 30s.</div>
   </div>
   <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
    {lastFetchAt && <span style={{ fontSize: 11, color: C.textMute }}>Last check: {lastFetchAt.toLocaleTimeString()}</span>}
    <button onClick={() => { setLoading(true); loadData(); }} disabled={loading} style={{ padding: "6px 12px", background: C.panelHi, color: C.text, border: `1px solid ${C.border}`, borderRadius: 6, fontSize: 12, cursor: loading ? "wait" : "pointer" }}>
     {loading ? "Loading…" : "↻ Refresh"}
    </button>
   </div>
  </div>

  {error && (
   <div style={{ padding: 10, background: C.red + "18", color: C.red, borderRadius: 6, fontSize: 12 }}>
    Error: {error}
   </div>
  )}

  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 14 }}>
   {POSTING_PLATFORMS.map((platform, i) => {
    const pdata = platformMap[platform.toLowerCase()] || {};
    return (
     <div key={i} style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
       <div style={{ fontSize: 14, fontWeight: 700, color: C.text }}>{platform}</div>
       <span style={{ color: statusColor(pdata.healthy === true ? "healthy" : pdata.status), fontWeight: 700, fontSize: 12 }}>{statusLabel(pdata)}</span>
      </div>

      <div style={{ fontSize: 11, color: C.textDim, marginBottom: 8 }}>
       Token: <span style={{ color: pdata.has_global_token ? C.green : C.textMute, fontWeight: 600 }}>{pdata.has_global_token ? "configured" : "missing"}</span>
      </div>

      {typeof pdata.installs === "number" && (
       <div style={{ fontSize: 11, color: C.textDim, marginBottom: 8 }}>
        Customer installs: <span style={{ color: C.text, fontWeight: 600 }}>{pdata.installs}</span>
       </div>
      )}

      {pdata.last_check && (
       <div style={{ fontSize: 11, color: C.textDim, marginBottom: 8 }}>
        Last check: <span style={{ color: C.text }}>{new Date(pdata.last_check).toLocaleString()}</span>
       </div>
      )}

      {pdata.last_status && (
       <div style={{ fontSize: 11, color: C.textDim, marginBottom: 12 }}>
        Last status: <span style={{ color: C.text }}>{pdata.last_status}</span>
       </div>
      )}

      {!pdata.healthy && (
       <button onClick={() => { window.location.href = `/api/oauth/start/${platform.toLowerCase()}`; }} style={{ width: "100%", padding: "6px 12px", background: C.blue, color: "white", border: "none", borderRadius: 6, fontSize: 11, fontWeight: 700, cursor: "pointer" }}>
        Re-authenticate
       </button>
      )}
     </div>
    );
   })}
  </div>

  {data && !error && (
   <div style={{ fontSize: 11, color: C.textMute, fontStyle: "italic" }}>
    Showing {POSTING_PLATFORMS.length} of {Object.keys(platformMap).length} platforms returned by /api/platforms/health (filtered to posting platforms).
   </div>
  )}
 </div>
 );
}

// ============================================================================
// APP — root component, sidebar nav + page router, wraps in GatewayProvider
// ============================================================================

// ============================================================================
// CRM PAGE — full customer record system, D1-backed
// Endpoints: /api/crm/dashboard, /api/crm/customers, /api/crm/customers/:id, /api/crm/customers/:id/:relation
// Added 2026-05-05
// ============================================================================
function CRMPage() {
 const [view, setView] = useState("list"); // list | detail | new
 const [customers, setCustomers] = useState([]);
 const [stats, setStats] = useState(null);
 const [recentActivity, setRecentActivity] = useState([]);
 const [search, setSearch] = useState("");
 const [statusFilter, setStatusFilter] = useState("");
 const [tierFilter, setTierFilter] = useState("");
 const [selectedId, setSelectedId] = useState(null);
 const [detail, setDetail] = useState(null);
 const [loading, setLoading] = useState(true);
 const [busy, setBusy] = useState(false);
 const [error, setError] = useState(null);
 const [newCustomer, setNewCustomer] = useState({
 brand_name: "",
 primary_email: "",
 website_url: "",
 industry: "",
 industry_category: "",
 regulated_industry: 0,
 regulator_category: "",
 tier: "standard",
 subscription_status: "trialing",
 });
 const [tab, setTab] = useState("today");

 const loadList = async () => {
 setLoading(true);
 setError(null);
 const params = new URLSearchParams();
 if (search) params.set("q", search);
 if (statusFilter) params.set("status", statusFilter);
 if (tierFilter) params.set("tier", tierFilter);
 // Load list and dashboard INDEPENDENTLY — a network error or partial outage on
 // one endpoint shouldn't blank the whole page. Each side reports its own error.
 const listErrors = [];
 try {
   const listResp = await fetch(`/api/crm/customers?${params.toString()}`, { credentials: "include" });
   if (listResp.ok) {
     const j = await listResp.json();
     setCustomers(j.customers || []);
   } else {
     const j = await listResp.json().catch(() => ({}));
     listErrors.push(`Customers list: ${j.message || j.error || `HTTP ${listResp.status}`}`);
   }
 } catch (e) {
   listErrors.push(`Customers list: ${e.message || e}`);
 }
 try {
   const dashResp = await fetch(`/api/crm/dashboard`, { credentials: "include" });
   if (dashResp.ok) {
     const j = await dashResp.json();
     setStats(j.stats || null);
     setRecentActivity(j.recent_activity || []);
   } else {
     const j = await dashResp.json().catch(() => ({}));
     listErrors.push(`Dashboard: ${j.message || j.error || `HTTP ${dashResp.status}`}`);
   }
 } catch (e) {
   listErrors.push(`Dashboard: ${e.message || e}`);
 }
 if (listErrors.length) setError(listErrors.join(" · "));
 setLoading(false);
 };

 const loadDetail = async (id) => {
 setLoading(true);
 try {
 const resp = await fetch(`/api/crm/customers/${id}`, { credentials: "include" });
 if (resp.ok) {
 const j = await resp.json();
 setDetail(j);
 setView("detail");
 setSelectedId(id);
 } else {
 setError(`Detail load failed: ${resp.status}`);
 }
 } catch (e) { setError(e.message); }
 setLoading(false);
 };

 useEffect(() => { if (view === "list") loadList(); /* eslint-disable-next-line */ }, [view, statusFilter, tierFilter]);

 const onSearch = (e) => { e.preventDefault(); loadList(); };

 const createCustomer = async () => {
 if (!newCustomer.brand_name || !newCustomer.primary_email) {
 alert("Brand name and primary email are required.");
 return;
 }
 setBusy(true);
 try {
 const resp = await fetch(`/api/crm/customers`, {
 method: "POST", credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify(newCustomer),
 });
 if (resp.ok) {
 const j = await resp.json();
 setView("list");
 setNewCustomer({ brand_name: "", primary_email: "", website_url: "", industry: "", industry_category: "", regulated_industry: 0, regulator_category: "", tier: "standard", subscription_status: "trialing" });
 loadList();
 if (j.customer?.id) loadDetail(j.customer.id);
 } else {
 const j = await resp.json().catch(() => ({}));
 alert(`Create failed: ${j.error || resp.status}`);
 }
 } catch (e) { alert(`Error: ${e.message}`); }
 setBusy(false);
 };

 const patchField = async (id, field, value) => {
 setBusy(true);
 try {
 const resp = await fetch(`/api/crm/customers/${id}`, {
 method: "PATCH", credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ [field]: value }),
 });
 if (resp.ok) loadDetail(id);
 else { const j = await resp.json().catch(()=>({})); alert(`Update failed: ${j.error || resp.status}`); }
 } catch (e) { alert(`Error: ${e.message}`); }
 setBusy(false);
 };

 const addNote = async () => {
 const body = prompt("Note text:");
 if (!body || !selectedId) return;
 setBusy(true);
 try {
 const resp = await fetch(`/api/crm/customers/${selectedId}/notes`, {
 method: "POST", credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ body, author: "owner" }),
 });
 if (resp.ok) loadDetail(selectedId);
 } catch (e) { alert(e.message); }
 setBusy(false);
 };

 const addContact = async () => {
 const name = prompt("Contact name:"); if (!name) return;
 const role = prompt("Role (owner/cco/marketer/tech/billing/other):") || "other";
 const email = prompt("Email:") || "";
 const phone = prompt("Phone (optional):") || "";
 setBusy(true);
 try {
 const resp = await fetch(`/api/crm/customers/${selectedId}/contacts`, {
 method: "POST", credentials: "include",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ name, role, email, phone, is_primary: 0, is_billing: role === "billing" ? 1 : 0, is_compliance: role === "cco" ? 1 : 0 }),
 });
 if (resp.ok) loadDetail(selectedId);
 } catch (e) { alert(e.message); }
 setBusy(false);
 };

 const triggerBackup = async () => {
 if (!confirm("Trigger an immediate R2 backup of every CRM table?")) return;
 setBusy(true);
 try {
 const resp = await fetch(`/api/crm/backup-now`, { method: "POST", credentials: "include" });
 const j = await resp.json();
 alert(resp.ok ? `Backup OK. Day=${j.day}. Rows: ${Object.entries(j.summary||{}).map(([k,v])=>`${k}=${v}`).join(", ")}` : `Backup failed: ${j.error || resp.status}`);
 } catch (e) { alert(e.message); }
 setBusy(false);
 };

 const migrateFromKv = async () => {
 if (!confirm("Backfill D1 from existing customers:* KV records? (Skips any already in D1.)")) return;
 setBusy(true);
 try {
 const resp = await fetch(`/api/crm/migrate-from-kv`, { method: "POST", credentials: "include" });
 const j = await resp.json();
 alert(resp.ok ? `Migrated: ${j.migrated}, Skipped: ${j.skipped}, Errors: ${(j.errors||[]).length}` : `Failed: ${j.error || resp.status}`);
 if (resp.ok) loadList();
 } catch (e) { alert(e.message); }
 setBusy(false);
 };

 if (view === "new") {
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
 <SectionCard title="Add Customer" subtitle="Create a new CRM record">
 <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
 {[
 ["brand_name", "Brand name *"],
 ["primary_email", "Primary email *"],
 ["website_url", "Website URL"],
 ["industry", "Industry (free text)"],
 ].map(([f, l]) => (
 <div key={f}>
 <div style={{ fontSize: 11, color: C.textDim, marginBottom: 4 }}>{l}</div>
 <input value={newCustomer[f] || ""} onChange={(e)=>setNewCustomer({...newCustomer, [f]: e.target.value})}
 style={{ width: "100%", padding: "8px 10px", background: C.panel, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 13 }} />
 </div>
 ))}
 <div>
 <div style={{ fontSize: 11, color: C.textDim, marginBottom: 4 }}>Industry category</div>
 <select value={newCustomer.industry_category} onChange={(e)=>setNewCustomer({...newCustomer, industry_category: e.target.value})}
 style={{ width: "100%", padding: "8px 10px", background: C.panel, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 13 }}>
 <option value="">— select —</option>
 <option value="saas">SaaS</option>
 <option value="ecommerce">E-commerce</option>
 <option value="local">Local service</option>
 <option value="regulated_finance">Finance (regulated)</option>
 <option value="regulated_legal">Legal (regulated)</option>
 <option value="regulated_health">Health (regulated)</option>
 <option value="other">Other</option>
 </select>
 </div>
 <div>
 <div style={{ fontSize: 11, color: C.textDim, marginBottom: 4 }}>Tier</div>
 <select value={newCustomer.tier} onChange={(e)=>setNewCustomer({...newCustomer, tier: e.target.value})}
 style={{ width: "100%", padding: "8px 10px", background: C.panel, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 13 }}>
 <option value="standard">Standard ($79)</option>
 <option value="growth">Growth ($149)</option>
 <option value="scale">Scale ($229)</option>
 </select>
 </div>
 <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
 <input id="cust_reg" type="checkbox" checked={!!newCustomer.regulated_industry} onChange={(e)=>setNewCustomer({...newCustomer, regulated_industry: e.target.checked ? 1 : 0})} />
 <label htmlFor="cust_reg" style={{ fontSize: 13, color: C.text }}>Regulated industry</label>
 </div>
 {newCustomer.regulated_industry ? (
 <div>
 <div style={{ fontSize: 11, color: C.textDim, marginBottom: 4 }}>Regulator category</div>
 <select value={newCustomer.regulator_category} onChange={(e)=>setNewCustomer({...newCustomer, regulator_category: e.target.value})}
 style={{ width: "100%", padding: "8px 10px", background: C.panel, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 13 }}>
 <option value="">— select —</option>
 <option value="securities">Securities</option>
 <option value="health">Health</option>
 <option value="legal">Legal</option>
 <option value="gambling">Gambling</option>
 <option value="cannabis">Cannabis</option>
 <option value="other">Other</option>
 </select>
 </div>
 ) : null}
 </div>
 <div style={{ display: "flex", gap: 8, marginTop: 16 }}>
 <button disabled={busy} onClick={createCustomer} style={{ background: C.green, color: "white", border: "none", padding: "9px 16px", borderRadius: 6, fontSize: 13, fontWeight: 700, cursor: "pointer" }}>{busy ? "Creating…" : "Create customer"}</button>
 <button onClick={()=>setView("list")} style={{ background: C.panel, color: C.text, border: `1px solid ${C.border}`, padding: "9px 16px", borderRadius: 6, fontSize: 13, cursor: "pointer" }}>Cancel</button>
 </div>
 </SectionCard>
 </div>
 );
 }

 if (view === "detail" && detail) {
 const c = detail.customer;
 const fmt = (ms) => ms ? new Date(ms).toLocaleString() : "—";
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
 <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
 <button onClick={()=>setView("list")} style={{ background: C.panel, color: C.text, border: `1px solid ${C.border}`, padding: "6px 12px", borderRadius: 6, fontSize: 12, cursor: "pointer" }}>← Back to list</button>
 <div style={{ fontSize: 18, fontWeight: 700 }}>{c.brand_name}</div>
 <div style={{ fontSize: 11, color: C.textDim, fontFamily: "ui-monospace, monospace" }}>{c.id}</div>
 {c.regulated_industry ? <span style={{ background: C.yellow, color: "#000", padding: "2px 8px", borderRadius: 4, fontSize: 10, fontWeight: 700 }}>REGULATED · {c.regulator_category || ""}</span> : null}
 <span style={{ background: c.subscription_status === "active" ? C.green : C.violet, color: "white", padding: "2px 8px", borderRadius: 4, fontSize: 10, fontWeight: 700 }}>{c.subscription_status?.toUpperCase()}</span>
 </div>

 {(() => {
   const openTasks = (detail.tasks || []).filter(t => t.status === "queued" || t.status === "in_progress").length;
   const openApprovals = (detail.approvals || []).filter(a => a.status === "queued").length;
   const todayBadge = openTasks + openApprovals;
   return (
 <div style={{ display: "flex", gap: 4, borderBottom: `1px solid ${C.border}`, flexWrap: "wrap" }}>
 {[
 ["today", `🎯 Today${todayBadge ? ` (${todayBadge})` : ""}`],
 ["overview", "Overview"],
 ["contacts", `Contacts (${detail.contacts?.length || 0})`],
 ["deliverables", `Deliverables (${detail.deliverables?.length || 0})`],
 ["tasks", `Tasks (${detail.tasks?.length || 0})`],
 ["approvals", `Approvals (${detail.approvals?.length || 0})`],
 ["notes", `Notes (${detail.notes?.length || 0})`],
 ["contact-log", `Contact Log (${detail.contact_log?.length || 0})`],
 ["billing", `Billing (${detail.billing_events?.length || 0})`],
 ["audit", `Audit (${detail.audit_log?.length || 0})`],
 ].map(([id, label]) => (
 <button key={id} onClick={()=>setTab(id)}
 style={{ background: tab === id ? C.panelHi : "transparent", color: tab === id ? C.text : C.textDim, border: "none", borderBottom: tab === id ? `2px solid ${C.accent}` : "2px solid transparent", padding: "8px 14px", fontSize: 12, cursor: "pointer", fontWeight: tab === id ? 600 : 400 }}>{label}</button>
 ))}
 </div>
 );
 })()}

 {tab === "today" && (() => {
   const openTasks = (detail.tasks || []).filter(t => t.status === "queued" || t.status === "in_progress").slice(0, 10);
   const openApprovals = (detail.approvals || []).filter(a => a.status === "queued").slice(0, 10);
   const recentDeliverables = (detail.deliverables || []).slice(0, 5);
   const recentContact = (detail.contact_log || []).slice(0, 5);
   const recentAudit = (detail.audit_log || []).slice(0, 5);
   const fmtDate = (ms) => ms ? new Date(ms).toLocaleString() : "—";
   const priColor = (p) => p === "P0" ? C.red : p === "P1" ? C.orange : C.yellow;
   return (
   <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
     <div style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16 }}>
       <div style={{ fontSize: 13, color: C.textDim, lineHeight: 1.6 }}>
         <strong style={{ color: C.text }}>What's happening with {c.brand_name} right now.</strong> Open tasks are things the customer (or an agent) still needs to do. Pending approvals are items waiting for sign-off. The recent activity strips show the last few touches across deliverables, contact, and the audit log — enough context to know if this customer is healthy without scrolling through every tab.
       </div>
     </div>

     <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
       <SectionCard title={`📋 Open tasks (${openTasks.length})`} subtitle="What the customer or an agent still needs to do">
         {openTasks.length === 0 ? <div style={{ color: C.textMute, fontSize: 12, fontStyle: "italic" }}>None open. Customer is current.</div> : (
           <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
             {openTasks.map(t => (
               <div key={t.id} style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 6, padding: 10 }}>
                 <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 4 }}>
                   <span style={{ background: priColor(t.priority), color: "#000", fontSize: 9, fontWeight: 800, padding: "1px 6px", borderRadius: 3 }}>{t.priority}</span>
                   <span style={{ fontSize: 12, fontWeight: 600, color: C.text }}>{t.title}</span>
                 </div>
                 <div style={{ fontSize: 11, color: C.textDim, lineHeight: 1.5 }}>{t.description || ""}</div>
                 <div style={{ fontSize: 10, color: C.textMute, marginTop: 4 }}>
                   {t.kind} · {t.assigned_to_agent ? `→ ${t.assigned_to_agent}` : "unassigned"} · {t.status} {t.due_date ? `· due ${fmtDate(t.due_date)}` : ""}
                 </div>
               </div>
             ))}
           </div>
         )}
       </SectionCard>
       <SectionCard title={`✅ Pending approvals (${openApprovals.length})`} subtitle="Waiting on customer / Owner / CCO sign-off">
         {openApprovals.length === 0 ? <div style={{ color: C.textMute, fontSize: 12, fontStyle: "italic" }}>No approvals queued.</div> : (
           <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
             {openApprovals.map(a => (
               <div key={a.id} style={{ background: C.panelHi, border: `1px solid ${C.border}`, borderRadius: 6, padding: 10 }}>
                 <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 4 }}>
                   <span style={{ background: priColor(a.priority), color: "#000", fontSize: 9, fontWeight: 800, padding: "1px 6px", borderRadius: 3 }}>{a.priority}</span>
                   <span style={{ fontSize: 12, fontWeight: 600, color: C.text }}>{a.kind}</span>
                 </div>
                 <div style={{ fontSize: 10, color: C.textMute }}>approver: {a.approver_role} · queued {fmtDate(a.created_at)}{a.expires_at ? ` · expires ${fmtDate(a.expires_at)}` : ""}</div>
               </div>
             ))}
           </div>
         )}
       </SectionCard>
     </div>

     <SectionCard title="🚀 Recent deliverables (last 5)" subtitle="What we've shipped for this customer most recently">
       {recentDeliverables.length === 0 ? <div style={{ color: C.textMute, fontSize: 12, fontStyle: "italic" }}>No deliverables yet.</div> : (
         <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
           {recentDeliverables.map(d => (
             <div key={d.id} style={{ display: "flex", gap: 12, padding: 8, borderBottom: `1px solid ${C.borderSoft}` }}>
               <div style={{ fontSize: 11, color: C.textMute, fontFamily: "ui-monospace, monospace", minWidth: 130 }}>{fmtDate(d.created_at)}</div>
               <div style={{ fontSize: 11, color: C.accent, minWidth: 130 }}>{d.kind}</div>
               <div style={{ flex: 1, fontSize: 12, color: C.text }}>{d.title || "(untitled)"}</div>
               <div style={{ fontSize: 10, color: d.status === "published" ? C.green : d.status === "rejected" ? C.red : C.yellow, textTransform: "uppercase", fontWeight: 700 }}>{d.status}</div>
             </div>
           ))}
         </div>
       )}
     </SectionCard>

     <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
       <SectionCard title="📬 Recent contact (last 5)" subtitle="Emails, SMS, dashboard chats">
         {recentContact.length === 0 ? <div style={{ color: C.textMute, fontSize: 12, fontStyle: "italic" }}>No recorded contact yet.</div> : (
           <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
             {recentContact.map(m => (
               <div key={m.id} style={{ fontSize: 11, padding: 6, borderBottom: `1px solid ${C.borderSoft}` }}>
                 <div style={{ display: "flex", gap: 8 }}>
                   <span style={{ color: m.direction === "inbound" ? C.lime : C.violet, fontWeight: 700 }}>{m.direction === "inbound" ? "← IN" : "→ OUT"}</span>
                   <span style={{ color: C.textMute }}>{m.channel}</span>
                   <span style={{ color: C.textMute, marginLeft: "auto" }}>{fmtDate(m.created_at)}</span>
                 </div>
                 <div style={{ color: C.text, marginTop: 2 }}>{m.subject || "(no subject)"}</div>
               </div>
             ))}
           </div>
         )}
       </SectionCard>
       <SectionCard title="📜 Recent audit events (last 5)" subtitle="Latest immutable log entries">
         {recentAudit.length === 0 ? <div style={{ color: C.textMute, fontSize: 12, fontStyle: "italic" }}>No audit entries yet.</div> : (
           <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
             {recentAudit.map(a => (
               <div key={a.id} style={{ fontSize: 11, padding: 6, borderBottom: `1px solid ${C.borderSoft}` }}>
                 <div style={{ display: "flex", gap: 8 }}>
                   <span style={{ color: C.accent, fontWeight: 700 }}>{a.event_type}</span>
                   <span style={{ color: C.textMute, marginLeft: "auto" }}>{fmtDate(a.created_at)}</span>
                 </div>
                 <div style={{ color: C.text, marginTop: 2 }}>{a.summary || a.event_kind || ""}</div>
               </div>
             ))}
           </div>
         )}
       </SectionCard>
     </div>
   </div>
   );
 })()}

 {tab === "overview" && (
 <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
 <SectionCard title="Identity">
 <KV k="Brand" v={c.brand_name} />
 <KV k="Legal entity" v={c.legal_entity_name || "—"} />
 <KV k="Email" v={c.primary_email} />
 <KV k="Phone" v={c.primary_phone || "—"} />
 <KV k="Website" v={c.website_url || "—"} />
 <KV k="Industry" v={c.industry || "—"} />
 <KV k="HQ" v={[c.hq_city, c.hq_state, c.hq_country].filter(Boolean).join(", ") || "—"} />
 <KV k="Timezone" v={c.timezone || "—"} />
 </SectionCard>
 <SectionCard title="Billing">
 <KV k="Tier" v={c.tier} />
 <KV k="Status" v={c.subscription_status} />
 <KV k="MRR" v={`$${((c.mrr_cents||0)/100).toFixed(2)}`} />
 <KV k="Stripe customer" v={c.stripe_customer_id || "—"} />
 <KV k="Stripe sub" v={c.stripe_subscription_id || "—"} />
 <KV k="Signup" v={fmt(c.signup_date)} />
 <KV k="Next renewal" v={fmt(c.next_renewal_at)} />
 <KV k="Founding" v={c.founding_member ? "Yes ($25 onboarding fee waived)" : "No"} />
 </SectionCard>
 <SectionCard title="Lifecycle / Install">
 <KV k="Onboarding stage" v={c.onboarding_stage} />
 <KV k="Initial rewrite" v={c.initial_rewrite_done ? "Done" : "Pending"} />
 <KV k="AI disclosure" v={c.ai_disclosure_accepted ? `Accepted ${fmt(c.ai_disclosure_accepted_at)}` : "Not accepted"} />
 <KV k="CMS" v={c.cms_platform || "—"} />
 <KV k="Install method" v={c.install_method || "—"} />
 <KV k="Primary host" v={c.primary_install_host || "—"} />
 <KV k="Install verified" v={fmt(c.install_verified_at)} />
 </SectionCard>
 <SectionCard title="Approval modes">
 {["haro_approval_mode","press_release_approval_mode","blog_approval_mode","rewrite_approval_mode","review_response_approval_mode","schema_approval_mode","social_post_approval_mode"].map(f => (
 <KV key={f} k={f.replace(/_/g, " ")} v={c[f] || "—"} />
 ))}
 </SectionCard>
 <SectionCard title="Delivery counters (lifetime)">
 <KV k="Paragraph rewrites" v={c.paragraph_rewrites_done_lifetime} />
 <KV k="Blog posts shipped" v={c.blog_posts_shipped_lifetime} />
 <KV k="FAQ items shipped" v={c.faq_items_shipped_lifetime} />
 <KV k="HARO pitches" v={c.haro_pitches_submitted_lifetime} />
 <KV k="Press releases" v={c.press_releases_submitted_lifetime} />
 <KV k="Wikidata edits" v={c.wikidata_edits_count} />
 <KV k="Citation reports" v={c.citation_reports_generated} />
 <KV k="Schema pages" v={c.schema_pages_published} />
 </SectionCard>
 <SectionCard title="Audit / activity">
 <KV k="Last audit score" v={c.last_audit_score != null ? `${c.last_audit_score}/100` : "—"} />
 <KV k="Last audit" v={fmt(c.last_audit_at)} />
 <KV k="Last citation report" v={fmt(c.last_citation_report_at)} />
 <KV k="Last activity" v={fmt(c.last_activity_at)} />
 <KV k="Created" v={fmt(c.created_at)} />
 <KV k="Updated" v={fmt(c.updated_at)} />
 </SectionCard>
 </div>
 )}

 {tab === "contacts" && (
 <SectionCard title="Contacts" subtitle="Multiple per customer (owner, CCO, billing, tech)">
 <button onClick={addContact} disabled={busy} style={{ background: C.accent, color: "#000", border: "none", padding: "6px 12px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer", marginBottom: 8 }}>+ Add contact</button>
 <table style={{ width: "100%", fontSize: 12 }}>
 <thead><tr style={{ borderBottom: `1px solid ${C.border}` }}>{["Name","Role","Email","Phone","Flags"].map(h=><th key={h} style={{ textAlign: "left", padding: 8, color: C.textDim }}>{h}</th>)}</tr></thead>
 <tbody>
 {(detail.contacts || []).map(x => (
 <tr key={x.id} style={{ borderBottom: `1px solid ${C.borderSoft}` }}>
 <td style={{ padding: 8 }}>{x.name}</td>
 <td style={{ padding: 8 }}>{x.role}</td>
 <td style={{ padding: 8 }}>{x.email || "—"}</td>
 <td style={{ padding: 8 }}>{x.phone || "—"}</td>
 <td style={{ padding: 8 }}>{[x.is_primary&&"primary",x.is_billing&&"billing",x.is_compliance&&"compliance"].filter(Boolean).join(" · ") || "—"}</td>
 </tr>
 ))}
 </tbody>
 </table>
 </SectionCard>
 )}

 {tab === "deliverables" && (
 <SectionCard title="Deliverables" subtitle="Every artifact shipped or queued">
 <table style={{ width: "100%", fontSize: 12 }}>
 <thead><tr style={{ borderBottom: `1px solid ${C.border}` }}>{["Kind","Title","Status","Approved by","Published","Agent","Cost"].map(h=><th key={h} style={{ textAlign: "left", padding: 8, color: C.textDim }}>{h}</th>)}</tr></thead>
 <tbody>
 {(detail.deliverables || []).slice(0, 100).map(d => (
 <tr key={d.id} style={{ borderBottom: `1px solid ${C.borderSoft}` }}>
 <td style={{ padding: 8 }}>{d.kind}</td>
 <td style={{ padding: 8 }}>{d.title || d.url || d.id}</td>
 <td style={{ padding: 8 }}>{d.status}</td>
 <td style={{ padding: 8 }}>{d.approved_by || "—"}</td>
 <td style={{ padding: 8 }}>{d.published_at ? new Date(d.published_at).toLocaleDateString() : "—"}</td>
 <td style={{ padding: 8 }}>{d.generated_by_agent || "—"}</td>
 <td style={{ padding: 8 }}>${((d.cost_cents||0)/100).toFixed(2)}</td>
 </tr>
 ))}
 </tbody>
 </table>
 </SectionCard>
 )}

 {tab === "tasks" && (
 <SectionCard title="Tasks" subtitle="Per-customer next-actions">
 <table style={{ width: "100%", fontSize: 12 }}>
 <thead><tr style={{ borderBottom: `1px solid ${C.border}` }}>{["Title","Kind","Priority","Status","Agent","Due"].map(h=><th key={h} style={{ textAlign: "left", padding: 8, color: C.textDim }}>{h}</th>)}</tr></thead>
 <tbody>
 {(detail.tasks || []).map(t => (
 <tr key={t.id} style={{ borderBottom: `1px solid ${C.borderSoft}` }}>
 <td style={{ padding: 8 }}>{t.title}</td>
 <td style={{ padding: 8 }}>{t.kind}</td>
 <td style={{ padding: 8 }}>{t.priority}</td>
 <td style={{ padding: 8 }}>{t.status}</td>
 <td style={{ padding: 8 }}>{t.assigned_to_agent || "—"}</td>
 <td style={{ padding: 8 }}>{t.due_date ? new Date(t.due_date).toLocaleDateString() : "—"}</td>
 </tr>
 ))}
 </tbody>
 </table>
 </SectionCard>
 )}

 {tab === "approvals" && (
 <SectionCard title="Approvals" subtitle="Items waiting for customer/owner/CCO sign-off">
 <table style={{ width: "100%", fontSize: 12 }}>
 <thead><tr style={{ borderBottom: `1px solid ${C.border}` }}>{["Kind","Approver","Priority","Status","Created"].map(h=><th key={h} style={{ textAlign: "left", padding: 8, color: C.textDim }}>{h}</th>)}</tr></thead>
 <tbody>
 {(detail.approvals || []).map(a => (
 <tr key={a.id} style={{ borderBottom: `1px solid ${C.borderSoft}` }}>
 <td style={{ padding: 8 }}>{a.kind}</td>
 <td style={{ padding: 8 }}>{a.approver_role}</td>
 <td style={{ padding: 8 }}>{a.priority}</td>
 <td style={{ padding: 8 }}>{a.status}</td>
 <td style={{ padding: 8 }}>{a.created_at ? new Date(a.created_at).toLocaleString() : "—"}</td>
 </tr>
 ))}
 </tbody>
 </table>
 </SectionCard>
 )}

 {tab === "notes" && (
 <SectionCard title="Notes" subtitle="Owner-only; append-only (immutable)">
 <button onClick={addNote} disabled={busy} style={{ background: C.accent, color: "#000", border: "none", padding: "6px 12px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer", marginBottom: 8 }}>+ Add note</button>
 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 {(detail.notes || []).map(n => (
 <div key={n.id} style={{ background: C.panel, border: `1px solid ${C.border}`, borderRadius: 8, padding: 10 }}>
 <div style={{ fontSize: 11, color: C.textDim, marginBottom: 4 }}>{n.author} · {new Date(n.created_at).toLocaleString()}{n.pinned ? " · 📌" : ""}</div>
 <div style={{ fontSize: 13, color: C.text, whiteSpace: "pre-wrap" }}>{n.body}</div>
 </div>
 ))}
 </div>
 </SectionCard>
 )}

 {tab === "contact-log" && (
 <SectionCard title="Contact log" subtitle="Every email/SMS/call/chat">
 <table style={{ width: "100%", fontSize: 12 }}>
 <thead><tr style={{ borderBottom: `1px solid ${C.border}` }}>{["Date","Direction","Channel","Subject","From / To","Intent"].map(h=><th key={h} style={{ textAlign: "left", padding: 8, color: C.textDim }}>{h}</th>)}</tr></thead>
 <tbody>
 {(detail.contact_log || []).map(l => (
 <tr key={l.id} style={{ borderBottom: `1px solid ${C.borderSoft}` }}>
 <td style={{ padding: 8 }}>{new Date(l.created_at).toLocaleString()}</td>
 <td style={{ padding: 8 }}>{l.direction}</td>
 <td style={{ padding: 8 }}>{l.channel}</td>
 <td style={{ padding: 8 }}>{l.subject || "—"}</td>
 <td style={{ padding: 8, fontSize: 11, color: C.textDim }}>{l.direction === "inbound" ? l.from_address : l.to_address}</td>
 <td style={{ padding: 8 }}>{l.intent || "—"}{l.escalated_to_owner ? " · ⬆ owner" : ""}</td>
 </tr>
 ))}
 </tbody>
 </table>
 </SectionCard>
 )}

 {tab === "billing" && (
 <SectionCard title="Billing events" subtitle="Stripe webhooks">
 <table style={{ width: "100%", fontSize: 12 }}>
 <thead><tr style={{ borderBottom: `1px solid ${C.border}` }}>{["Date","Type","Amount","Invoice","Stripe event"].map(h=><th key={h} style={{ textAlign: "left", padding: 8, color: C.textDim }}>{h}</th>)}</tr></thead>
 <tbody>
 {(detail.billing_events || []).map(b => (
 <tr key={b.id} style={{ borderBottom: `1px solid ${C.borderSoft}` }}>
 <td style={{ padding: 8 }}>{new Date(b.created_at).toLocaleString()}</td>
 <td style={{ padding: 8 }}>{b.event_type}</td>
 <td style={{ padding: 8 }}>{b.amount_cents != null ? `$${(b.amount_cents/100).toFixed(2)}` : "—"}</td>
 <td style={{ padding: 8, fontFamily: "ui-monospace, monospace", fontSize: 11 }}>{b.invoice_id || "—"}</td>
 <td style={{ padding: 8, fontFamily: "ui-monospace, monospace", fontSize: 11 }}>{b.stripe_event_id || "—"}</td>
 </tr>
 ))}
 </tbody>
 </table>
 </SectionCard>
 )}

 {tab === "audit" && (
 <SectionCard title="Audit log" subtitle="Append-only; 7y retention for regulated, 90d for non-regulated">
 <table style={{ width: "100%", fontSize: 12 }}>
 <thead><tr style={{ borderBottom: `1px solid ${C.border}` }}>{["Date","Type","Kind","Actor","Summary"].map(h=><th key={h} style={{ textAlign: "left", padding: 8, color: C.textDim }}>{h}</th>)}</tr></thead>
 <tbody>
 {(detail.audit_log || []).map(a => (
 <tr key={a.id} style={{ borderBottom: `1px solid ${C.borderSoft}` }}>
 <td style={{ padding: 8 }}>{new Date(a.created_at).toLocaleString()}</td>
 <td style={{ padding: 8 }}>{a.event_type}</td>
 <td style={{ padding: 8 }}>{a.event_kind || "—"}</td>
 <td style={{ padding: 8 }}>{a.actor_type}{a.actor_id ? ` · ${a.actor_id}` : ""}</td>
 <td style={{ padding: 8 }}>{a.summary || "—"}</td>
 </tr>
 ))}
 </tbody>
 </table>
 </SectionCard>
 )}
 </div>
 );
 }

 // LIST VIEW (default)
 return (
 <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
 <div>
 <div style={{ fontSize: 22, fontWeight: 800, color: C.text, letterSpacing: -0.3 }}>CRM (full record)</div>
 <div style={{ fontSize: 12, color: C.textDim, marginTop: 2 }}>Every customer with full record — HARO submissions, blog posts, paragraph rewrites, approvals, tasks. D1-backed.</div>
 </div>
 {error ? <div style={{ background: "#3a1a1a", border: `1px solid ${C.red}`, color: C.red, padding: 10, borderRadius: 6, fontSize: 12 }}>{error}</div> : null}

 {stats && (
 <div style={{ display: "grid", gridTemplateColumns: "repeat(6, 1fr)", gap: 10 }}>
 <MetricTile label="Active" value={String(stats.active)} accent={C.green} />
 <MetricTile label="Trialing" value={String(stats.trialing)} accent={C.violet} />
 <MetricTile label="Regulated" value={String(stats.regulated)} accent={C.yellow} />
 <MetricTile label="MRR" value={`$${((stats.mrr_cents_total||0)/100).toFixed(0)}`} accent={C.accent} />
 <MetricTile label="Open tasks" value={String(stats.open_tasks)} accent={C.blue} />
 <MetricTile label="Open approvals" value={String(stats.open_approvals)} accent={C.orange} />
 </div>
 )}

 <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
 <form onSubmit={onSearch} style={{ display: "flex", gap: 6 }}>
 <input value={search} onChange={(e)=>setSearch(e.target.value)} placeholder="Search brand / email / domain…"
 style={{ padding: "8px 12px", background: C.panel, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 13, width: 280 }} />
 <button type="submit" style={{ background: C.accent, color: "#000", border: "none", padding: "8px 14px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Search</button>
 </form>
 <select value={statusFilter} onChange={(e)=>setStatusFilter(e.target.value)} style={{ padding: "8px 10px", background: C.panel, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 12 }}>
 <option value="">All statuses</option>
 <option value="active">Active</option>
 <option value="trialing">Trialing</option>
 <option value="past_due">Past due</option>
 <option value="paused">Paused</option>
 <option value="cancelled">Cancelled</option>
 </select>
 <select value={tierFilter} onChange={(e)=>setTierFilter(e.target.value)} style={{ padding: "8px 10px", background: C.panel, border: `1px solid ${C.border}`, borderRadius: 6, color: C.text, fontSize: 12 }}>
 <option value="">All tiers</option>
 <option value="standard">Standard</option>
 <option value="growth">Growth</option>
 <option value="scale">Scale</option>
 </select>
 <div style={{ flex: 1 }} />
 <button onClick={()=>setView("new")} style={{ background: C.green, color: "white", border: "none", padding: "8px 14px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>+ Add customer</button>
 <button onClick={migrateFromKv} disabled={busy} style={{ background: C.violet, color: "white", border: "none", padding: "8px 14px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Backfill from KV</button>
 <button onClick={triggerBackup} disabled={busy} style={{ background: C.blue, color: "white", border: "none", padding: "8px 14px", borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: "pointer" }}>Backup now → R2</button>
 </div>

 <SectionCard title="Customer roster" subtitle={loading ? "Loading…" : `${customers.length} customers`}>
 {loading ? <div style={{ color: C.textDim }}>Loading…</div> : customers.length === 0 ? <div style={{ color: C.textDim, padding: 20, textAlign: "center" }}>No customers yet.</div> : (
 <div style={{ overflowX: "auto" }}>
 <table style={{ width: "100%", fontSize: 12, borderCollapse: "collapse" }}>
 <thead>
 <tr style={{ borderBottom: `1px solid ${C.border}` }}>
 {["Brand","Email","Tier","Status","MRR","Stage","Regulated","Signup","Activity"].map(h => <th key={h} style={{ textAlign: "left", padding: "10px 8px", color: C.textDim, fontWeight: 700 }}>{h}</th>)}
 </tr>
 </thead>
 <tbody>
 {customers.map(c => (
 <tr key={c.id} onClick={()=>loadDetail(c.id)} style={{ cursor: "pointer", borderBottom: `1px solid ${C.borderSoft}` }}>
 <td style={{ padding: "10px 8px", fontWeight: 600, color: C.text }}>{c.brand_name}</td>
 <td style={{ padding: "10px 8px", color: C.textDim }}>{c.primary_email}</td>
 <td style={{ padding: "10px 8px" }}>{c.tier}</td>
 <td style={{ padding: "10px 8px", color: c.subscription_status === "active" ? C.green : C.textDim }}>{c.subscription_status}</td>
 <td style={{ padding: "10px 8px" }}>${((c.mrr_cents||0)/100).toFixed(0)}</td>
 <td style={{ padding: "10px 8px", color: C.textDim }}>{c.onboarding_stage}</td>
 <td style={{ padding: "10px 8px" }}>{c.regulated_industry ? "⚖️ " + (c.regulator_category || "") : "—"}</td>
 <td style={{ padding: "10px 8px", color: C.textDim }}>{c.signup_date ? new Date(c.signup_date).toLocaleDateString() : "—"}</td>
 <td style={{ padding: "10px 8px", color: C.textDim }}>{c.last_activity_at ? new Date(c.last_activity_at).toLocaleDateString() : "—"}</td>
 </tr>
 ))}
 </tbody>
 </table>
 </div>
 )}
 </SectionCard>

 {recentActivity.length > 0 && (
 <SectionCard title="Recent CRM activity" subtitle="Last 50 audit events across all customers">
 <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
 {recentActivity.slice(0, 30).map(a => (
 <div key={a.id} style={{ fontSize: 11, color: C.textDim, padding: "4px 0", borderBottom: `1px solid ${C.borderSoft}` }}>
 <span style={{ color: C.textMute }}>{new Date(a.created_at).toLocaleString()}</span>
 {" · "}<span style={{ color: C.accent }}>{a.event_type}</span>
 {" · "}<span style={{ fontFamily: "ui-monospace, monospace" }}>{a.customer_id}</span>
 {" — "}{a.summary || "(no summary)"}
 </div>
 ))}
 </div>
 </SectionCard>
 )}
 </div>
 );
}

// Small key/value renderer used in CRM detail
function KV({ k, v }) {
 return (
 <div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0", borderBottom: `1px solid ${C.borderSoft}`, fontSize: 12, gap: 16 }}>
 <span style={{ color: C.textDim, textTransform: "capitalize" }}>{k}</span>
 <span style={{ color: C.text, textAlign: "right", fontFamily: typeof v === "string" && v.includes("@") ? "ui-monospace, monospace" : undefined }}>{v == null || v === "" ? "—" : String(v)}</span>
 </div>
 );
}




// CustomerRosterPage — lightweight roster (use CRM page for full record)
function CustomerRosterPage() {
 const [customers, setCustomers] = React.useState(null);
 const [err, setErr] = React.useState("");
 React.useEffect(() => {
  fetch("/api/customers/roster", { credentials: "include" })
   .then(r => r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`))
   .then(j => setCustomers(j.customers || j.roster || []))
   .catch(e => setErr(String(e)));
 }, []);
 return React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 16 } },
  React.createElement("div", { style: { fontSize: 22, fontWeight: 800, color: C.text } }, "Customer Roster"),
  React.createElement("div", { style: { fontSize: 13, color: C.textDim, lineHeight: 1.6 } },
   "All customers with their plan tier, status, and last activity. For full CRM record (HARO submissions, blog posts, paragraph rewrites, etc.) open CRM (full record)."),
  err && React.createElement("div", { style: { padding: 12, background: `${C.red}22`, border: `1px solid ${C.red}44`, borderRadius: 8, color: C.red, fontSize: 13 } }, "Error: ", err),
  customers === null && !err && React.createElement("div", { style: { padding: 24, color: C.textDim, textAlign: "center" } }, "Loading roster..."),
  customers && customers.length === 0 && React.createElement("div", { style: { padding: 48, color: C.textDim, textAlign: "center", border: `1px dashed ${C.border}`, borderRadius: 12, background: C.panel } }, "No customers yet."),
  customers && customers.length > 0 && React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 12, overflow: "hidden" } },
   customers.map((c, i) => React.createElement("div", { key: c.id || c.customer_id || i, style: { padding: "12px 16px", borderBottom: i < customers.length - 1 ? `1px solid ${C.borderSoft}` : "none", display: "flex", justifyContent: "space-between", alignItems: "center" } },
    React.createElement("div", null,
     React.createElement("div", { style: { fontWeight: 600, color: C.text } }, c.brand_name || c.name || c.customer_id || "(no name)"),
     React.createElement("div", { style: { fontSize: 11, color: C.textMute, marginTop: 2 } }, c.primary_email || c.email || ""),
    ),
    React.createElement("div", { style: { display: "flex", gap: 8, alignItems: "center" } },
     c.tier && React.createElement("span", { style: { fontSize: 11, padding: "2px 8px", borderRadius: 4, background: C.panelHi, color: C.text } }, c.tier),
     c.subscription_status && React.createElement("span", { style: { fontSize: 11, padding: "2px 8px", borderRadius: 4, background: c.subscription_status === "active" ? `${C.green}22` : C.panelHi, color: c.subscription_status === "active" ? C.green : C.textDim } }, c.subscription_status)
    )
   ))
  )
 );
}

// OnboardingTrackerPage — shows where each customer is in the onboarding flow
function OnboardingTrackerPage() {
 const [data, setData] = React.useState(null);
 const [err, setErr] = React.useState("");
 React.useEffect(() => {
  fetch("/api/customers/onboarding", { credentials: "include" })
   .then(r => r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`))
   .then(j => setData(j))
   .catch(e => setErr(String(e)));
 }, []);
 const customers = data?.customers || data?.customers_in_onboarding || [];
 return React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 16 } },
  React.createElement("div", { style: { fontSize: 22, fontWeight: 800, color: C.text } }, "Onboarding Tracker"),
  React.createElement("div", { style: { fontSize: 13, color: C.textDim, lineHeight: 1.6 } },
   "Each customer's progress through the 20-step signup wizard + first-month onboarding. Watches for stalls — Owner alerts trigger when a stage hasn't progressed in 24h."),
  err && React.createElement("div", { style: { padding: 12, background: `${C.red}22`, border: `1px solid ${C.red}44`, borderRadius: 8, color: C.red, fontSize: 13 } }, "Error: ", err),
  data === null && !err && React.createElement("div", { style: { padding: 24, color: C.textDim, textAlign: "center" } }, "Loading onboarding state..."),
  data && customers.length === 0 && React.createElement("div", { style: { padding: 48, color: C.textDim, textAlign: "center", border: `1px dashed ${C.border}`, borderRadius: 12, background: C.panel } }, "No customers in onboarding yet."),
  data && customers.length > 0 && React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } },
   customers.map((c, i) => React.createElement("div", { key: c.customer_id || i, style: { padding: 16, background: C.panel, border: `1px solid ${C.border}`, borderRadius: 8 } },
    React.createElement("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 8 } },
     React.createElement("div", { style: { fontWeight: 600, color: C.text } }, c.brand_name || c.customer_id),
     React.createElement("div", { style: { fontSize: 12, color: c.stalled ? C.red : C.textDim } }, "Stage: ", c.onboarding_stage || "unknown")
    ),
    c.last_progress_at && React.createElement("div", { style: { fontSize: 11, color: C.textMute } }, "Last progress: ", new Date(c.last_progress_at).toLocaleString())
   ))
  )
 );
}

// APIKeysPage — read-only view of which API keys are configured (no values shown)
function APIKeysPage() {
 const [data, setData] = React.useState(null);
 const [err, setErr] = React.useState("");
 React.useEffect(() => {
  fetch("/api/health", { credentials: "include" })
   .then(r => r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`))
   .then(j => setData(j))
   .catch(e => setErr(String(e)));
 }, []);
 const config = data?.config || {};
 const keys = Object.keys(config).sort();
 return React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 16 } },
  React.createElement("div", { style: { fontSize: 22, fontWeight: 800, color: C.text } }, "API Keys"),
  React.createElement("div", { style: { fontSize: 13, color: C.textDim, lineHeight: 1.6 } },
   "Which API keys are configured on the worker. Values are never shown — only presence/absence. To add or change a key, edit it in Cloudflare Pages → Settings → Environment variables (encrypted Secrets only)."),
  err && React.createElement("div", { style: { padding: 12, background: `${C.red}22`, border: `1px solid ${C.red}44`, borderRadius: 8, color: C.red, fontSize: 13 } }, "Error: ", err),
  data === null && !err && React.createElement("div", { style: { padding: 24, color: C.textDim, textAlign: "center" } }, "Loading key config..."),
  data && React.createElement("div", { style: { background: C.panel, border: `1px solid ${C.border}`, borderRadius: 12, overflow: "hidden" } },
   keys.map((k, i) => React.createElement("div", { key: k, style: { padding: "10px 16px", borderBottom: i < keys.length - 1 ? `1px solid ${C.borderSoft}` : "none", display: "flex", justifyContent: "space-between", alignItems: "center" } },
    React.createElement("code", { style: { color: C.text, fontSize: 12 } }, k),
    React.createElement("span", { style: { fontSize: 11, padding: "2px 8px", borderRadius: 4, background: config[k] ? `${C.green}22` : `${C.red}22`, color: config[k] ? C.green : C.red } }, config[k] ? "CONFIGURED" : "MISSING")
   ))
  )
 );
}

// CEOLearningsPage — display CEO's accumulated learnings + clear button
function CEOLearningsPage() {
 const [learnings, setLearnings] = React.useState(null);
 const [err, setErr] = React.useState("");
 const load = React.useCallback(() => {
  fetch("/api/content-memory?kind=learnings", { credentials: "include" })
   .then(r => r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`))
   .then(j => setLearnings(j.learnings || j.items || []))
   .catch(e => setErr(String(e)));
 }, []);
 React.useEffect(() => { load(); }, [load]);
 return React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 16 } },
  React.createElement("div", { style: { fontSize: 22, fontWeight: 800, color: C.text } }, "CEO Learnings"),
  React.createElement("div", { style: { fontSize: 13, color: C.textDim, lineHeight: 1.6 } },
   "Compounding knowledge the CEO has accumulated about what works (campaigns that landed customers, content angles that drove citations, things to avoid). Auto-promoted from action log when a hypothesis is validated by outcome data."),
  err && React.createElement("div", { style: { padding: 12, background: `${C.red}22`, border: `1px solid ${C.red}44`, borderRadius: 8, color: C.red, fontSize: 13 } }, "Error: ", err),
  learnings === null && !err && React.createElement("div", { style: { padding: 24, color: C.textDim, textAlign: "center" } }, "Loading learnings..."),
  learnings && learnings.length === 0 && React.createElement("div", { style: { padding: 48, color: C.textDim, textAlign: "center", border: `1px dashed ${C.border}`, borderRadius: 12, background: C.panel } }, "No learnings recorded yet. CEO will accumulate these as Live Phase + Live Phase generate outcome data."),
  learnings && learnings.length > 0 && React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } },
   learnings.map((L, i) => React.createElement("div", { key: i, style: { padding: 16, background: C.panel, border: `1px solid ${C.border}`, borderRadius: 8 } },
    React.createElement("div", { style: { fontWeight: 600, color: C.text, marginBottom: 4 } }, L.title || L.summary || `Learning ${i+1}`),
    L.evidence && React.createElement("div", { style: { fontSize: 12, color: C.textDim, marginTop: 6 } }, "Evidence: ", L.evidence),
    L.recorded_at && React.createElement("div", { style: { fontSize: 11, color: C.textMute, marginTop: 6 } }, new Date(L.recorded_at).toLocaleString())
   ))
  )
 );
}

function MissingPagePlaceholder({ activeId }) {
 return React.createElement("div", { style: { padding: 24, display: "flex", flexDirection: "column", gap: 12 } },
  React.createElement("div", { style: { fontSize: 22, fontWeight: 800, color: C.text, letterSpacing: -0.4 } }, "Page not yet built"),
  React.createElement("div", { style: { fontSize: 13, color: C.textDim, lineHeight: 1.6 } },
   "This nav entry (", React.createElement("code", { style: { background: C.panel, padding: "2px 6px", borderRadius: 4, color: C.text } }, activeId || "unknown"),
   ") is wired in the sidebar but its page component is not yet implemented. The audit pass logged this for the next sprint."
  ),
  React.createElement("div", { style: { fontSize: 12, color: C.textMute, marginTop: 8 } }, "Until then, use Talk to CEO or Action Log for live ops.")
 );
}

function App() {
 // Read /admin/<route> from the URL so deep-links work (e.g. /admin/complaints).
 const initialActive = (() => {
 try {
 const p = (window.location.pathname || "").replace(/^\/admin\/?/, "").replace(/\/$/, "");
 if (p) return p;
 } catch {}
 return "dashboard";
 })();
 const [active, setActive] = useState(initialActive);
 const [collapsed, setCollapsed] = useState(false);
 const [selectedAgentId, setSelectedAgentId] = useState(null);
 const [selectedDeptId, setSelectedDeptId] = useState(null);
 const [complaintsBadge, setComplaintsBadge] = useState(0);

 // Poll /api/complaints/sla-status every 60s to update sidebar badge (red if past_sla > 0).
 useEffect(() => {
 const load = async () => {
 try {
 const r = await fetch("/api/complaints/sla-status", { credentials: "include" });
 if (!r.ok) return;
 const j = await r.json();
 setComplaintsBadge(j.past_sla || 0);
 } catch {}
 };
 load();
 const t = setInterval(load, 60000);
 return () => clearInterval(t);
 }, []);

 const onNavigate = (id) => {
 if (!id) return;
 if (id.startsWith("agent:")) {
 setSelectedAgentId(id.slice(6));
 setActive("agent");
 return;
 }
 if (id.startsWith("dept:")) {
 setSelectedDeptId(id.slice(5));
 setActive("department");
 return;
 }
 setActive(id);
 };

 const nav = [
 {
 label: "Overview",
 items: [
 { id: "dashboard", label: "Dashboard", icon: "🏠", color: C.accent },
 { id: "ceo-chat", label: "Talk to CEO", icon: "💬", color: C.accent },
 { id: "live-control", label: "Live Control", icon: "🎛️", color: C.violet },
 { id: "owner-queue", label: "Owner Queue", icon: "📋", color: C.orange },
 { id: "action-log", label: "Action Log", icon: "📜", color: C.blue },
 { id: "changes", label: "Changes & Revert", icon: "↩️", color: C.yellow },
 ],
 },
 {
 label: "Customers & Revenue",
 items: [
 { id: "customers", label: "Customer Roster", icon: "👥", color: C.accent },
 { id: "crm", label: "CRM (full record)", icon: "💼", color: C.accent },
 { id: "onboarding", label: "Onboarding Tracker", icon: "🛠️", color: C.yellow },
 { id: "stalled-approvals", label: "Stalled Approvals", icon: "⚠️", color: C.red },
 { id: "complaints", label: "Complaints", icon: "🚨", color: (complaintsBadge > 0) ? C.red : C.accent, badge: complaintsBadge > 0 ? String(complaintsBadge) : null },
 ],
 },
 {
 label: "Content & Publishing",
 items: [
 { id: "compliance-queue", label: "Compliance Queue", icon: "⚖️", color: C.yellow },
 { id: "paste-queue", label: "Paste Queue", icon: "📋", color: C.pink },
 { id: "wikidata-queue", label: "Wikidata Queue", icon: "📝", color: C.violet },
 ],
 },
 {
 label: "Marketing & Outreach",
 items: [
 { id: "cold-email-pace", label: "Cold Email Pace", icon: "📧", color: C.accent },
 { id: "platform-accounts", label: "Platform Accounts", icon: "🌐", color: C.blue },
 { id: "platform-status", label: "Platform Status", icon: "📡", color: C.blue },
 ],
 },
 {
 label: "DevOps & Monitoring",
 items: [
 { id: "system-health", label: "System Health", icon: "🩺", color: C.green },
 { id: "cron-status", label: "Cron Status", icon: "🕐", color: C.yellow },
 { id: "browser-sessions", label: "Browser Sessions", icon: "🍪", color: C.orange },
 { id: "regulator-queue", label: "Regulator Queue", icon: "⚖", color: C.red || C.orange },
 ],
 },
 {
 label: "Insights",
 items: [
 { id: "agent-briefs", label: "Agent Activity", icon: "📊", color: C.lime },
 { id: "citation-reports", label: "Citation Reports", icon: "📑", color: C.lime },
 { id: "customer-flows", label: "Customer Flows", icon: "🚪", color: C.accent },
 ],
 },
 {
 label: "Agents",
 items: AGENTS.map(a => ({
 id: `agent:${a.id}`, label: a.name, icon: a.icon, color: a.color,
 })),
 },
 {
 label: "Departments",
 items: DEPARTMENTS.map(d => ({
 id: `dept:${d.id}`, label: d.name, icon: d.icon, color: d.accent,
 })),
 },
 {
 label: "Operations",
 items: [
 { id: "api-keys", label: "API Keys", icon: "🔑", color: C.teal },
 { id: "learnings", label: "CEO Learnings", icon: "🧠", color: C.violet },
 { id: "spend", label: "Spend & Balances", icon: "💰", color: C.lime, externalHref: "/admin/spend.html" },
 { id: "referrals", label: "Referrals", icon: "📈", color: C.cyan },
 { id: "dm-queue", label: "DM Queue", icon: "✉️", color: C.violet },
 { id: "dm-candidates", label: "DM Candidates", icon: "🔍", color: C.teal },
 { id: "inbox", label: "Inbox", icon: "📥", color: C.accent, externalHref: "/admin/inbox.html" },
 { id: "haro-queue", label: "HARO Queue", icon: "📰", color: C.violet, externalHref: "/admin/haro-queue.html" },
 { id: "press-queue", label: "Press Queue", icon: "📣", color: C.orange, externalHref: "/admin/press-queue.html" },
 { id: "legal", label: "Legal Library", icon: "⚖️", color: C.yellow, externalHref: "/admin/legal.html" },
 { id: "billing", label: "Customer Revenue", icon: "💳", color: C.lime },
 ],
 }];
 const sidebarActive = active === "agent" && selectedAgentId ? `agent:${selectedAgentId}` : active === "department" && selectedDeptId ? `dept:${selectedDeptId}` : active;
 let page;
 if (active === "dashboard") page = React.createElement(DashboardPage, { onNavigate, agents: AGENTS });
 else if (active === "ceo-chat") page = React.createElement(CEOChatPage, { onNavigate });
 else if (active === "live-control") page = React.createElement(LiveControlPage, null);
 else if (active === "owner-queue") page = React.createElement(ApprovalQueuePage, null);
 else if (active === "action-log") page = React.createElement(ActionLogPage, null);
 else if (active === "changes") page = (typeof ChangesPage === "function") ? React.createElement(ChangesPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "customer-roster") page = React.createElement(CustomerRosterPage, null);
 else if (active === "crm") page = (typeof CRMPage === "function") ? React.createElement(CRMPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "onboarding") page = (typeof OnboardingTrackerPage === "function") ? React.createElement(OnboardingTrackerPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "stalled-approvals") page = (typeof StalledApprovalsPage === "function") ? React.createElement(StalledApprovalsPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "compliance-queue") page = (typeof ComplianceQueuePage === "function") ? React.createElement(ComplianceQueuePage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "paste-queue") page = (typeof PasteQueuePage === "function") ? React.createElement(PasteQueuePage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "wikidata-queue") page = (typeof WikidataQueuePage === "function") ? React.createElement(WikidataQueuePage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "cold-email-pace") page = (typeof ColdEmailPacePage === "function") ? React.createElement(ColdEmailPacePage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "platform-accounts") page = (typeof PlatformAccountsPage === "function") ? React.createElement(PlatformAccountsPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "platform-status") page = (typeof PlatformStatusPage === "function") ? React.createElement(PlatformStatusPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "cron-status") page = (typeof CronStatusPage === "function") ? React.createElement(CronStatusPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "browser-sessions") page = (typeof BrowserSessionsPage === "function") ? React.createElement(BrowserSessionsPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "agent-briefs") page = (typeof AgentBriefsPage === "function") ? React.createElement(AgentBriefsPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "citation-reports") page = (typeof CitationReportsPage === "function") ? React.createElement(CitationReportsPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "customer-flows") page = (typeof CustomerFlowsPage === "function") ? React.createElement(CustomerFlowsPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "api-keys") page = (typeof APIKeysPage === "function") ? React.createElement(APIKeysPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "learnings") page = (typeof CEOLearningsPage === "function") ? React.createElement(CEOLearningsPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "billing") page = (typeof BillingPage === "function") ? React.createElement(BillingPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "referrals") page = (typeof ReferralsPage === "function") ? React.createElement(ReferralsPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "dm-queue") page = (typeof DMQueuePage === "function") ? React.createElement(DMQueuePage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "dm-candidates") page = (typeof DMCandidatesPage === "function") ? React.createElement(DMCandidatesPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "customers") page = React.createElement(CustomerRosterPage, null);
 else if (active === "complaints") page = (typeof ComplaintsPage === "function") ? React.createElement(ComplaintsPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "system-health") page = (typeof SystemHealthPage === "function") ? React.createElement(SystemHealthPage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "regulator-queue") page = (typeof RegulatorQueuePage === "function") ? React.createElement(RegulatorQueuePage, null) : React.createElement(MissingPagePlaceholder, { activeId: active });
 else if (active === "agent" && selectedAgentId) {
  const agent = AGENTS.find(a => a.id === selectedAgentId);
  page = agent ? React.createElement(AgentPage, { agent, onNavigate }) : React.createElement(MissingPagePlaceholder, { activeId: active });
 } else if (active === "department" && selectedDeptId) {
  const dept = DEPARTMENTS.find(d => d.id === selectedDeptId);
  page = dept ? React.createElement(DepartmentPage, { dept, onNavigate }) : React.createElement(MissingPagePlaceholder, { activeId: active });
 } else {
  page = React.createElement(DashboardPage, { onNavigate, agents: AGENTS });
 }
 return React.createElement(GatewayProvider, null,
  React.createElement("div", { style: { display: "flex", height: "100vh", width: "100vw", background: C.bg, color: C.text, overflow: "hidden" } },
   React.createElement(Sidebar, { nav, active: sidebarActive, onSelect: onNavigate, collapsed, onToggleCollapsed: () => setCollapsed(c => !c) }),
   React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } },
    React.createElement("div", { style: { padding: "10px 24px", borderBottom: "1px solid " + C.border, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, background: C.panel } },
     React.createElement("div", { style: { fontSize: 12, color: C.textMute, letterSpacing: 0.4 } }, "OpenClaw Command Center · Toutmark · Owner: River"),
     (typeof GatewayStatusPill === "function") ? React.createElement(GatewayStatusPill, null) : null
    ),
    React.createElement("div", { style: { flex: 1, overflowY: "auto", padding: 24 } }, page)
   )
  )
 );
}

ReactDOM.createRoot(document.getElementById("root")).render(React.createElement(App, null));
