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

// ─── Auth: apiFetch wrapper + useAuth hook + LoginScreen ─────────────────────
// (Phase 3 additions — kept at the top so Phase 2's tenant edits stay isolated.)

// apiFetch: drop-in fetch replacement that attaches the Bearer session token and
// hard-resets on 401 so the LoginScreen re-mounts. Same call signature as fetch.
function apiFetch(url, opts = {}) {
  const token = localStorage.getItem("atlas_session");
  const headers = new Headers(opts.headers || {});
  if (token) headers.set("Authorization", "Bearer " + token);
  const next = { ...opts, headers };
  return fetch(url, next).then(r => {
    if (r.status === 401) {
      localStorage.removeItem("atlas_session");
      localStorage.removeItem("atlas_wallet");
      window.location.reload();
    }
    return r;
  });
}

// Phase 13h: read a cookie value from document.cookie. Used so we can
// degrade gracefully when localStorage is blocked or scrubbed.
function _readCookie(name) {
  try {
    const parts = (document.cookie || "").split(";");
    for (const p of parts) {
      const [k, ...rest] = p.trim().split("=");
      if (k === name) return rest.join("=");
    }
  } catch (e) {}
  return null;
}

// Phase 13h: collect device meta for fingerprint stability.
function _deviceMeta() {
  try {
    return {
      screen: `${(window.screen && window.screen.width) || 0}x${(window.screen && window.screen.height) || 0}`,
      dpr: window.devicePixelRatio || 1,
      tz: Intl.DateTimeFormat().resolvedOptions().timeZone || "",
    };
  } catch (e) { return {}; }
}

// useAuth: localStorage-backed session state with cookie fallback (Phase 13h).
// Exposes token, wallet, me, login, logout.
function useAuth() {
  const [token, setToken] = useState(() =>
    localStorage.getItem("atlas_session") || _readCookie("nixzlab_session") || null);
  const [wallet, setWallet] = useState(() => localStorage.getItem("atlas_wallet") || null);
  const [me, setMe] = useState(null);

  // re-read on mount in case another tab logged in
  useEffect(() => {
    const t = localStorage.getItem("atlas_session") || _readCookie("nixzlab_session");
    const w = localStorage.getItem("atlas_wallet");
    if (t !== token) setToken(t);
    if (w !== wallet) setWallet(w);
  }, []);

  // when we have a wallet, fetch role/tenant info for Phase 4 to consume
  useEffect(() => {
    if (!token || !wallet) { setMe(null); return; }
    apiFetch("/api/me.json?wallet=" + encodeURIComponent(wallet))
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d) setMe(d); })
      .catch(() => {});
  }, [token, wallet]);

  const login = useCallback((newToken, newWallet) => {
    if (newToken) localStorage.setItem("atlas_session", newToken);
    if (newWallet) localStorage.setItem("atlas_wallet", newWallet);
    setToken(newToken || null);
    setWallet(newWallet || null);
  }, []);

  const logout = useCallback(() => {
    localStorage.removeItem("atlas_session");
    localStorage.removeItem("atlas_wallet");
    setToken(null);
    setWallet(null);
    setMe(null);
  }, []);

  return { token, wallet, me, login, logout };
}

// LoginScreen: wallet OTP via /api/proxy/auth/wallet/otp/{request,verify}
// (or /api/proxy/auth/wallet/email[/verify] when the identifier looks like an
// email). One field — branches on the '@' character.
function LoginScreen({ onLogin }) {
  // Phase 13h: prefer cookie email hint, fall back to empty.
  const _hint = _readCookie("nixzlab_email_hint") || "";
  const [identifier, setIdentifier] = useState("");
  const [code, setCode] = useState("");
  const [step, setStep] = useState("request");  // 'request' | 'verify'
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  // Phase 13e: password mode toggle — email+password instead of OTP.
  const [mode, setMode] = useState("otp");      // 'otp' | 'password'
  const [pwEmail, setPwEmail] = useState(_hint);
  const [pwPass, setPwPass] = useState("");
  // Phase 13h: remember-device + magic-link
  const [rememberDevice, setRememberDevice] = useState(true);
  const [magicLink, setMagicLink] = useState(null);

  const isEmail = identifier.includes("@");
  const channel = isEmail ? "email" : "phone";

  const requestMagic = async (emailValue) => {
    const em = (emailValue || pwEmail || identifier || "").trim().toLowerCase();
    if (!em || !em.includes("@")) { setErr("Enter your email first."); return; }
    setErr(null); setBusy(true); setMagicLink(null);
    try {
      const r = await fetch("/api/identity/magic/request", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({email: em}),
      });
      const d = await r.json().catch(() => ({}));
      if (!r.ok || !d.ok) { setErr(d.error || ("Magic-link request failed (" + r.status + ")")); return; }
      setMagicLink(d.link);
    } catch (e) {
      setErr("Network error requesting magic link.");
    } finally { setBusy(false); }
  };

  const passwordLogin = async () => {
    setErr(null);
    if (!pwEmail.trim() || !pwPass) { setErr("Enter email and password."); return; }
    setBusy(true);
    try {
      const r = await fetch("/api/identity/password/login", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({email: pwEmail.trim().toLowerCase(), password: pwPass,
                              device_meta: _deviceMeta(),
                              remember_device: rememberDevice}),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok || !data.ok) { setErr(data.error || ("Login failed (" + r.status + ")")); return; }
      const sessionToken = data.session_token;
      const walletAddr   = data.wallet;
      if (!sessionToken) { setErr("Server returned no session token."); return; }
      onLogin(sessionToken, walletAddr);
    } catch (e) {
      setErr("Network error during password login.");
    } finally { setBusy(false); }
  };

  const requestOtp = async () => {
    setErr(null);
    if (!identifier.trim()) { setErr("Enter an email or phone number."); return; }
    setBusy(true);
    try {
      const body = isEmail ? { email: identifier.trim() } : { phone: identifier.trim() };
      const url = isEmail
        ? "/api/proxy/auth/wallet/email"
        : "/api/proxy/auth/wallet/otp/request";
      const r = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) { setErr(data.error || ("Couldn't send code (" + r.status + ")")); return; }
      setStep("verify");
    } catch (e) {
      setErr("Network error sending code.");
    } finally {
      setBusy(false);
    }
  };

  const verifyOtp = async () => {
    setErr(null);
    if (!code.trim()) { setErr("Enter the 6-digit code."); return; }
    setBusy(true);
    try {
      const body = isEmail
        ? { email: identifier.trim(), code: code.trim(),
            device_meta: _deviceMeta(), remember_device: rememberDevice }
        : { phone: identifier.trim(), code: code.trim(),
            device_meta: _deviceMeta(), remember_device: rememberDevice };
      const url = isEmail
        ? "/api/proxy/auth/wallet/email/verify"
        : "/api/proxy/auth/wallet/otp/verify";
      const r = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) { setErr(data.error || ("Invalid code (" + r.status + ")")); return; }
      const sessionToken = data.session_token || data.token;
      const walletAddr = data.wallet || data.wallet_address || data.sui_address;
      if (!sessionToken) { setErr("Server returned no session token."); return; }
      onLogin(sessionToken, walletAddr);
    } catch (e) {
      setErr("Network error verifying code.");
    } finally {
      setBusy(false);
    }
  };

  if (mode === "password") {
    return (
      <div className="empty" style={{ minHeight: "100vh", justifyContent: "center" }}>
        <h1>Sign in to Nixz<span style={{ color: "var(--accent)" }}>lab</span></h1>
        <p>Email + password. Set yours up first via Settings → Identity & Auth.</p>
        <div className="wf-card" style={{ width: 360, maxWidth: "90vw", cursor: "default", gap: 14 }}>
          <div style={{ display: "flex", flexDirection: "column", gap: 8, width: "100%" }}>
            <label style={{ fontSize: 12, color: "var(--fg2)" }}>email</label>
            <input type="email" placeholder="you@example.com" value={pwEmail}
                   disabled={busy} onChange={e => setPwEmail(e.target.value)}
                   onKeyDown={e => { if (e.key === "Enter") passwordLogin(); }}
                   style={{ background: "var(--panel2)", color: "var(--fg)",
                            border: "1px solid var(--line2)", borderRadius: 8,
                            padding: "10px 12px", fontSize: 14, width: "100%" }} />
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 8, width: "100%" }}>
            <label style={{ fontSize: 12, color: "var(--fg2)" }}>password</label>
            <input type="password" placeholder="••••••••" value={pwPass}
                   disabled={busy} onChange={e => setPwPass(e.target.value)}
                   onKeyDown={e => { if (e.key === "Enter") passwordLogin(); }}
                   style={{ background: "var(--panel2)", color: "var(--fg)",
                            border: "1px solid var(--line2)", borderRadius: 8,
                            padding: "10px 12px", fontSize: 14, width: "100%" }} />
          </div>
          {/* Phase 13h: remember-device toggle */}
          <label style={{ display: "flex", alignItems: "center", gap: 8, width: "100%",
                          fontSize: 12, color: "var(--fg2)", userSelect: "none" }}>
            <input type="checkbox" checked={rememberDevice}
                   onChange={e => setRememberDevice(e.target.checked)}
                   disabled={busy} />
            Remember this device
          </label>
          <button className="send-btn" onClick={passwordLogin} disabled={busy}
                  style={{ width: "100%", borderRadius: 8, padding: "10px 16px" }}>
            {busy ? "Signing in…" : "Sign in"}
          </button>
          {/* Phase 13h: magic-link request */}
          <button onClick={() => requestMagic(pwEmail)} disabled={busy}
                  style={{ width: "100%", borderRadius: 8, padding: "8px 16px",
                           background: "var(--panel2)", color: "var(--fg)",
                           border: "1px solid var(--line2)", cursor: "pointer", fontSize: 13 }}>
            {busy ? "…" : "Magic link instead"}
          </button>
          {magicLink && (
            <div style={{ width: "100%", padding: "10px 12px", borderRadius: 6,
                          background: "rgba(57,255,20,0.06)", border: "1px solid rgba(57,255,20,0.35)",
                          color: "var(--fg)", fontSize: 12, wordBreak: "break-all" }}>
              <div style={{ marginBottom: 6, color: "var(--accent)", fontWeight: 600 }}>
                Magic link (15 min, one-time):
              </div>
              <a href={magicLink} style={{ color: "var(--link)" }}>{magicLink}</a>
            </div>
          )}
          {err && (
            <div style={{ width: "100%", padding: "8px 10px", borderRadius: 6,
                          background: "rgba(255,80,80,0.08)", border: "1px solid rgba(255,80,80,0.4)",
                          color: "#ffb4b4", fontSize: 12 }}>{err}</div>
          )}
          <a href="#" onClick={(e) => { e.preventDefault(); setMode("otp"); setErr(null); }}
             style={{ color: "var(--accent)", fontSize: 12, textDecoration: "none", marginTop: 4 }}>
            Use email or phone OTP instead
          </a>
        </div>
      </div>
    );
  }

  return (
    <div className="empty" style={{ minHeight: "100vh", justifyContent: "center" }}>
      <h1>Sign in to Nixz<span style={{ color: "var(--accent)" }}>lab</span></h1>
      <p>One field — email or phone. We text/email a 6-digit code; no password needed.</p>
      <div className="wf-card" style={{ width: 360, maxWidth: "90vw", cursor: "default", gap: 14 }}>
        <div style={{ display: "flex", flexDirection: "column", gap: 8, width: "100%" }}>
          <label style={{ fontSize: 12, color: "var(--fg2)" }}>
            {step === "verify" ? channel + " (locked)" : "email or phone"}
          </label>
          <input
            type="text"
            placeholder="you@example.com  or  +15551234567"
            value={identifier}
            disabled={step === "verify" || busy}
            onChange={e => setIdentifier(e.target.value)}
            onKeyDown={e => { if (e.key === "Enter" && step === "request") requestOtp(); }}
            style={{
              background: "var(--panel2)", color: "var(--fg)",
              border: "1px solid var(--line2)", borderRadius: 8,
              padding: "10px 12px", fontSize: 14, width: "100%",
            }}
          />
        </div>
        {step === "request" && (
          <button className="send-btn" onClick={requestOtp} disabled={busy} style={{ width: "100%", borderRadius: 8, padding: "10px 16px" }}>
            {busy ? "Sending…" : "Send code"}
          </button>
        )}
        {step === "verify" && (
          <>
            <div style={{ display: "flex", flexDirection: "column", gap: 8, width: "100%" }}>
              <label style={{ fontSize: 12, color: "var(--fg2)" }}>6-digit code</label>
              <input
                type="text"
                inputMode="numeric"
                autoComplete="one-time-code"
                pattern="[0-9]*"
                maxLength={6}
                placeholder="123456"
                value={code}
                disabled={busy}
                onChange={e => setCode(e.target.value.replace(/[^0-9]/g, ""))}
                onKeyDown={e => { if (e.key === "Enter") verifyOtp(); }}
                style={{
                  background: "var(--panel2)", color: "var(--fg)",
                  border: "1px solid var(--line2)", borderRadius: 8,
                  padding: "10px 12px", fontSize: 18, letterSpacing: 4,
                  textAlign: "center", width: "100%",
                }}
                autoFocus
              />
            </div>
            <div style={{ display: "flex", gap: 8, width: "100%" }}>
              <button
                onClick={() => { setStep("request"); setCode(""); setErr(null); }}
                disabled={busy}
                style={{
                  flex: "0 0 auto", padding: "10px 14px", borderRadius: 8,
                  background: "var(--panel2)", color: "var(--fg2)",
                  border: "1px solid var(--line2)", cursor: "pointer",
                }}
              >
                ← change
              </button>
              <button className="send-btn" onClick={verifyOtp} disabled={busy} style={{ flex: 1, borderRadius: 8, padding: "10px 16px" }}>
                {busy ? "Verifying…" : "Verify"}
              </button>
            </div>
          </>
        )}
        {err && (
          <div style={{
            width: "100%", padding: "8px 10px", borderRadius: 6,
            background: "rgba(255,80,80,0.08)", border: "1px solid rgba(255,80,80,0.4)",
            color: "#ffb4b4", fontSize: 12,
          }}>
            {err}
          </div>
        )}
        {/* Phase 13h: magic-link option for OTP mode too */}
        <button onClick={() => requestMagic(identifier)} disabled={busy || !isEmail}
                style={{ width: "100%", borderRadius: 8, padding: "8px 16px",
                         background: "var(--panel2)", color: "var(--fg)",
                         border: "1px solid var(--line2)", cursor: "pointer", fontSize: 13 }}>
          {busy ? "…" : "Magic link instead (email only)"}
        </button>
        {magicLink && (
          <div style={{ width: "100%", padding: "10px 12px", borderRadius: 6,
                        background: "rgba(57,255,20,0.06)", border: "1px solid rgba(57,255,20,0.35)",
                        color: "var(--fg)", fontSize: 12, wordBreak: "break-all" }}>
            <div style={{ marginBottom: 6, color: "var(--accent)", fontWeight: 600 }}>
              Magic link (15 min, one-time):
            </div>
            <a href={magicLink} style={{ color: "var(--link)" }}>{magicLink}</a>
          </div>
        )}
        <label style={{ display: "flex", alignItems: "center", gap: 8, width: "100%",
                        fontSize: 12, color: "var(--fg2)", userSelect: "none" }}>
          <input type="checkbox" checked={rememberDevice}
                 onChange={e => setRememberDevice(e.target.checked)}
                 disabled={busy} />
          Remember this device
        </label>
        <a href="#" onClick={(e) => { e.preventDefault(); setMode("password"); setErr(null); }}
           style={{ color: "var(--accent)", fontSize: 12, textDecoration: "none", marginTop: 4 }}>
          Use email + password instead
        </a>
      </div>
    </div>
  );
}

// ─── Workflow taxonomy: top tabs (categories) and subtabs (specific flows) ───
// Phase 12a: 📦 Library is a special category — instead of filtering chats it
// renders the LibraryTab component (addons grid). The subtabs entry is empty
// because Library doesn't filter chats by workflow.
const CATEGORIES = [
  { id: "all",      label: "All",      subtabs: ["agents","mcp","image","video","3d","archviz"] },
  { id: "generate", label: "Generate", subtabs: ["image","video","3d"] },
  { id: "capture",  label: "Capture",  subtabs: ["archviz"] },
  { id: "library",  label: "📦 Library", subtabs: [], special: "library" },
  { id: "talk",     label: "Talk",     subtabs: ["agents","mcp"] },
  // Phase 13g: central I/O tab — sidebar items also point here.
  { id: "io",       label: "📥📤 I/O", subtabs: [], special: "io" },
];

// ─── Tenant config: slug-driven branding + chrome gating ─────────────────────
const DEFAULT_TENANT = {
  slug: "main",
  branding: {
    name: "Nixzlab",
    accent_color: "#39ff14",
    logo_url: null,
    font: null,
    favicon: null,
    custom_copy: { welcome: "Pick a workflow to start", send_button_label: "↑" },
  },
  allowed_workflows: ["archviz","video","image","3d","agents","mcp"],
  hidden_chrome: [],
  role_defaults: { signup: "admin" },
};

function useTenant() {
  const [tenant, setTenant] = useState(DEFAULT_TENANT);
  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const slug = params.get("slug") || "main";
    fetchJson(`/api/tenant.json?slug=${encodeURIComponent(slug)}`)
      .then(t => { if (t && t.branding) setTenant(t); })
      .catch(() => { /* keep DEFAULT_TENANT */ });
  }, []);

  // Apply branding side-effects whenever tenant changes
  useEffect(() => {
    const b = tenant.branding || {};
    if (b.accent_color) {
      document.documentElement.style.setProperty("--accent", b.accent_color);
      document.documentElement.style.setProperty("--accent2", b.accent_color);
    }
    if (b.name) document.title = b.name;
    if (b.font) {
      const id = "tenant-font-link";
      let link = document.getElementById(id);
      if (!link) {
        link = document.createElement("link");
        link.id = id; link.rel = "stylesheet";
        document.head.appendChild(link);
      }
      link.href = b.font;
    }
    if (b.favicon) {
      const id = "tenant-favicon-link";
      let link = document.getElementById(id);
      if (!link) {
        link = document.createElement("link");
        link.id = id; link.rel = "icon";
        document.head.appendChild(link);
      }
      link.href = b.favicon;
    }
  }, [tenant]);

  return tenant;
}

// ─── Helpers ─────────────────────────────────────────────────────────────────
const fetchJson = (u, opts={}) => fetch(u, opts).then(r => r.json());
const fetchText = (u, opts={}) => fetch(u, opts).then(r => r.text());

// Match the inline tokens emitted by server.py: cid:, space:cid:, image:, video:,
// pdf:, doc:, audio:, upload:chat=, approval:chat=…:field=…
const _PDF_RE = /^pdf:/, _DOC_RE = /^doc:/, _AUDIO_RE = /^audio:/;
const TOKEN_RE = /(space:cid:[0-9a-f]{16}|cid:[0-9a-f]{16}|image:\S+\.(?:png|jpg|jpeg|webp)|video:\S+\.mp4|pdf:\S+\.pdf|doc:\S+\.(?:docx|doc)|audio:\S+\.(?:mp3|wav|m4a|ogg)|upload:chat=[A-Za-z0-9_-]+|approval:chat=[A-Za-z0-9_-]+:field=[A-Za-z0-9_]+)/g;
// Map raw filesystem path → /uploads/ static route (server-side _msg_render does
// the same; mirror it here so React renders attachments correctly).
function _atlasSrc(p) {
  if (!p) return p;
  const upPrefix = "/data/Projects/ATLAS/uploads/";
  if (p.startsWith(upPrefix)) return "/uploads/" + p.slice(upPrefix.length);
  if (p.startsWith("/")) return p;
  return "/" + p.replace(/^\.\//, "");
}
function _basename(p) { return (p || "").split("/").pop(); }
function renderMessage(text, onUploaded, onMention) {
  const parts = [];
  let lastIdx = 0;
  let m;
  TOKEN_RE.lastIndex = 0;
  while ((m = TOKEN_RE.exec(text)) !== null) {
    if (m.index > lastIdx) parts.push(text.slice(lastIdx, m.index));
    const tok = m[0];
    if (tok.startsWith("space:cid:")) {
      const cid = tok.slice(10);
      parts.push(<iframe key={m.index} src={`/bundles/${cid}/space.html`} allow="fullscreen" style={{height:480}} />);
    } else if (tok.startsWith("cid:")) {
      const cid = tok.slice(4);
      parts.push(<iframe key={m.index} src={`/bundles/${cid}/play.html`} style={{height:340}} />);
    } else if (tok.startsWith("image:")) {
      const p = tok.slice(6);
      parts.push(<img key={m.index} src={_atlasSrc(p)} loading="lazy" />);
    } else if (tok.startsWith("video:")) {
      const p = tok.slice(6);
      parts.push(<video key={m.index} src={_atlasSrc(p)} controls autoPlay muted loop playsInline />);
    } else if (_PDF_RE.test(tok)) {
      // Phase 8: PDF token → inline iframe (browsers render PDFs natively).
      const p = tok.slice(4);
      const src = _atlasSrc(p) + "#toolbar=0";
      const fn = _basename(p);
      parts.push(
        <div key={m.index} className="pdf-card" style={{margin:"6px 0"}}>
          <iframe src={src} title={fn} loading="lazy"
            style={{width:"100%",height:420,border:"1px solid #2a2a33",borderRadius:6,background:"#fff"}} />
          <div style={{fontSize:11,color:"#7d8590",marginTop:4,display:"flex",gap:8,alignItems:"center"}}>
            <span>📄 {fn}</span>
            <a href={_atlasSrc(p)} target="_blank" rel="noreferrer" style={{color:"#58a6ff"}}>open</a>
            {onMention && (
              <a href="#" onClick={(e)=>{e.preventDefault(); onMention("@"+_basename(p).replace(/\W+/g,"_"));}}
                 style={{color:"#39ff14",textDecoration:"none"}}>▶ chat-with-doc</a>
            )}
          </div>
        </div>
      );
    } else if (_DOC_RE.test(tok)) {
      const p = tok.slice(4);
      const src = _atlasSrc(p);
      const fn = _basename(p);
      parts.push(
        <div key={m.index} className="doc-card"
             style={{margin:"6px 0",padding:10,background:"#15151c",border:"1px solid #2a2a33",borderRadius:6,display:"flex",gap:10,alignItems:"center"}}>
          <span style={{fontSize:22}}>📝</span>
          <div style={{flex:1}}>
            <div style={{color:"#e6edf3",fontSize:13}}>{fn}</div>
            <div style={{fontSize:11,color:"#7d8590"}}>word document</div>
          </div>
          <a href={src} download style={{color:"#58a6ff",fontSize:12,textDecoration:"none"}}>download</a>
          {onMention && (
            <a href="#" onClick={(e)=>{e.preventDefault(); onMention("@"+fn.replace(/\W+/g,"_"));}}
               style={{color:"#39ff14",fontSize:12,textDecoration:"none"}}>▶ chat-with-doc</a>
          )}
        </div>
      );
    } else if (_AUDIO_RE.test(tok)) {
      const p = tok.slice(6);
      const fn = _basename(p);
      parts.push(
        <div key={m.index} className="audio-card"
             style={{margin:"6px 0",padding:10,background:"#15151c",border:"1px solid #2a2a33",borderRadius:6}}>
          <div style={{fontSize:11,color:"#7d8590",marginBottom:6}}>🔊 {fn}</div>
          <audio src={_atlasSrc(p)} controls style={{width:"100%"}} />
        </div>
      );
    } else if (tok.startsWith("upload:chat=")) {
      const chat = tok.slice(12);
      parts.push(<UploadForm key={m.index} chat={chat} onUploaded={onUploaded} />);
    } else if (tok.startsWith("approval:chat=")) {
      // approval:chat=<chat>:field=<draft_field>
      const rest = tok.slice("approval:chat=".length);
      const sep = rest.indexOf(":field=");
      const chat = rest.slice(0, sep);
      const field = rest.slice(sep + ":field=".length);
      parts.push(<ApprovalCard key={m.index} chat={chat} field={field} onResolved={onUploaded} />);
    }
    lastIdx = m.index + tok.length;
  }
  if (lastIdx < text.length) parts.push(text.slice(lastIdx));
  return parts.length ? parts : [text];
}

function UploadForm({ chat, onUploaded }) {
  const [busy, setBusy] = useState(false);
  const [pct, setPct] = useState(0);
  const onSubmit = (e) => {
    e.preventDefault();
    const fd = new FormData(e.target);
    setBusy(true); setPct(0);
    const xhr = new XMLHttpRequest();
    xhr.open("POST", "/api/upload-flow");
    // Phase 3: attach Bearer token + handle 401 the same way apiFetch does.
    const _atlasToken = localStorage.getItem("atlas_session");
    if (_atlasToken) xhr.setRequestHeader("Authorization", "Bearer " + _atlasToken);
    xhr.upload.onprogress = (ev) => { if (ev.lengthComputable) setPct(Math.round(ev.loaded/ev.total*100)); };
    xhr.onload = () => {
      setBusy(false);
      if (xhr.status === 401) {
        localStorage.removeItem("atlas_session");
        localStorage.removeItem("atlas_wallet");
        window.location.reload();
        return;
      }
      onUploaded && onUploaded(chat);
    };
    xhr.onerror = () => { setBusy(false); alert("upload failed"); };
    xhr.send(fd);
  };
  return (
    <form className="upload-form" onSubmit={onSubmit}>
      <input type="hidden" name="c" value={chat} />
      <input type="file" name="file" accept="video/*" required disabled={busy} />
      <button type="submit" disabled={busy}>{busy ? `↑ ${pct}%` : "↑ upload"}</button>
    </form>
  );
}

function ApprovalCard({ chat, field, onResolved }) {
  // Fetch chat meta to grab the draft body; allow approve / edit / cancel.
  const [draft, setDraft] = useState("");
  const [busy, setBusy] = useState(false);
  useEffect(() => {
    fetchJson(`/api/chat.json?c=${encodeURIComponent(chat)}`).catch(() => {});
    // Pull the draft body from the rendered chat (the assistant message just
    // before the approval token usually contains it in a code block). Fallback:
    // hit a lightweight meta-shaped endpoint via the existing chats.json list.
    fetch(`/api/chats.json`).then(r => r.json()).then(list => {
      // No direct meta endpoint; the server already renders the draft in the
      // preceding assistant message, so we just provide an empty editor that
      // sends whatever the user types. (Phase 5 keeps this simple.)
    }).catch(() => {});
  }, [chat, field]);
  const post = async (action, body) => {
    setBusy(true);
    const params = new URLSearchParams();
    if (body) params.set("body", body);
    const _atlasToken = localStorage.getItem("atlas_session");
    const headers = {"Content-Type": "application/x-www-form-urlencoded"};
    if (_atlasToken) headers["Authorization"] = "Bearer " + _atlasToken;
    await fetch(`/api/approve?c=${encodeURIComponent(chat)}&action=${action}`, {
      method: "POST", headers, body: params.toString()
    });
    setBusy(false);
    onResolved && onResolved(chat);
  };
  return (
    <div className="approval-card" style={{margin:"8px 0",padding:10,background:"#15151c",border:"1px dashed #2a2a33",borderRadius:6}}>
      <div style={{fontSize:11,color:"#7d8590",marginBottom:6}}>
        📝 draft awaiting approval — chat={chat} field={field}
      </div>
      <textarea
        value={draft}
        onChange={(e) => setDraft(e.target.value)}
        placeholder="(optional) edited draft body — leave blank to approve as-is"
        style={{width:"100%",minHeight:120,background:"#0a0a0e",color:"#ddd",border:"1px solid #2a2a33",borderRadius:4,padding:8,fontFamily:"ui-monospace,monospace",fontSize:12}}
      />
      <div style={{display:"flex",gap:6,marginTop:6}}>
        <button type="button" disabled={busy} onClick={() => post("approve")}
          style={{background:"#39ff14",color:"#0a0a0e",border:"none",borderRadius:4,padding:"6px 12px",fontWeight:600,cursor:"pointer"}}>
          ✓ approve
        </button>
        <button type="button" disabled={busy || !draft.trim()} onClick={() => post("edit", draft)}
          style={{background:"#1f5fbb",color:"#fff",border:"none",borderRadius:4,padding:"6px 12px",fontWeight:600,cursor:"pointer"}}>
          ✎ edit + approve
        </button>
        <button type="button" disabled={busy} onClick={() => post("cancel")}
          style={{background:"#f85149",color:"#fff",border:"none",borderRadius:4,padding:"6px 12px",fontWeight:600,cursor:"pointer"}}>
          ✗ cancel
        </button>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════════════════
// PHASE 10 — Card grammar + lineage + slide transitions
// ═══════════════════════════════════════════════════════════════════════════
// Every chat message gets dispatched through CARD_TYPES by `kind`. Rows on
// disk without `kind` fall back to TextCard (current bubble look). Phase 11
// will splice user-authored templates into CARD_TYPES at runtime — keep the
// registry as a plain object so `CARD_TYPES[id] = Cmp` Just Works.
//
// Lineage is propagated via React Context. Each MessageCard registers its
// index + lineage_of array on mount; on hover, the registry resolves all
// upstream indexes and applies `.lineage-glow` to their DOM nodes.

const LineageContext = React.createContext(null);

function LineageProvider({ children }) {
  // Map<index, {ref, lineageOf:number[]}>
  const registryRef = useRef(new Map());
  const register = useCallback((idx, ref, lineageOf) => {
    registryRef.current.set(idx, { ref, lineageOf: lineageOf || [] });
    return () => { registryRef.current.delete(idx); };
  }, []);
  const glow = useCallback((idx, on) => {
    const reg = registryRef.current;
    const entry = reg.get(idx);
    if (!entry) return;
    const seen = new Set();
    const walk = (i) => {
      if (seen.has(i)) return; seen.add(i);
      const e = reg.get(i);
      if (!e) return;
      (e.lineageOf || []).forEach(p => {
        const pe = reg.get(p);
        if (pe && pe.ref && pe.ref.current) {
          pe.ref.current.classList.toggle("lineage-glow", on);
        }
        walk(p);
      });
    };
    walk(idx);
  }, []);
  return (
    <LineageContext.Provider value={{ register, glow }}>
      {children}
    </LineageContext.Provider>
  );
}

// ─── Card helpers ──────────────────────────────────────────────────────────
function _kindIcon(k) {
  return {
    email: "📧", call: "📞", doc: "📄", media: "🖼", approval: "⏳",
    recall: "📚", template: "📄", workflow_step: "▶", entity: "👤", system: "▶",
  }[k] || "•";
}
function _ts(ts) {
  if (!ts) return "";
  try {
    const d = new Date((ts > 1e12 ? ts : ts * 1000));
    return d.toLocaleString();
  } catch (e) { return ""; }
}

// ─── 1. TextCard — fallback (legacy bubble look, retuned to the new grammar)
function TextCard({ msg, onUploaded, onMention }) {
  // Reuse legacy renderMessage so existing token-based content still works.
  return (
    <div className={`card card-text ${msg.role === "user" ? "card-user" : "card-assistant"}`}>
      <div className="card-body">
        {renderMessage(msg.text || "", onUploaded, onMention)}
      </div>
    </div>
  );
}

// ─── 2. EmailCard ──────────────────────────────────────────────────────────
function EmailCard({ msg, onAction }) {
  const p = msg.payload || {};
  const c = msg._chat;
  const [menuOpen, setMenuOpen] = useState(false);
  const [busy, setBusy] = useState(false);
  const onSpam = async () => {
    setBusy(true);
    try {
      const r = await apiFetch("/api/email/spam", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({ c, sender: p.sender || "" }),
      });
      const d = await r.json().catch(() => ({}));
      if (d.ok) onAction && onAction("spammed", d);
    } finally { setBusy(false); }
  };
  const onReassign = async (new_flow) => {
    setBusy(true);
    try {
      const r = await apiFetch("/api/email/reassign", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({ c, new_flow }),
      });
      const d = await r.json().catch(() => ({}));
      if (d.ok) onAction && onAction("reassigned", d);
    } finally { setBusy(false); setMenuOpen(false); }
  };
  const onHook = async () => {
    setBusy(true);
    try {
      const r = await apiFetch("/api/email/hook-new", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({ c }),
      });
      const d = await r.json().catch(() => ({}));
      if (d.ok) onAction && onAction("hook-new", d);
    } finally { setBusy(false); }
  };
  return (
    <div className="card card-email">
      <div className="card-caption">📧 INBOUND · {p.channel || "inbox"} · {_ts(p.received_at)}</div>
      <div className="card-body">
        <div className="email-from">
          <span className="email-from-name">{p.from_name || p.sender || "(unknown)"}</span>
          {p.sender && <span className="email-from-addr">&lt;{p.sender}&gt;</span>}
        </div>
        <div className="email-subject">{p.subject || "(no subject)"}</div>
        <div className="email-body">{p.body || ""}</div>
        {(p.attachments || []).length > 0 && (
          <div className="email-attachments">
            {(p.attachments || []).map((a, i) => (
              <a key={i} className={`att-chip att-${a.mime || "file"}`}
                 href={_atlasSrc(a.path || "")} target="_blank" rel="noreferrer">
                <span className="att-icon">{({pdf:"📄",image:"🖼",audio:"🎵",video:"🎞",docx:"📝"}[a.mime] || "📎")}</span>
                <span className="att-name">{a.name || _basename(a.path || "")}</span>
              </a>
            ))}
          </div>
        )}
      </div>
      <div className="card-footer">
        <button className="card-btn card-btn-warn" disabled={busy} onClick={onSpam}>🚫 spam</button>
        <div className="card-btn-menu">
          <button className="card-btn" disabled={busy} onClick={() => setMenuOpen(o => !o)}>📋 workflow ▾</button>
          {menuOpen && (
            <div className="card-menu">
              {Object.entries(((typeof window !== "undefined" && window.__ATLAS_WORKFLOWS) || {})).map(([fid, w]) => (
                <button key={fid} className="card-menu-item" onClick={() => onReassign(fid)}>
                  {w.icon || "⚙"} {w.label || fid}
                </button>
              ))}
            </div>
          )}
        </div>
        <button className="card-btn card-btn-accent" disabled={busy} onClick={onHook}>⊕ hook</button>
      </div>
    </div>
  );
}

// ─── 3. CallCard ───────────────────────────────────────────────────────────
function CallCard({ msg }) {
  const p = msg.payload || {};
  return (
    <div className="card card-call">
      <div className="card-caption">📞 CALL · {p.duration || "?"} · {_ts(p.received_at || p.ts)}</div>
      <div className="card-body">
        <div className="call-from">{p.caller || p.from || "(unknown caller)"}</div>
        {p.transcript && <div className="call-transcript">{p.transcript.slice(0, 600)}</div>}
        {p.audio && <audio src={_atlasSrc(p.audio)} controls style={{width:"100%"}} />}
      </div>
      <div className="card-footer">
        <button className="card-btn">📋 workflow ▾</button>
        <button className="card-btn card-btn-accent">⊕ hook</button>
      </div>
    </div>
  );
}

// ─── 4. DocumentCard ───────────────────────────────────────────────────────
function DocumentCard({ msg, onMention }) {
  const p = msg.payload || {};
  const isPdf = (p.mime || "").includes("pdf") || (p.path || "").toLowerCase().endsWith(".pdf");
  return (
    <div className="card card-doc">
      <div className="card-caption">📄 DOCUMENT · {p.source || "upload"} · {_ts(p.ts)}</div>
      <div className="card-body">
        {isPdf
          ? <iframe src={_atlasSrc(p.path || "") + "#toolbar=0"} title={p.name || "doc"} />
          : <div className="doc-icon-block"><span className="doc-icon">📝</span><span>{p.name || _basename(p.path || "")}</span></div>}
      </div>
      <div className="card-footer">
        <button className="card-btn" onClick={() => onMention && onMention("@" + (p.name || "doc").replace(/\W+/g,"_"))}>💬 chat-with</button>
        <a className="card-btn" href={_atlasSrc(p.path || "")} download>⬇ download</a>
        <button className="card-btn card-btn-accent">📤 send</button>
      </div>
    </div>
  );
}

// ─── 5. MediaCard ──────────────────────────────────────────────────────────
function MediaCard({ msg }) {
  const p = msg.payload || {};
  const k = p.media_kind || (p.path || "").split(".").pop().toLowerCase();
  const icon = k === "mp4" || k === "webm" ? "🎞" : (k === "mp3" || k === "wav" ? "🎵" : "🖼");
  return (
    <div className="card card-media">
      <div className="card-caption">{icon} MEDIA · {_ts(p.ts)}</div>
      <div className="card-body">
        {(k === "mp4" || k === "webm") ? <video src={_atlasSrc(p.path || "")} controls />
          : (k === "mp3" || k === "wav" || k === "m4a") ? <audio src={_atlasSrc(p.path || "")} controls style={{width:"100%"}} />
          : <img src={_atlasSrc(p.path || "")} alt={p.name || ""} />}
      </div>
      <div className="card-footer">
        <a className="card-btn" href={_atlasSrc(p.path || "")} download>⬇ download</a>
        <button className="card-btn card-btn-accent">📤 share</button>
      </div>
    </div>
  );
}

// ─── 6. ApprovalCard (Phase 10 grammar — re-style of Phase 5 component) ────
function ApprovalCardV2({ msg, onResolved }) {
  const p = msg.payload || {};
  const chat = p.chat || msg._chat;
  const field = p.field || "draft_email";
  const draftPreview = p.draft || "";
  const [draft, setDraft] = useState(draftPreview);
  const [busy, setBusy] = useState(false);
  const post = async (action, body) => {
    setBusy(true);
    const params = new URLSearchParams();
    if (body) params.set("body", body);
    const _atlasToken = localStorage.getItem("atlas_session");
    const headers = {"Content-Type": "application/x-www-form-urlencoded"};
    if (_atlasToken) headers["Authorization"] = "Bearer " + _atlasToken;
    await fetch(`/api/approve?c=${encodeURIComponent(chat)}&action=${action}`, {
      method: "POST", headers, body: params.toString()
    });
    setBusy(false);
    onResolved && onResolved(chat);
  };
  return (
    <div className="card card-approval">
      <div className="card-caption">⏳ APPROVAL · {p.flow || field}</div>
      <div className="card-body">
        <textarea value={draft} onChange={e => setDraft(e.target.value)}
          placeholder="(optional) edited draft body — leave blank to approve as-is" />
      </div>
      <div className="card-footer">
        <button className="card-btn card-btn-accent" disabled={busy} onClick={() => post("approve")}>✓ approve</button>
        <button className="card-btn" disabled={busy || !draft.trim()} onClick={() => post("edit", draft)}>✎ edit</button>
        <button className="card-btn card-btn-warn" disabled={busy} onClick={() => post("cancel")}>✗ cancel</button>
      </div>
    </div>
  );
}

// ─── 7. RecallCard ─────────────────────────────────────────────────────────
function RecallCard({ msg }) {
  const p = msg.payload || {};
  const hits = p.hits || [];
  const [expanded, setExpanded] = useState(false);
  const shown = expanded ? hits : hits.slice(0, 4);
  return (
    <div className="card card-recall">
      <div className="card-caption">📚 RECALLED · {hits.length} hits · {p.folder || "(global)"}</div>
      <div className="card-body">
        {shown.map((h, i) => (
          <div key={i} className="recall-chip">
            <span className="recall-score">{Number(h.score || 0).toFixed(2)}</span>
            <span className="recall-source">{h.source || h.path || "?"}</span>
            <span className="recall-text">{(h.text || "").slice(0, 140)}</span>
          </div>
        ))}
      </div>
      <div className="card-footer">
        {hits.length > 4 && (
          <button className="card-btn" onClick={() => setExpanded(e => !e)}>{expanded ? "collapse" : "expand"}</button>
        )}
        <button className="card-btn card-btn-accent">add to context</button>
      </div>
    </div>
  );
}

// ─── 8. TemplateCard ───────────────────────────────────────────────────────
function TemplateCard({ msg }) {
  const p = msg.payload || {};
  return (
    <div className="card card-template">
      <div className="card-caption">📄 GENERATED · {p.template_id || "(template)"}</div>
      <div className="card-body">
        <div className="template-preview">
          <span className="template-icon">📝</span>
          <div>
            <div className="template-name">{p.out_name || _basename(p.path || "") || "document.docx"}</div>
            {p.preview && <div className="template-snippet">{(p.preview || "").slice(0, 200)}</div>}
          </div>
        </div>
      </div>
      <div className="card-footer">
        <button className="card-btn card-btn-accent">📤 send as attachment</button>
        <a className="card-btn" href={_atlasSrc(p.path || "")} download>💾 save copy</a>
      </div>
    </div>
  );
}

// ─── 9. WorkflowStepCard ───────────────────────────────────────────────────
function WorkflowStepCard({ msg, onUploaded, onMention }) {
  const p = msg.payload || {};
  return (
    <div className="card card-workflow-step">
      <div className="card-caption">▶ STEP · {p.flow || "?"} · step #{(p.step ?? 0)}</div>
      <div className="card-body">
        {p.ask && <div className="step-ask">{p.ask}</div>}
        {p.upload && <UploadForm chat={p.chat || msg._chat} onUploaded={onUploaded} />}
      </div>
      <div className="card-footer">
        <span className="card-status">{p.status || "awaiting input"}</span>
      </div>
    </div>
  );
}

// ─── 10. EntityCard ────────────────────────────────────────────────────────
function EntityCard({ msg }) {
  const p = msg.payload || {};
  const profile = p.profile || {};
  const events = p.recent_events || [];
  return (
    <div className="card card-entity">
      <div className="card-caption">👤 ENTITY · {p.kind || "?"}</div>
      <div className="card-body">
        <div className="entity-name">{p.name || p.id || "(unnamed)"}</div>
        <div className="entity-kv">
          {Object.entries(profile).slice(0, 8).map(([k, v]) => (
            <div key={k} className="entity-kv-row"><span className="entity-k">{k}</span><span className="entity-v">{String(v)}</span></div>
          ))}
        </div>
        {events.length > 0 && (
          <div className="entity-events">
            {events.slice(0, 4).map((e, i) => (
              <div key={i} className="entity-event">{e.kind} — {_ts(e.ts)}</div>
            ))}
          </div>
        )}
      </div>
      <div className="card-footer">
        <button className="card-btn">view</button>
        <button className="card-btn">edit</button>
        <button className="card-btn card-btn-accent">link</button>
      </div>
    </div>
  );
}

// ─── 11. SystemCard ────────────────────────────────────────────────────────
function SystemCard({ msg, onAction }) {
  const p = msg.payload || {};
  const t = p.text || msg.text || "▶ system";
  // Phase 11.5: if payload carries a tour_hint, render a [💡 Show me how]
  // button. Clicking POSTs /api/tour/start with the hint's intent.
  const hint = p.tour_hint;
  return (
    <div className="card card-system">
      <div className="card-body">{t}</div>
      {hint && hint.intent && (
        <div className="card-footer system-card-footer">
          <button className="card-btn card-btn-accent"
                  onClick={() => onAction && onAction("tour-hint", hint)}>
            💡 Show me how
          </button>
        </div>
      )}
    </div>
  );
}

// ─── 12. LLMCard (Phase 11) — LLM thoughts + structured choices ────────────
// Payload shape (mirrors telos-triggers::Predicate {kind,args} discriminator
// for choices, oracle_route() envelope {path,confidence} for path label):
//   { thoughts, choices:[{id, label, action_kind, action_args, hint}], picked,
//     confidence, suggested_pick, can_refine, can_regenerate, model, path }
function LLMCard({ msg, onAction }) {
  const p = msg.payload || {};
  const c = msg._chat;
  const idx = msg._idx;
  const [thoughtsOpen, setThoughtsOpen] = useState(false);
  const [refineOpen, setRefineOpen] = useState(false);
  const [refineText, setRefineText] = useState("");
  const [customOpen, setCustomOpen] = useState(false);
  const [customText, setCustomText] = useState("");
  const [busy, setBusy] = useState(false);
  const pct = Math.round((Number(p.confidence) || 0) * 100);
  const path = p.path || "cold";
  const picked = p.picked;
  const choices = Array.isArray(p.choices) ? p.choices : [];
  const suggested = p.suggested_pick;
  const post = async (url, payload) => {
    setBusy(true);
    try {
      const r = await apiFetch(url, {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      const d = await r.json().catch(() => ({}));
      if (d.ok && onAction) onAction("llm", d);
      return d;
    } finally { setBusy(false); }
  };
  const pick = (choice_id, custom_label) =>
    post("/api/llmcard/pick", { c, msg_idx: idx, choice_id, custom_label });
  const regenerate = () => post("/api/llmcard/regenerate", { c, msg_idx: idx });
  const refine = async () => {
    if (!refineText.trim()) return;
    const d = await post("/api/llmcard/refine", { c, msg_idx: idx, instructions: refineText.trim() });
    if (d.ok) { setRefineOpen(false); setRefineText(""); }
  };
  const sendCustom = async () => {
    if (!customText.trim()) return;
    await pick(null, customText.trim());
    setCustomOpen(false); setCustomText("");
  };
  return (
    <div className="card card-llm">
      <div className="card-caption llm-caption">
        🤖 LLM · {p.model || "model"} · {pct}% · path: <span className={`llm-path llm-path-${path}`}>{path}</span>
      </div>
      <div className="card-body">
        <details className="llm-thoughts" open={thoughtsOpen} onToggle={e => setThoughtsOpen(e.target.open)}>
          <summary>{thoughtsOpen ? "▾" : "▸"} thoughts</summary>
          <div className="llm-thoughts-body">{p.thoughts || "(no thoughts emitted)"}</div>
        </details>
        {picked ? (
          <div className="llm-picked">
            <span className="llm-picked-pill">✓ {p.picked_label || picked}</span>
            {p.picked_at && (Date.now()/1000 - p.picked_at) < 30 && (
              <button className="llm-undo" disabled
                      title="undo not yet wired in v0">undo</button>
            )}
          </div>
        ) : (
          <div className="llm-choices">
            {choices.map(ch => (
              <button key={ch.id} className={`llm-choice ${ch.id === suggested ? "llm-choice-suggested" : ""}`}
                      onClick={() => pick(ch.id)} disabled={busy}>
                <div className="llm-choice-label">{ch.label}</div>
                {ch.hint && <div className="llm-choice-hint">{ch.hint}</div>}
              </button>
            ))}
            {customOpen ? (
              <div className="llm-custom">
                <input autoFocus value={customText} onChange={e => setCustomText(e.target.value)}
                       placeholder="Type your own choice…"
                       onKeyDown={e => { if (e.key==="Enter") sendCustom(); if (e.key==="Escape") setCustomOpen(false); }} />
                <button className="card-btn card-btn-accent" onClick={sendCustom} disabled={busy || !customText.trim()}>send</button>
                <button className="card-btn" onClick={() => setCustomOpen(false)}>cancel</button>
              </div>
            ) : (
              <button className="llm-choice llm-choice-custom" onClick={() => setCustomOpen(true)} disabled={busy}>
                <div className="llm-choice-label">⊕ Add your own</div>
              </button>
            )}
          </div>
        )}
        {refineOpen && (
          <div className="llm-refine">
            <textarea autoFocus value={refineText} onChange={e => setRefineText(e.target.value)}
                      placeholder="What should the LLM do differently? (specifics)" rows={2} />
            <div className="llm-refine-row">
              <button className="card-btn card-btn-accent" onClick={refine} disabled={busy || !refineText.trim()}>refine →</button>
              <button className="card-btn" onClick={() => { setRefineOpen(false); setRefineText(""); }}>cancel</button>
            </div>
          </div>
        )}
      </div>
      {!picked && (
        <div className="card-footer llm-footer">
          {p.can_regenerate !== false && (
            <button className="card-btn" onClick={regenerate} disabled={busy} title="re-run with the same prompt">↻ Regenerate</button>
          )}
          {p.can_refine !== false && (
            <button className="card-btn" onClick={() => setRefineOpen(o => !o)} disabled={busy} title="add refinement instructions">✎ Refine</button>
          )}
        </div>
      )}
    </div>
  );
}

// ─── 13. CustomerSummaryCard (Phase 11 example — UER renderer) ─────────────
// Renders the customer-summary.json template's `customer_summary` kind. Shows
// client name + key contact fields + recent events + open case count.
function CustomerSummaryCard({ msg }) {
  const p = msg.payload || {};
  const profile = p.profile || {};
  const events = p.recent_events || [];
  const cases = p.open_cases || [];
  return (
    <div className="card card-customer-summary">
      <div className="card-caption">👤 CUSTOMER · {p.name || p.id || "?"}</div>
      <div className="card-body">
        <div className="entity-kv">
          {["email","phone","tenant","wallet"].map(k => profile[k] && (
            <div key={k} className="entity-kv-row">
              <span className="entity-k">{k}</span>
              <span className="entity-v">{String(profile[k])}</span>
            </div>
          ))}
        </div>
        {cases.length > 0 && (
          <div className="customer-cases">
            <div className="entity-events-label">Open cases ({cases.length})</div>
            {cases.slice(0, 4).map((cs, i) => (
              <div key={i} className="entity-event">{cs.kind || "case"} — {cs.title || cs.id}</div>
            ))}
          </div>
        )}
        {events.length > 0 && (
          <div className="entity-events">
            <div className="entity-events-label">Recent activity</div>
            {events.slice(0, 5).map((e, i) => (
              <div key={i} className="entity-event">{e.kind} — {_ts(e.ts)}</div>
            ))}
          </div>
        )}
      </div>
      <div className="card-footer">
        <button className="card-btn">view profile</button>
        <button className="card-btn card-btn-accent">new case</button>
      </div>
    </div>
  );
}

// ─── 14. UserTemplateCard — generic renderer for user-authored templates ───
// Renders any card_template spec generically. The `customer_summary` example
// short-circuits to CustomerSummaryCard; everything else falls through here.
function UserTemplateCard({ msg, spec }) {
  const p = msg.payload || {};
  const layout = (spec && spec.layout) || {};
  const fields = ((spec && spec.schema) || {}).fields || [];
  const actions = (spec && spec.actions) || [];
  const caption = (layout.caption_template || "{{name}}").replace(/\{\{(\w+)\}\}/g, (_, k) => p[k] != null ? String(p[k]) : "");
  const body_comps = layout.body_components || ["text"];
  return (
    <div className={`card card-user-template card-kind-${(spec && spec.kind) || "user"}`}>
      <div className="card-caption">{caption}</div>
      <div className="card-body">
        {body_comps.includes("fields") && (
          <div className="entity-kv">
            {fields.map(f => (
              <div key={f.name} className="entity-kv-row">
                <span className="entity-k">{f.name}</span>
                <span className="entity-v">{String(p[f.name] ?? "")}</span>
              </div>
            ))}
          </div>
        )}
        {body_comps.includes("text") && p.text && (<div className="card-text-body">{p.text}</div>)}
        {body_comps.includes("table") && Array.isArray(p.rows) && (
          <table className="card-table"><tbody>
            {p.rows.slice(0, 20).map((row, i) => (
              <tr key={i}>{(row || []).map((cell, j) => <td key={j}>{String(cell)}</td>)}</tr>
            ))}
          </tbody></table>
        )}
        {body_comps.includes("media") && p.url && (
          <div className="card-media"><img src={p.url} alt={p.alt || ""} style={{maxWidth:"100%"}} /></div>
        )}
        {body_comps.includes("iframe") && p.url && (
          <iframe src={p.url} className="card-iframe" sandbox="allow-scripts allow-same-origin" />
        )}
      </div>
      {actions.length > 0 && (
        <div className="card-footer">
          {actions.map(a => (
            <button key={a.id} className={`card-btn ${a.kind==='workflow_step'?'card-btn-accent':''}`}>{a.label}</button>
          ))}
        </div>
      )}
    </div>
  );
}

// ─── Registry: extensible at runtime (Phase 11 splices in user templates) ──
const CARD_TYPES = {
  text: TextCard,
  email: EmailCard,
  call: CallCard,
  doc: DocumentCard,
  media: MediaCard,
  approval: ApprovalCardV2,
  recall: RecallCard,
  template: TemplateCard,
  workflow_step: WorkflowStepCard,
  entity: EntityCard,
  system: SystemCard,
  llm: LLMCard,                          // Phase 11
  customer_summary: CustomerSummaryCard, // Phase 11 example
};
// Expose so Phase 11 user-authored templates can splice in at runtime:
//   window.CARD_TYPES.my_custom = MyComponent;
// `card-template-updated` events (fired by /api/card/template/save) trigger a
// refetch + splice via `mergeUserTemplates()` below.
if (typeof window !== "undefined") window.CARD_TYPES = CARD_TYPES;

// ─── Phase 13a: SETTINGS_REGISTRY — 20-section console catalog ──────────────
// Each entry: { group, id, icon, label, component, roles, sub_phase }
// `component` is a React component that renders inside the right-side
// `.settings-pane`. `component: null` renders a placeholder (claimed by
// later sub-phases). Sub-agents 13b/c/d/e/g splice their components by
// assigning `SETTINGS_REGISTRY[i].component = TheirSection` or by pushing
// new entries — order within a group is preserved render-time.
// `roles: null` means visible to everyone; otherwise array of role strings.
// Order matters: rendered top-down within each group.
// Phase 13.5: every entry now carries a `scope` field that drives the
// platform-mode toggle filter (SettingsPanel viewingMode). Scope values:
//   "platform"       → Tom-owned services only Tom configures (Porkbun, Stripe)
//   "tenant"         → per-tenant configuration (BYOK, branding, prompts…)
//   "shared_default" → platform admin sets a free default; tenants opt in OR BYOK
//   "both"           → visible in either viewing mode
const SETTINGS_REGISTRY = [
  // ─── CONFIG ───
  { group: "CONFIG", id: "accounts",        icon: "🔑", label: "Accounts & Auth",     component: null, roles: null,                    sub_phase: "13a", scope: "tenant" },
  { group: "CONFIG", id: "ai_connectors",   icon: "🤖", label: "AI Connectors",       component: null, roles: ["admin"],               sub_phase: "13d", scope: "shared_default" },
  { group: "CONFIG", id: "drives",          icon: "💾", label: "Drives & Storage",    component: null, roles: ["admin"],               sub_phase: "13c", scope: "tenant" },
  { group: "CONFIG", id: "payments",        icon: "💳", label: "Payments",            component: null, roles: ["admin"],               sub_phase: "13a", scope: "platform" },
  { group: "CONFIG", id: "identity_auth",   icon: "🔐", label: "Identity & Auth",     component: null, roles: ["admin"],               sub_phase: "13e", scope: "tenant" },
  { group: "CONFIG", id: "tenant_branding", icon: "🎨", label: "Tenant & Branding",   component: null, roles: ["admin"],               sub_phase: "13e", scope: "tenant" },
  // ─── PIPELINE ───
  { group: "PIPELINE", id: "addons",            icon: "📦", label: "Addons",            component: null, roles: ["admin","operator"], sub_phase: "13c", scope: "shared_default" },
  { group: "PIPELINE", id: "system_prompts",    icon: "📝", label: "System Prompts",    component: null, roles: ["admin","operator"], sub_phase: "13c", scope: "tenant" },
  { group: "PIPELINE", id: "endpoint_slugs",    icon: "🔗", label: "Endpoint Slugs",    component: null, roles: ["admin","operator"], sub_phase: "13c", scope: "tenant" },
  { group: "PIPELINE", id: "public_endpoints",  icon: "📡", label: "Public Endpoints",  component: null, roles: ["admin","operator"], sub_phase: "13c", scope: "tenant" },
  { group: "PIPELINE", id: "mcp_servers",       icon: "🛠", label: "MCP Servers",       component: null, roles: ["admin","operator"], sub_phase: "13c", scope: "tenant" },
  { group: "PIPELINE", id: "webhooks",          icon: "🔌", label: "Webhooks",          component: null, roles: ["admin","operator"], sub_phase: "13b", scope: "tenant" },
  // ─── OPERATIONS ───
  { group: "OPERATIONS", id: "notifications",   icon: "🔔", label: "Notifications",       component: null, roles: null,                sub_phase: "13b", scope: "tenant" },
  { group: "OPERATIONS", id: "team",            icon: "👥", label: "Team / Users",        component: null, roles: ["admin"],            sub_phase: "13b", scope: "tenant" },
  { group: "OPERATIONS", id: "health",          icon: "📡", label: "Integrations Health", component: null, roles: ["admin","operator"], sub_phase: "13b", scope: "tenant", platform_rollup: true },
  { group: "OPERATIONS", id: "voice",           icon: "🎤", label: "Voice & TTS",         component: null, roles: ["admin","operator"], sub_phase: "13b", scope: "shared_default" },
  { group: "OPERATIONS", id: "usage_tokens",    icon: "📊", label: "Usage & Tokens",      component: null, roles: ["admin"],            sub_phase: "13e", scope: "tenant", platform_rollup: true },
  { group: "OPERATIONS", id: "inflow",          icon: "📥", label: "Inflow",              component: null, roles: ["admin","operator"], sub_phase: "13g", scope: "tenant" },
  { group: "OPERATIONS", id: "outflow",         icon: "📤", label: "Outflow",             component: null, roles: ["admin","operator"], sub_phase: "13g", scope: "tenant" },
  // ─── GATEWAY ───
  { group: "GATEWAY", id: "proxy_gateway",    icon: "🌐", label: "Proxy & Gateway",    component: null, roles: ["admin"], sub_phase: "13d", scope: "tenant" },
  { group: "GATEWAY", id: "reasoning_capture", icon: "🧠", label: "Reasoning Capture", component: null, roles: ["admin"], sub_phase: "13d", scope: "tenant", platform_storage_target: true },
  { group: "GATEWAY", id: "marketplace",      icon: "🏪", label: "Marketplace",        component: null, roles: ["admin"], sub_phase: "13d", scope: "both" },
  { group: "GATEWAY", id: "domains_hosting",  icon: "🌐", label: "Domains & Hosting",  component: null, roles: ["admin"], sub_phase: "13a", scope: "platform", tenant_request_workflow: "request-custom-domain" },
];

const SETTINGS_GROUP_ORDER = ["CONFIG", "PIPELINE", "OPERATIONS", "GATEWAY"];
const SETTINGS_GROUP_LABELS = {
  CONFIG: "─── CONFIG ───",
  PIPELINE: "─── PIPELINE ───",
  OPERATIONS: "─── OPERATIONS ───",
  GATEWAY: "─── GATEWAY ───",
};

if (typeof window !== "undefined") {
  window.SETTINGS_REGISTRY = SETTINGS_REGISTRY;
  // Sub-agents (13b/c/d/e/g) can call this to register their section
  // component after module load (hot-reload scenarios). Returns true on hit.
  window.setSettingsRegistry = function setSettingsRegistry(id, component) {
    const row = SETTINGS_REGISTRY.find(r => r.id === id);
    if (row) { row.component = component; return true; }
    return false;
  };
}

// Phase 11: pull user-authored card templates and splice them into CARD_TYPES.
// Each spec gets rendered through UserTemplateCard (or a built-in component if
// CARD_TYPES already has an entry for that kind — built-ins win).
function mergeUserTemplates(specs) {
  if (!Array.isArray(specs)) return;
  specs.forEach(spec => {
    if (!spec || !spec.kind) return;
    if (CARD_TYPES[spec.kind]) return; // built-in or already merged
    CARD_TYPES[spec.kind] = function MergedTemplateCard(props) {
      return <UserTemplateCard {...props} spec={spec} />;
    };
  });
  if (typeof window !== "undefined") window.CARD_TYPES = CARD_TYPES;
}
// Fire-and-forget initial pull at module load (idempotent).
if (typeof window !== "undefined") {
  fetch("/api/cards.json").then(r => r.json()).then(d => mergeUserTemplates(d.templates || [])).catch(() => {});
  // Listen for the HX-Trigger header surfaced by /api/card/template/save —
  // window event name = "card-template-updated". CardBuilderModal also fires
  // this manually on save so multi-tab installs stay in sync.
  window.addEventListener("card-template-updated", () => {
    fetch("/api/cards.json").then(r => r.json()).then(d => mergeUserTemplates(d.templates || [])).catch(() => {});
  });
  // Phase 12a: addons-updated fans out the registry refresh. LibraryTab,
  // PlansSection, and (downstream) any registry consumer listens for this
  // event. Card saves now fire BOTH card-template-updated + addons-updated.
  window.addEventListener("addons-updated", () => {
    fetch("/api/cards.json").then(r => r.json()).then(d => mergeUserTemplates(d.templates || [])).catch(() => {});
  });
  // HTMX-style HX-Trigger header bridge — if any future response carries
  // multiple events as JSON, parse + redispatch. Compatible with htmx semantics.
  document.addEventListener("htmx:afterRequest", (e) => {
    try {
      const trig = e.detail && e.detail.xhr && e.detail.xhr.getResponseHeader("HX-Trigger");
      if (!trig) return;
      if (trig.startsWith("{")) {
        const evs = JSON.parse(trig);
        Object.keys(evs || {}).forEach(name => window.dispatchEvent(new CustomEvent(name, { detail: evs[name] })));
      } else {
        window.dispatchEvent(new CustomEvent(trig));
      }
    } catch (err) {}
  });
}

// ─── Phase 12a: 📦 Library — addons grid (8 kinds) ─────────────────────────
// Listens for the addons-updated window event so installs/uninstalls splice
// live. Each card shows icon + name + description + author + version + actions.
const _ADDON_KIND_ICONS = {
  mcp_tool: "🔌", card: "📇", workflow: "🔁", template: "📄",
  flow_step: "⛓", prompt: "📝", tour: "🧭", theme: "🎨",
};
const _ADDON_KINDS_ORDER = ["mcp_tool","card","workflow","template","flow_step","prompt","tour","theme"];

function LibraryTab() {
  const [addons, setAddons] = useState({});
  const [counts, setCounts] = useState({});
  const [filterKind, setFilterKind] = useState(null);
  const [query, setQuery] = useState("");
  const [busy, setBusy] = useState(null);

  const refresh = useCallback(() => {
    const qp = new URLSearchParams();
    if (filterKind) qp.set("kind", filterKind);
    if (query) qp.set("q", query);
    fetch("/api/addons.json?" + qp.toString())
      .then(r => r.json())
      .then(d => { setAddons(d.addons || {}); setCounts(d.counts || {}); })
      .catch(() => {});
  }, [filterKind, query]);

  useEffect(() => { refresh(); }, [refresh]);
  useEffect(() => {
    const onUpd = () => refresh();
    window.addEventListener("addons-updated", onUpd);
    return () => window.removeEventListener("addons-updated", onUpd);
  }, [refresh]);

  const install = useCallback(async (manifest) => {
    setBusy("install");
    try {
      const r = await fetch("/api/addons/install", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(manifest),
      });
      const d = await r.json();
      if (d.ok) { try { window.dispatchEvent(new CustomEvent("addons-updated", { detail: d })); } catch (e) {} }
      refresh();
    } finally { setBusy(null); }
  }, [refresh]);

  const uninstall = useCallback(async (kind, id) => {
    if (!window.confirm(`Uninstall ${kind}/${id}?`)) return;
    setBusy(`uninstall-${id}`);
    try {
      const r = await fetch("/api/addons/uninstall", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ kind, id }),
      });
      const d = await r.json();
      if (!d.ok) alert("Uninstall failed: " + (d.error || "?"));
      else { try { window.dispatchEvent(new CustomEvent("addons-updated", { detail: d })); } catch (e) {} }
      refresh();
    } finally { setBusy(null); }
  }, [refresh]);

  const publish = useCallback(async (kind, id) => {
    setBusy(`publish-${id}`);
    try {
      const r = await fetch("/api/addons/publish", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ kind, id }),
      });
      const d = await r.json();
      alert(d.ok ? `Published → ${d.published_to}` : `Publish failed: ${d.error}`);
    } finally { setBusy(null); }
  }, []);

  // Flatten + render
  const visible = [];
  _ADDON_KINDS_ORDER.forEach(k => {
    if (filterKind && filterKind !== k) return;
    (addons[k] || []).forEach(a => visible.push({ ...a, _kind: k }));
  });

  return (
    <div className="library-tab" style={{ padding: 16 }}>
      <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap", marginBottom: 14 }}>
        <h2 style={{ margin: 0, fontSize: 16 }}>📦 Library</h2>
        <span style={{ color: "var(--fg2)", fontSize: 12 }}>
          {Object.values(counts).reduce((a,b) => a+(b||0), 0)} addons across {_ADDON_KINDS_ORDER.length} kinds
        </span>
        <div style={{ flex: 1 }} />
        <input type="text" placeholder="search…" value={query}
               onChange={e => setQuery(e.target.value)}
               style={{ padding: "5px 8px", border: "1px solid var(--border)", borderRadius: 6, fontSize: 12, width: 180 }} />
        <button onClick={() => {
          const raw = window.prompt("Paste addon manifest JSON (kind+id required):");
          if (!raw) return;
          try { install(JSON.parse(raw)); }
          catch (e) { alert("Invalid JSON: " + e.message); }
        }}
                style={{ padding: "5px 10px", borderRadius: 6, fontSize: 12, cursor: "pointer", background: "var(--accent)", color: "#0a0a0e", border: "none", fontWeight: 600 }}
                disabled={busy === "install"}>
          + install JSON
        </button>
      </div>
      <div className="addon-filter-chips" style={{ display: "flex", gap: 6, marginBottom: 12, flexWrap: "wrap" }}>
        <button onClick={() => setFilterKind(null)}
                style={{ padding: "4px 10px", borderRadius: 12, border: filterKind===null ? "1px solid var(--accent)" : "1px solid var(--border)", background: "transparent", color: "var(--fg)", fontSize: 11, cursor: "pointer" }}>
          all · {Object.values(counts).reduce((a,b)=>a+(b||0),0)}
        </button>
        {_ADDON_KINDS_ORDER.map(k => (
          <button key={k} onClick={() => setFilterKind(k)}
                  style={{ padding: "4px 10px", borderRadius: 12, border: filterKind===k ? "1px solid var(--accent)" : "1px solid var(--border)", background: "transparent", color: "var(--fg)", fontSize: 11, cursor: "pointer" }}>
            {_ADDON_KIND_ICONS[k] || "•"} {k} · {counts[k] || 0}
          </button>
        ))}
      </div>
      <div className="workflow-grid addon-grid"
           style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))", gap: 10 }}>
        {visible.length === 0 && (
          <div style={{ color: "var(--fg2)", fontSize: 12, padding: 20, gridColumn: "1 / -1" }}>
            No addons{filterKind ? ` of kind ${filterKind}` : ""}{query ? ` matching "${query}"` : ""}.
          </div>
        )}
        {visible.map(a => (
          <div key={`${a._kind}-${a.id}`} className="wf-card addon-card"
               style={{ display: "flex", flexDirection: "column", gap: 4, background: "var(--bg-soft)", border: "1px solid var(--border)", borderRadius: 8, padding: 12 }}>
            <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
              <span style={{ fontSize: 18 }}>{_ADDON_KIND_ICONS[a._kind] || "•"}</span>
              <span style={{ fontWeight: 600, fontSize: 13 }}>{a.name || a.id}</span>
              <span style={{ marginLeft: "auto", color: "var(--fg2)", fontSize: 10 }}>{a.version || "v0"}</span>
            </div>
            <div style={{ color: "var(--fg2)", fontSize: 11, minHeight: 28, lineHeight: 1.3 }}>
              {a.description || `${a._kind} · ${a.id}`}
            </div>
            <div style={{ color: "var(--fg2)", fontSize: 10 }}>
              by {a.author || "unknown"} · id {a.id}
            </div>
            <div style={{ display: "flex", gap: 4, marginTop: 6 }}>
              <button onClick={() => publish(a._kind, a.id)}
                      disabled={busy === `publish-${a.id}`}
                      style={{ padding: "3px 8px", borderRadius: 4, border: "1px solid var(--border)", background: "transparent", color: "var(--fg)", fontSize: 10, cursor: "pointer" }}>
                ↑ publish
              </button>
              <button onClick={() => uninstall(a._kind, a.id)}
                      disabled={busy === `uninstall-${a.id}`}
                      style={{ padding: "3px 8px", borderRadius: 4, border: "1px solid var(--border)", background: "transparent", color: "#f88", fontSize: 10, cursor: "pointer" }}>
                ✕ uninstall
              </button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── Phase 12b: 🧬 Plans sidebar section ───────────────────────────────────
// Admin/operator only. Lists recent plan rows with a kind icon + status dot.
// Clicking a row opens a detail modal showing the full plan record.
const _PLAN_KIND_ICONS = {
  agent_spawn:    "🤖",
  tool_call:      "🛠",
  workflow_run:   "🔁",
  generative_call:"⚡",
  addon_install:  "📦",
  user_build:     "🛠",
};
function _planStatusColor(s) {
  if (s === "done")      return "#39ff14";
  if (s === "running")   return "#58a6ff";
  if (s === "failed")    return "#f85149";
  if (s === "cancelled") return "#888";
  return "#aaa";
}
function PlansSection({ role }) {
  const [plans, setPlans] = useState([]);
  const [detail, setDetail] = useState(null);
  const [expanded, setExpanded] = useState(false);
  if (role !== "admin" && role !== "operator") return null;

  const refresh = useCallback(() => {
    fetch("/api/plans.json?limit=15").then(r => r.json())
      .then(d => setPlans(d.plans || []))
      .catch(() => {});
  }, []);
  useEffect(() => { refresh(); const t = setInterval(refresh, 6000); return () => clearInterval(t); }, [refresh]);
  useEffect(() => {
    const onUpd = () => refresh();
    window.addEventListener("addons-updated", onUpd);
    window.addEventListener("plans-updated", onUpd);
    return () => {
      window.removeEventListener("addons-updated", onUpd);
      window.removeEventListener("plans-updated", onUpd);
    };
  }, [refresh]);

  const openDetail = async (pid) => {
    try {
      const r = await fetch("/api/plan.json?id=" + encodeURIComponent(pid));
      const d = await r.json();
      setDetail(d);
    } catch (e) {}
  };

  const visible = expanded ? plans : plans.slice(0, 5);
  return (
    <div className="plans-section" data-tour-id="sidebar-plans" style={{ margin: "8px 0" }}>
      <div className="chat-list-group" onClick={() => setExpanded(e => !e)}
           style={{ cursor: "pointer", display: "flex", alignItems: "center", gap: 4 }}>
        <span style={{ fontSize: 10 }}>{expanded ? "▾" : "▸"}</span>
        <span>🧬 Plans ({plans.length})</span>
      </div>
      {visible.map(p => (
        <div key={p.plan_id}
             className="plans-row chat-row"
             onClick={() => openDetail(p.plan_id)}
             style={{ display: "flex", gap: 6, alignItems: "center", padding: "4px 8px",
                      fontSize: 11, cursor: "pointer", borderRadius: 4 }}>
          <span style={{ width: 8, height: 8, borderRadius: 4, background: _planStatusColor(p.status), flex: "none" }} />
          <span style={{ fontSize: 12 }}>{_PLAN_KIND_ICONS[p.kind] || "•"}</span>
          <span style={{ flex: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", color: "var(--fg)" }}
                title={p.intent}>
            {p.intent || p.kind}
          </span>
          <span style={{ color: "var(--fg2)", fontSize: 10 }}>
            {p.started_at ? _ts(p.started_at) : ""}
          </span>
        </div>
      ))}
      {!expanded && plans.length > 5 && (
        <div className="chat-list-group" style={{ fontSize: 10, color: "var(--fg2)", padding: "2px 8px" }}>
          +{plans.length - 5} more…
        </div>
      )}
      {detail && (
        <div className="modal-backdrop" onClick={() => setDetail(null)}
             style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.7)", zIndex: 200,
                      display: "flex", alignItems: "center", justifyContent: "center" }}>
          <div onClick={e => e.stopPropagation()}
               style={{ background: "var(--bg)", border: "1px solid var(--border)",
                        borderRadius: 8, padding: 20, width: "90%", maxWidth: 720,
                        maxHeight: "80vh", overflow: "auto" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 14 }}>
              <span style={{ fontSize: 22 }}>{_PLAN_KIND_ICONS[detail.kind] || "•"}</span>
              <h3 style={{ margin: 0, flex: 1 }}>Plan {detail.plan_id}</h3>
              <button onClick={() => setDetail(null)}
                      style={{ background: "transparent", border: "1px solid var(--border)",
                               color: "var(--fg)", padding: "4px 8px", borderRadius: 4, cursor: "pointer" }}>
                ✕
              </button>
            </div>
            <div style={{ display: "flex", gap: 6, marginBottom: 10, flexWrap: "wrap" }}>
              <span style={{ fontSize: 11, padding: "2px 8px", borderRadius: 10, background: _planStatusColor(detail.status), color: "#0a0a0e", fontWeight: 600 }}>
                {detail.status}
              </span>
              <span style={{ fontSize: 11, padding: "2px 8px", borderRadius: 10, background: "var(--bg-soft)", border: "1px solid var(--border)" }}>
                kind: {detail.kind}
              </span>
              {detail.chain_id && (
                <span style={{ fontSize: 11, padding: "2px 8px", borderRadius: 10, background: "var(--bg-soft)", border: "1px solid var(--border)" }}>
                  chain: {detail.chain_id.slice(0, 10)}…
                </span>
              )}
            </div>
            <div style={{ fontSize: 12, marginBottom: 12 }}>
              <strong>Intent:</strong> {detail.intent || "—"}
            </div>
            <pre style={{ fontSize: 10, background: "var(--bg-soft)", padding: 10, borderRadius: 6, overflow: "auto", maxHeight: 360 }}>
              {JSON.stringify(detail, null, 2)}
            </pre>
            <div style={{ display: "flex", gap: 6, marginTop: 12 }}>
              <button onClick={async () => {
                const r = await fetch("/api/plan/replay?id=" + encodeURIComponent(detail.plan_id), { method: "POST" });
                const d = await r.json();
                alert(d.ok ? "Replay queued" : "Replay failed: " + (d.error || "?"));
              }}
                      style={{ padding: "5px 12px", borderRadius: 4, border: "1px solid var(--border)", background: "transparent", color: "var(--fg)", fontSize: 11, cursor: "pointer" }}>
                ▶ Replay
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// Infer kind from a legacy text-only message (backwards-compat).
function _inferKind(msg) {
  if (msg.kind) return msg.kind;
  const t = msg.text || "";
  if (/approval:chat=/.test(t)) return "approval";  // handled inline by TextCard via renderMessage tokens, but we keep TextCard so legacy bubbles still work
  // Default: TextCard handles the rest (tokens render inline through legacy renderMessage)
  return "text";
}

// ─── MessageCard: the row-level wrapper. Registers lineage + hover handlers.
function MessageCard({ msg, idx, chat, onUploaded, onMention, onOpenLineage, onAction }) {
  const ref = useRef(null);
  const lineage = useContext(LineageContext);
  const lineageOf = msg.lineage_of || msg.lineageOf || [];
  useEffect(() => {
    if (!lineage) return;
    return lineage.register(idx, ref, lineageOf);
  }, [idx, lineage, JSON.stringify(lineageOf)]);
  const onEnter = () => lineage && lineage.glow(idx, true);
  const onLeave = () => lineage && lineage.glow(idx, false);
  const onKeyDown = (e) => {
    if (e.key === "ArrowRight") { e.preventDefault(); onOpenLineage && onOpenLineage(idx); }
  };
  const kind = _inferKind(msg);
  const Cmp = CARD_TYPES[kind] || TextCard;
  // attach _chat + _idx so deep card components (EmailCard, ApprovalCardV2,
  // LLMCard) know their chat name and their position in the chat (LLMCard
  // posts its index back to /api/llmcard/pick).
  const msgWithChat = { ...msg, _chat: chat, _idx: idx };
  return (
    <div ref={ref} className="msg-row card-row" data-idx={idx} data-kind={kind}
         tabIndex={0} onMouseEnter={onEnter} onMouseLeave={onLeave} onKeyDown={onKeyDown}>
      <div className={`msg-avatar ${msg.role === "user" ? "u" : "a"}`}>
        {msg.role === "user" ? "U" : "Z"}
      </div>
      <div className="msg-body">
        <Cmp msg={msgWithChat} onUploaded={onUploaded} onMention={onMention} onAction={onAction} onResolved={onUploaded} />
      </div>
      {(lineageOf.length > 0) && (
        <button className="lineage-affordance" title="show lineage trace"
                onClick={() => onOpenLineage && onOpenLineage(idx)}>↗</button>
      )}
    </div>
  );
}

// ─── Lineage drawer (slides in from right on demand) ───────────────────────
function LineageDrawer({ openFor, messages, onClose }) {
  const drawerRef = useRef(null);
  useEffect(() => {
    if (openFor == null) return;
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
      if (e.key === "ArrowLeft") onClose();
    };
    const onClick = (e) => {
      if (drawerRef.current && !drawerRef.current.contains(e.target)) onClose();
    };
    document.addEventListener("keydown", onKey);
    setTimeout(() => document.addEventListener("mousedown", onClick), 50);
    return () => {
      document.removeEventListener("keydown", onKey);
      document.removeEventListener("mousedown", onClick);
    };
  }, [openFor, onClose]);
  if (openFor == null) return null;
  // Build the trace tree by walking lineage_of recursively
  const trace = [];
  const seen = new Set();
  const walk = (i, depth) => {
    if (seen.has(i)) return; seen.add(i);
    const m = messages[i];
    if (!m) return;
    trace.push({ idx: i, depth, msg: m });
    const parents = m.lineage_of || m.lineageOf || [];
    parents.forEach(p => walk(p, depth + 1));
  };
  walk(openFor, 0);
  return (
    <aside ref={drawerRef} className="lineage-drawer" role="dialog" aria-label="lineage trace">
      <div className="lineage-drawer-head">
        <div className="lineage-drawer-title">Lineage trace</div>
        <button className="lineage-drawer-close" onClick={onClose} aria-label="close">✕</button>
      </div>
      <div className="lineage-drawer-body">
        {trace.length === 1 && <div className="lineage-empty">No upstream lineage recorded.</div>}
        {trace.map(({ idx, depth, msg }) => (
          <div key={idx} className="lineage-node" style={{ marginLeft: depth * 14 }}>
            <span className="lineage-kind">{_kindIcon(msg.kind || "text")}</span>
            <span className="lineage-role">{msg.role}</span>
            <span className="lineage-text">{(msg.text || (msg.payload && (msg.payload.subject || msg.payload.text)) || msg.kind || "").toString().slice(0, 80)}</span>
          </div>
        ))}
      </div>
    </aside>
  );
}

// ═══════════════════════════════════════════════════════════════════════════
// ─── Phase 7: status computation, folder helpers, hooks ───────────────────────
// Per-chat status from meta.step + workflow step.type + last role + mtime.
// Pure function; no new persistence. Rule contract from PHASE 7 of plan.
function computeChatStatus(chat, workflows) {
  if (!chat) return "idle";
  const flow = chat.flow ? workflows[chat.flow] : null;
  const steps = (flow && flow.steps) || [];
  const stepIdx = chat.step || 0;
  const step = steps[stepIdx] || {};
  const stepType = step.type ||
    (step.approval ? "approval" : step.upload ? "upload" : step.action ? "action" : step.ask ? "ask" : "");
  const last = chat.last_role;
  const lastApproval = chat.last_has_approval;
  const awaiting = chat.awaiting;
  // 🔴 red
  if (stepType === "approval" || step.approval || lastApproval || awaiting) return "red";
  // 🟢 green
  if (stepType === "action" || chat.gpu_active) return "green";
  // 🟡 yellow
  const recent = (chat.age != null) && chat.age < 300; // 5min
  if (stepType === "ask" || stepType === "upload") {
    if (last !== "assistant") return "yellow";
  }
  if (recent && last === "user") return "yellow";
  // ⚫ idle
  if (flow && stepIdx >= steps.length) return "idle";
  if (!flow) return "idle";
  return "idle";
}
const STATUS_RANK = { red: 3, yellow: 2, green: 1, idle: 0 };
function rollupStatus(statuses) {
  let best = "idle"; let bestR = 0;
  for (const s of statuses) {
    const r = STATUS_RANK[s] || 0;
    if (r > bestR) { best = s; bestR = r; }
  }
  return best;
}
function StatusDot({ status }) {
  return <span className={`status-dot status-${status || "idle"}`} title={status}>●</span>;
}

// useStepCount: returns # for the breadcrumb pill.
//   flow + step < len  → len - step  (shrinking)
//   flow + step >= len → null        (idle)
//   no flow            → draft_steps.length  (growing; 0 ok)
function useStepCount(chat, workflows) {
  if (!chat) return null;
  const flow = chat.flow ? workflows[chat.flow] : null;
  if (flow) {
    const steps = flow.steps || [];
    const stepIdx = chat.step || 0;
    if (stepIdx < steps.length) return steps.length - stepIdx;
    return null;
  }
  return (chat.draft_steps && chat.draft_steps.length) || 0;
}

// ─── Main App ────────────────────────────────────────────────────────────────
// Phase 4: which account services are SHARED infra (visible to operators)
// vs per-user OAuth keys (admin-only). Operators see shared infra only.
const SHARED_INFRA_ACCOUNTS = ["telegram", "twilio", "postmark", "gmail"];

// New-folder modal (reuses .settings-modal styling).
function NewFolderModal({ onClose, onCreated }) {
  const [name, setName] = useState("");
  const [color, setColor] = useState("");
  const [busy, setBusy] = useState(false);
  const create = async () => {
    if (!name.trim()) return;
    setBusy(true);
    const id = name.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").slice(0, 40) || "folder";
    const body = new URLSearchParams({ id, name: name.trim(), color_hint: color.trim() });
    try {
      const r = await apiFetch("/api/folders/save", { method: "POST", body }).then(r => r.json());
      if (r.ok) { onCreated && onCreated(r.folder); onClose(); }
    } finally { setBusy(false); }
  };
  return (
    <div className="settings-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="settings-modal" style={{ maxWidth: 440 }}>
        <div className="settings-head">
          <div>
            <div className="settings-title">New folder</div>
            <div className="settings-sub">Group chats for status rollup.</div>
          </div>
          <button className="settings-close" onClick={onClose}>×</button>
        </div>
        <div style={{ padding: "20px 24px 24px", display: "flex", flexDirection: "column", gap: 10 }}>
          <input autoFocus placeholder="Folder name (e.g. Insurance)" value={name}
                 onChange={e => setName(e.target.value)}
                 onKeyDown={e => { if (e.key === "Enter") create(); }}
                 style={{ background: "var(--bg)", border: "1px solid var(--line)", color: "var(--fg)", padding: "8px 10px", borderRadius: 6, fontSize: 13 }} />
          <input placeholder="Color hint (optional, e.g. #ff6b6b)" value={color}
                 onChange={e => setColor(e.target.value)}
                 style={{ background: "var(--bg)", border: "1px solid var(--line)", color: "var(--fg)", padding: "8px 10px", borderRadius: 6, fontSize: 13 }} />
          <button className="send-btn" onClick={create} disabled={busy || !name.trim()}
                  style={{ width: "100%", borderRadius: 8, padding: "10px 12px" }}>
            {busy ? "Creating…" : "Create folder"}
          </button>
        </div>
      </div>
    </div>
  );
}

// Phase 9: Mount drive modal — adds a local filesystem source to a folder, then
// kicks the background indexer. Re-uses .settings-modal styling.
function MountDriveModal({ folder, onClose, onMounted }) {
  const [path, setPath] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  const mount = async () => {
    if (!path.trim() || !folder) return;
    setBusy(true); setErr("");
    try {
      const r = await apiFetch(`/api/folders/${encodeURIComponent(folder.id)}/mount`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ kind: "local", path: path.trim() }),
      }).then(r => r.json());
      if (r.ok) { onMounted && onMounted(r); onClose(); }
      else { setErr(r.error || r.msg || "mount failed"); }
    } catch (e) { setErr(String(e)); }
    finally { setBusy(false); }
  };
  return (
    <div className="settings-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="settings-modal" style={{ maxWidth: 520 }}>
        <div className="settings-head">
          <div>
            <div className="settings-title">Mount drive · 📁 {folder?.name}</div>
            <div className="settings-sub">Walks a local directory; mints one artifact per file + builds a vector index.</div>
          </div>
          <button className="settings-close" onClick={onClose}>×</button>
        </div>
        <div style={{ padding: "20px 24px 24px", display: "flex", flexDirection: "column", gap: 10 }}>
          <input autoFocus placeholder="/absolute/path/to/folder" value={path}
                 onChange={e => setPath(e.target.value)}
                 onKeyDown={e => { if (e.key === "Enter") mount(); }}
                 style={{ background: "var(--bg)", border: "1px solid var(--line)", color: "var(--fg)", padding: "8px 10px", borderRadius: 6, fontSize: 13, fontFamily: "ui-monospace,monospace" }} />
          {folder?.sources?.length > 0 && (
            <div style={{ background: "var(--panel2)", border: "1px solid var(--line)", borderRadius: 6, padding: 10, fontSize: 11, color: "var(--fg2)" }}>
              <div style={{ fontWeight: 600, color: "var(--fg)", marginBottom: 6 }}>Existing sources</div>
              {folder.sources.map((s, i) => (
                <div key={i} style={{ display: "flex", gap: 6, alignItems: "center", padding: "3px 0" }}>
                  <span>📁</span>
                  <code style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis" }}>{s.path}</code>
                  <span style={{ color: s.status === "ready" ? "#39ff14" : s.status === "indexing" ? "#ffd700" : "#7d8590" }}>
                    {s.status}{s.indexed != null ? ` · ${s.indexed}` : ""}
                  </span>
                </div>
              ))}
            </div>
          )}
          {err && <div style={{ color: "#f85149", fontSize: 12 }}>⚠ {err}</div>}
          <button className="send-btn" onClick={mount} disabled={busy || !path.trim()}
                  style={{ width: "100%", borderRadius: 8, padding: "10px 12px" }}>
            {busy ? "Mounting…" : "📁 Mount + index"}
          </button>
        </div>
      </div>
    </div>
  );
}

// Save-as-workflow modal — turns draft_steps into a registered WORKFLOWS entry.
function SaveWorkflowModal({ onClose, draftSteps, onSaved }) {
  // Phase 10: if /api/email/hook-new just stashed a scaffold, prefill from it.
  const scaffold = (typeof window !== "undefined" && window.__atlas_hook_scaffold) || null;
  const [label, setLabel] = useState(scaffold ? (scaffold.label_suggestion || "") : "");
  const [icon, setIcon] = useState("⚙");
  const [tagline, setTagline] = useState("");
  const [tenants, setTenants] = useState("");
  const [scaffoldSteps] = useState(scaffold ? (scaffold.steps_suggestion || []) : null);
  const [busy, setBusy] = useState(false);
  // clear scaffold once consumed so the modal doesn't prefill forever
  useEffect(() => () => { try { delete window.__atlas_hook_scaffold; } catch (e) {} }, []);
  const save = async () => {
    if (!label.trim()) return;
    setBusy(true);
    const id = (scaffold && scaffold.name_suggestion)
      || label.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").slice(0, 40) || "workflow";
    // Prefer scaffold steps if present (structured); else fall back to plain draftSteps strings.
    const steps = scaffoldSteps
      ? scaffoldSteps
      : (draftSteps || []).map((s, i) => ({ id: "s" + i, ask: s }));
    const body = new URLSearchParams({
      id, label: label.trim(), icon: icon || "⚙", tagline: tagline.trim(),
      steps: JSON.stringify(steps), allowed_tenants: tenants.trim(),
    });
    try {
      const r = await apiFetch("/api/workflow/save", { method: "POST", body }).then(r => r.json());
      if (r.ok) { onSaved && onSaved(r); onClose(); }
    } finally { setBusy(false); }
  };
  return (
    <div className="settings-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="settings-modal" style={{ maxWidth: 520 }}>
        <div className="settings-head">
          <div>
            <div className="settings-title">Save as workflow</div>
            <div className="settings-sub">{(draftSteps || []).length} draft steps captured.</div>
          </div>
          <button className="settings-close" onClick={onClose}>×</button>
        </div>
        <div style={{ padding: "20px 24px 24px", display: "flex", flexDirection: "column", gap: 10 }}>
          <input autoFocus placeholder="Workflow name (e.g. Tenant Onboarding)" value={label}
                 onChange={e => setLabel(e.target.value)}
                 style={{ background: "var(--bg)", border: "1px solid var(--line)", color: "var(--fg)", padding: "8px 10px", borderRadius: 6, fontSize: 13 }} />
          <input placeholder="Icon (emoji)" value={icon}
                 onChange={e => setIcon(e.target.value)}
                 style={{ background: "var(--bg)", border: "1px solid var(--line)", color: "var(--fg)", padding: "8px 10px", borderRadius: 6, fontSize: 13 }} />
          <input placeholder="Tagline" value={tagline}
                 onChange={e => setTagline(e.target.value)}
                 style={{ background: "var(--bg)", border: "1px solid var(--line)", color: "var(--fg)", padding: "8px 10px", borderRadius: 6, fontSize: 13 }} />
          <input placeholder="Allowed tenants (comma separated, blank = all)" value={tenants}
                 onChange={e => setTenants(e.target.value)}
                 style={{ background: "var(--bg)", border: "1px solid var(--line)", color: "var(--fg)", padding: "8px 10px", borderRadius: 6, fontSize: 13 }} />
          <div style={{ background: "var(--panel2)", border: "1px solid var(--line)", borderRadius: 6, padding: 10, fontSize: 11, color: "var(--fg2)", maxHeight: 140, overflowY: "auto" }}>
            {scaffoldSteps
              ? scaffoldSteps.map((s, i) => (
                  <div key={i}>{i + 1}. {s.ask || s.action || (s.approval ? "[approval]" : s.final ? "[final]" : JSON.stringify(s))}</div>
                ))
              : (draftSteps || []).map((s, i) => <div key={i}>{i + 1}. {s}</div>)}
          </div>
          <button className="send-btn" onClick={save} disabled={busy || !label.trim()}
                  style={{ width: "100%", borderRadius: 8, padding: "10px 12px" }}>
            {busy ? "Saving…" : "💾 Save workflow"}
          </button>
        </div>
      </div>
    </div>
  );
}

// ─── CardBuilderModal (Phase 11) — chat-driven card template authoring ─────
// 5-step wizard mirroring telos-card-registry::Card shape (id/kind/schema/
// layout/actions/meta). Posts to /api/card/template/save and fires the
// `card-template-updated` window event so window.CARD_TYPES splices live.
const _CB_FIELD_TYPES = ["text","number","date","select","boolean","file","textarea","url","email"];
const _CB_BODY_COMPS  = ["text","table","buttons","form","media","iframe","events","fields"];
const _CB_ACTION_KINDS = ["workflow_step","http_call","emit_card","close","noop"];
function CardBuilderModal({ onClose, chat, onSaved }) {
  const [step, setStep] = useState(1);
  const [name, setName] = useState("");
  const [kind, setKind] = useState("");
  const [description, setDescription] = useState("");
  const [fields, setFields] = useState([{ name: "label", type: "text", required: true }]);
  const [bodyComps, setBodyComps] = useState(["text"]);
  const [captionTpl, setCaptionTpl] = useState("{{name}}");
  const [actions, setActions] = useState([{ id: "act1", label: "Continue", kind: "workflow_step", args: {} }]);
  const [tenantScope, setTenantScope] = useState("global");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  // Step 1 helper: ask the LLM to suggest kind + initial fields from current chat.
  const suggest = async () => {
    if (!chat) return;
    setBusy(true);
    try {
      // We don't have a dedicated /suggest endpoint v0 — derive a kind slug from
      // the entered `name`. Phase 12 will wire this into a real LLM suggestion.
      const slug = (name || "card").trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
      if (!kind && slug) setKind(slug.slice(0, 40) || "user_card");
    } finally { setBusy(false); }
  };
  const addField = () => setFields(fs => [...fs, { name: `field${fs.length+1}`, type: "text", required: false }]);
  const setField = (i, patch) => setFields(fs => fs.map((f, j) => j === i ? { ...f, ...patch } : f));
  const removeField = (i) => setFields(fs => fs.filter((_, j) => j !== i));
  const toggleBody = (c) => setBodyComps(bc => bc.includes(c) ? bc.filter(x => x !== c) : [...bc, c]);
  const addAction = () => setActions(as => [...as, { id: `act${as.length+1}`, label: "Action", kind: "workflow_step", args: {} }]);
  const setAction = (i, patch) => setActions(as => as.map((a, j) => j === i ? { ...a, ...patch } : a));
  const removeAction = (i) => setActions(as => as.filter((_, j) => j !== i));
  const save = async () => {
    setBusy(true); setError("");
    try {
      const id = (name || kind).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").slice(0, 40) || "card";
      const spec = {
        id, kind: kind.trim() || id.replace(/-/g, "_"),
        name: name.trim() || id,
        description: description.trim(),
        schema: { fields: fields.filter(f => f.name) },
        layout: {
          caption_template: captionTpl,
          body_components: bodyComps,
          footer_components: actions.length ? ["actions"] : [],
        },
        actions: actions.filter(a => a.label),
        tenant_scope: tenantScope,
        meta: { author_wallet: "", created_at: new Date().toISOString().slice(0,10), version: "v0" },
      };
      const r = await apiFetch("/api/card/template/save", {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify(spec),
      });
      const d = await r.json().catch(() => ({}));
      if (!d.ok) { setError(d.error || "save failed"); return; }
      // Fire the registry-merge event so window.CARD_TYPES picks up the new kind
      // without a page reload. mergeUserTemplates() also runs server-broadcast.
      try { window.dispatchEvent(new CustomEvent("card-template-updated", { detail: d })); } catch (e) {}
      if (onSaved) onSaved(d);
      onClose();
    } finally { setBusy(false); }
  };
  const stepHead = (n, label) => (
    <div className={`cb-step-pip ${step >= n ? "cb-step-active" : ""}`} onClick={() => setStep(n)}>
      <span className="cb-step-n">{n}</span><span className="cb-step-l">{label}</span>
    </div>
  );
  return (
    <div className="settings-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="settings-modal card-builder-modal" style={{ maxWidth: 640 }}>
        <div className="settings-head">
          <div>
            <div className="settings-title">+ Create card template</div>
            <div className="settings-sub">step {step} of 5 · stored at /cards/&lt;id&gt;.json</div>
          </div>
          <button className="settings-close" onClick={onClose}>×</button>
        </div>
        <div className="cb-steps-row">
          {stepHead(1, "name")}{stepHead(2, "fields")}{stepHead(3, "layout")}
          {stepHead(4, "actions")}{stepHead(5, "preview")}
        </div>
        <div style={{ padding: "16px 24px 24px", display: "flex", flexDirection: "column", gap: 10 }}>
          {step === 1 && (
            <>
              <label className="cb-label">What kind of thing does this card show?</label>
              <input autoFocus placeholder="Display name (e.g. Customer Summary)" value={name}
                     onChange={e => setName(e.target.value)} className="cb-input" />
              <input placeholder="kind identifier (snake_case, e.g. customer_summary)" value={kind}
                     onChange={e => setKind(e.target.value.replace(/[^a-zA-Z0-9_]/g, "_"))} className="cb-input" />
              <textarea placeholder="Description — what this card does" value={description}
                        onChange={e => setDescription(e.target.value)} rows={2} className="cb-input" />
              <select value={tenantScope} onChange={e => setTenantScope(e.target.value)} className="cb-input">
                <option value="global">Visible to all tenants</option>
                <option value="tenant:main">Tenant: main</option>
              </select>
              <button className="card-btn" onClick={suggest} disabled={busy || !name}>💡 suggest kind from name</button>
            </>
          )}
          {step === 2 && (
            <>
              <label className="cb-label">Fields ({fields.length})</label>
              {fields.map((f, i) => (
                <div key={i} className="cb-field-row">
                  <input value={f.name} placeholder="field name"
                         onChange={e => setField(i, { name: e.target.value })}
                         className="cb-input" style={{ flex: 2 }} />
                  <select value={f.type} onChange={e => setField(i, { type: e.target.value })}
                          className="cb-input" style={{ flex: 1 }}>
                    {_CB_FIELD_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
                  </select>
                  <label className="cb-checkbox-label">
                    <input type="checkbox" checked={!!f.required} onChange={e => setField(i, { required: e.target.checked })} />
                    req
                  </label>
                  <button className="card-btn card-btn-warn" onClick={() => removeField(i)}>×</button>
                </div>
              ))}
              <button className="card-btn" onClick={addField}>+ add field</button>
            </>
          )}
          {step === 3 && (
            <>
              <label className="cb-label">Caption template (use {`{{field_name}}`})</label>
              <input value={captionTpl} onChange={e => setCaptionTpl(e.target.value)} className="cb-input" />
              <label className="cb-label">Body components</label>
              <div className="cb-checkbox-grid">
                {_CB_BODY_COMPS.map(c => (
                  <label key={c} className="cb-checkbox-label">
                    <input type="checkbox" checked={bodyComps.includes(c)} onChange={() => toggleBody(c)} />
                    {c}
                  </label>
                ))}
              </div>
            </>
          )}
          {step === 4 && (
            <>
              <label className="cb-label">Action buttons ({actions.length})</label>
              {actions.map((a, i) => (
                <div key={i} className="cb-field-row">
                  <input value={a.label} placeholder="label"
                         onChange={e => setAction(i, { label: e.target.value })}
                         className="cb-input" style={{ flex: 2 }} />
                  <select value={a.kind} onChange={e => setAction(i, { kind: e.target.value })}
                          className="cb-input" style={{ flex: 1 }}>
                    {_CB_ACTION_KINDS.map(t => <option key={t} value={t}>{t}</option>)}
                  </select>
                  <button className="card-btn card-btn-warn" onClick={() => removeAction(i)}>×</button>
                </div>
              ))}
              <button className="card-btn" onClick={addAction}>+ add action</button>
            </>
          )}
          {step === 5 && (
            <>
              <label className="cb-label">Preview</label>
              <pre className="cb-preview">{JSON.stringify({
                id: (name || kind).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"),
                kind, name, description, fields, layout: { caption_template: captionTpl, body_components: bodyComps },
                actions, tenant_scope: tenantScope,
              }, null, 2)}</pre>
              {error && <div className="cb-error">⚠ {error}</div>}
            </>
          )}
          <div className="cb-nav-row">
            {step > 1 && <button className="card-btn" onClick={() => setStep(s => s - 1)} disabled={busy}>← back</button>}
            <div style={{ flex: 1 }} />
            {step < 5 && <button className="card-btn card-btn-accent" onClick={() => setStep(s => s + 1)} disabled={busy}>next →</button>}
            {step === 5 && <button className="send-btn" onClick={save} disabled={busy || !name || !kind} style={{ borderRadius: 8, padding: "8px 14px" }}>{busy ? "saving…" : "💾 save template"}</button>}
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Phase 13a: AccountsAuthSection ─────────────────────────────────────────
// Wraps the existing acct-card grid (Telegram, Twilio, Postmark, Gmail,
// OpenAI key, Stripe, Porkbun, etc. — everything currently in /api/accounts).
// All save / test round-trips are preserved unchanged. Operators see only
// SHARED_INFRA_ACCOUNTS; admin/null see everything.
function AccountsAuthSection({ role }) {
  const [accounts, setAccounts] = useState({});
  const [drafts, setDrafts] = useState({});      // per-service field drafts
  const [busy, setBusy] = useState({});
  const [feedback, setFeedback] = useState({});  // per-service last test result
  useEffect(() => { fetchJson("/api/accounts.json").then(setAccounts); }, []);
  const visibleAccounts = role === "operator"
    ? Object.fromEntries(Object.entries(accounts).filter(([sid]) => SHARED_INFRA_ACCOUNTS.includes(sid)))
    : accounts;
  const setField = (svc, field, val) => setDrafts(d => ({ ...d, [svc]: { ...(d[svc]||{}), [field]: val } }));
  const save = async (svc) => {
    setBusy(b => ({ ...b, [svc]: "saving" }));
    const fd = new URLSearchParams({ service: svc, ...(drafts[svc]||{}) });
    const r = await fetch("/api/accounts/save", { method: "POST", body: fd }).then(r => r.json());
    setAccounts(r.accounts);
    setFeedback(f => ({ ...f, [svc]: { ok: r.ok, msg: r.msg } }));
    setBusy(b => ({ ...b, [svc]: null }));
  };
  const test = async (svc) => {
    setBusy(b => ({ ...b, [svc]: "testing" }));
    const fd = new URLSearchParams({ service: svc });
    const r = await fetch("/api/accounts/test", { method: "POST", body: fd }).then(r => r.json());
    setAccounts(r.accounts);
    setFeedback(f => ({ ...f, [svc]: { ok: r.ok, msg: r.msg } }));
    setBusy(b => ({ ...b, [svc]: null }));
  };
  return (
    <div>
      <h2>Accounts &amp; Auth</h2>
      <div className="settings-sub" style={{ marginBottom: 18 }}>
        Connect accounts so workflows can reach the world.
      </div>
      <div className="settings-grid settings-grid-inline">
        {Object.entries(visibleAccounts).map(([sid, spec]) => (
          <div key={sid} className="acct-card">
            <div className="acct-head">
              <span className="acct-icon">{spec.icon}</span>
              <span className="acct-label">{spec.label}</span>
              <span className={`acct-status ${spec.verified ? "verified" : spec.configured ? "configured" : spec.partial ? "partial" : "empty"}`}>
                {spec.verified ? "✓ verified" : spec.configured ? "saved" : spec.partial ? "partial" : "not set"}
              </span>
            </div>
            <div className="acct-fields">
              {spec.fields.map(f => {
                // Phase 11.5: tour-id alias map — keep the catalog stable while
                // server-side field names evolve. Falls back to a deterministic
                // `settings-<sid>-<field-without-underscores>` form.
                const TOUR_FIELD_ALIASES = {
                  "telegram:bot_token":     "settings-telegram-token",
                  "telegram:chat_id":       "settings-telegram-chatid",
                  "twilio:account_sid":     "settings-twilio-sid",
                  "twilio:auth_token":      "settings-twilio-token",
                  "twilio:from_number":     "settings-twilio-from",
                  "postmark:server_token":  "settings-postmark-token",
                  "gmail:address":          "settings-gmail-address",
                  "gmail:app_password":     "settings-gmail-apppassword",
                };
                const tid = TOUR_FIELD_ALIASES[`${sid}:${f}`] || `settings-${sid}-${f.replace(/_/g,"")}`;
                return (
                  <input key={f}
                    data-tour-id={tid}
                    type={f.includes("key") || f.includes("token") || f.includes("secret") || f.includes("password") ? "password" : "text"}
                    placeholder={spec.set_fields.includes(f) ? `${f} · ✓ set (re-enter to change)` : f}
                    value={(drafts[sid]||{})[f] || ""}
                    onChange={e => setField(sid, f, e.target.value)} />
                );
              })}
            </div>
            <div className="acct-actions">
              <button data-tour-id={`settings-${sid}-save`} onClick={() => save(sid)} disabled={busy[sid]}>{busy[sid]==="saving"?"saving…":"save"}</button>
              <button data-tour-id={`settings-${sid}-test`} onClick={() => test(sid)} disabled={busy[sid] || !spec.configured} className="test">{busy[sid]==="testing"?"testing…":"test"}</button>
              {feedback[sid] && (
                <span className={`acct-feedback ${feedback[sid].ok ? "ok" : "err"}`}>{feedback[sid].msg}</span>
              )}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// Wire the wrapper into the SETTINGS_REGISTRY now that the component exists.
// Sub-agents 13b/c/d/e/g splice their own components the same way (either at
// module load — by directly assigning — or via window.setSettingsRegistry).
(function attachAccountsAuth() {
  const row = SETTINGS_REGISTRY.find(r => r.id === "accounts");
  if (row) row.component = AccountsAuthSection;
})();

// ─── Phase 13d: ProxyGatewaySection — per-service direct/through_atlas toggle
// Defaults are `through_atlas` for ALL services (the moat). User can flip
// individual ones to bypass capture. Per-service inline stats: tokens captured,
// requests cached, latency saved. Stats from /api/gateway.json (cached).
function ProxyGatewaySection({ role }) {
  const [data, setData] = useState(null);
  const [pending, setPending] = useState({});
  const [busy, setBusy] = useState(false);
  const [savedAt, setSavedAt] = useState(0);
  const load = () => {
    fetch("/api/gateway.json?tenant=main")
      .then(r => r.json())
      .then(d => { setData(d); setPending({}); })
      .catch(() => setData({ services: [], gateway: {}, stats: {} }));
  };
  useEffect(() => { load(); }, []);
  if (!data) return <div className="settings-pane-loading">Loading proxy gateway…</div>;
  const services = data.services || [];
  const gw = { ...(data.gateway || {}), ...pending };
  const stats = data.stats || {};
  const flip = (svc) => {
    setPending(p => ({ ...p, [svc]: (gw[svc] === "through_atlas") ? "direct" : "through_atlas" }));
  };
  const save = () => {
    setBusy(true);
    fetch("/api/gateway/save", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ tenant: "main", gateway: gw }),
    })
      .then(r => r.json())
      .then(d => { setBusy(false); setSavedAt(Date.now()); setData(prev => ({ ...prev, gateway: d.gateway || gw })); setPending({}); })
      .catch(() => setBusy(false));
  };
  const dirty = Object.keys(pending).length > 0;
  return (
    <div>
      <h2>🌐 Proxy & Gateway</h2>
      <div className="settings-sub" style={{ marginBottom: 14 }}>
        Route each external service direct or through ATLAS. through_atlas captures the
        round-trip for replay, cache, and training — the moat. Defaults are ON.
      </div>
      <table className="proxy-table">
        <thead>
          <tr>
            <th>Service</th>
            <th>Mode</th>
            <th className="proxy-stats-col">Inline stats</th>
          </tr>
        </thead>
        <tbody>
          {services.map(svc => {
            const m = gw[svc] || "through_atlas";
            const st = stats[svc] || { rows: 0, tokens: 0 };
            const tokstr = st.tokens > 1000 ? (st.tokens / 1000).toFixed(1) + "K" : String(st.tokens);
            return (
              <tr key={svc} className={`proxy-row ${pending[svc] ? "proxy-row-dirty" : ""}`}>
                <td className="proxy-svc"><code>{svc}</code></td>
                <td>
                  <button
                    className={`proxy-toggle ${m === "through_atlas" ? "proxy-toggle-on" : "proxy-toggle-off"}`}
                    onClick={() => flip(svc)}
                    title={m === "through_atlas" ? "Click to bypass ATLAS (direct)" : "Click to route through ATLAS (capture on)"}
                  >
                    {m === "through_atlas" ? "↗ through_atlas" : "→ direct"}
                  </button>
                </td>
                <td className="proxy-stats">
                  Tokens captured: <b>{tokstr}</b>{" · "}
                  Requests cached: <b>{st.rows || 0}</b>{" · "}
                  Latency saved: <b>{((st.rows || 0) * 0.026).toFixed(1)}s</b>
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
      <div className="proxy-actions">
        <button className="go" onClick={save} disabled={!dirty || busy}>
          {busy ? "Saving…" : (dirty ? `Save (${Object.keys(pending).length} changed)` : "Saved")}
        </button>
        {savedAt > 0 && !dirty && <span className="proxy-saved-flash">✓ saved</span>}
      </div>
    </div>
  );
}

// ─── Phase 13d: ReasoningCaptureSection — four content toggles + PII guard
// PII anonymization (default ON) uses bin/cccatch::REDACTORS (proven; same
// redactor dropush/secscan trusts). Output target is droplet path or HF URL.
function ReasoningCaptureSection({ role }) {
  const [cfg, setCfg]     = useState(null);
  const [stats, setStats] = useState(null);
  const [busy, setBusy]   = useState(false);
  const load = () => {
    fetch("/api/capture/config.json?tenant=main").then(r => r.json()).then(d => setCfg(d.capture || {}));
    fetch("/api/capture/stats.json?tenant=main").then(r => r.json()).then(setStats);
  };
  useEffect(() => {
    load();
    const t = setInterval(() => {
      fetch("/api/capture/stats.json?tenant=main").then(r => r.json()).then(setStats).catch(() => {});
    }, 5000);
    return () => clearInterval(t);
  }, []);
  if (!cfg) return <div className="settings-pane-loading">Loading reasoning capture…</div>;
  const set = (k, v) => setCfg(c => ({ ...c, [k]: v }));
  const save = () => {
    setBusy(true);
    fetch("/api/capture/save", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ tenant: "main", capture: cfg }),
    }).then(r => r.json()).then(d => { setBusy(false); setCfg(d.capture || cfg); load(); })
      .catch(() => setBusy(false));
  };
  const T = ({k, label, hint}) => (
    <label className={`capture-toggle ${cfg[k] ? "capture-toggle-on" : ""}`}>
      <input type="checkbox" checked={!!cfg[k]} onChange={e => set(k, e.target.checked)} />
      <span className="capture-toggle-label">{label}</span>
      {hint && <span className="capture-toggle-hint">{hint}</span>}
    </label>
  );
  const tokens = stats ? stats.this_month_tokens : 0;
  const chains = stats ? stats.this_month_chains : 0;
  const qdepth = stats ? stats.queue_depth : 0;
  const tstr = tokens > 1000 ? (tokens / 1000).toFixed(1) + "K" : String(tokens || 0);
  return (
    <div>
      <h2>🧠 Reasoning Capture</h2>
      <div className="settings-sub" style={{ marginBottom: 14 }}>
        Every <code>through_atlas</code> turn is captured async to <code>dataset/llm_capture/</code>.
        PII tokens are stripped via the proven cccatch redactor before write.
      </div>
      <div className="capture-meter">
        <div className="capture-meter-big">{tstr}</div>
        <div className="capture-meter-sub">tokens captured this month · {chains} chains · queue depth {qdepth}</div>
      </div>
      <div className="capture-grid">
        <T k="extended_thinking"  label="Extended thinking"  hint="claude-opus thinking blocks" />
        <T k="tool_use_chains"    label="Tool-use chains"    hint="tool_use → tool_result pairs" />
        <T k="multi_turn_context" label="Multi-turn context" hint="prior turns + system prompt" />
        <T k="streaming_partials" label="Streaming partials" hint="incremental tokens (heavy)" />
      </div>
      <div className="capture-pii">
        <T k="pii_anonymization" label="PII anonymization" hint="hf_/sk-/ghp_/AKIA/xoxb redacted (cccatch)" />
      </div>
      <div className="capture-target">
        <div className="capture-target-label">Output target</div>
        <div className="capture-target-row">
          <select value={cfg.output_target || "droplet"} onChange={e => set("output_target", e.target.value)}>
            <option value="droplet">droplet (Tom's server)</option>
            <option value="hf">HuggingFace dataset</option>
          </select>
          <input
            type="text"
            placeholder={cfg.output_target === "hf" ? "https://huggingface.co/datasets/owner/name" : "/data/Projects/ATLAS/dataset/llm_capture"}
            value={cfg.target_path || ""}
            onChange={e => set("target_path", e.target.value)}
          />
        </div>
      </div>
      <div className="capture-actions">
        <button className="go" onClick={save} disabled={busy}>{busy ? "Saving…" : "Save capture config"}</button>
      </div>
    </div>
  );
}

// ─── Phase 13d: MarketplaceSection — browse/publish/install addons
// Storage: each published addon → /forge/published/<kind>_<id>.json
// Per-tenant slugs/<tenant>.json gains marketplace_optin (default false).
// Install count tracked via /forge/published/_stats.json. Featured curated
// via /forge/published/featured.json.
function MarketplaceSection({ role }) {
  const [data, setData] = useState(null);
  const [kindFilter, setKindFilter] = useState("");
  const [q, setQ] = useState("");
  const [installBusy, setInstallBusy] = useState({});
  const [publishOpen, setPublishOpen] = useState(false);
  const [publishKind, setPublishKind] = useState("workflow");
  const [publishId, setPublishId] = useState("");
  const [publishBusy, setPublishBusy] = useState(false);
  const load = () => {
    const u = new URL("/api/marketplace.json", window.location.href);
    if (kindFilter) u.searchParams.set("kind", kindFilter);
    if (q) u.searchParams.set("q", q);
    fetch(u).then(r => r.json()).then(setData).catch(() => setData({ addons: [], kinds: [] }));
  };
  useEffect(() => { load(); }, [kindFilter, q]);
  if (!data) return <div className="settings-pane-loading">Loading marketplace…</div>;
  const install = (row) => {
    const key = `${row.kind}/${row.id}`;
    setInstallBusy(b => ({ ...b, [key]: true }));
    fetch("/api/marketplace/install", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ kind: row.kind, id: row.id }),
    }).then(r => r.json()).then(() => { setInstallBusy(b => ({ ...b, [key]: false })); load(); })
      .catch(() => setInstallBusy(b => ({ ...b, [key]: false })));
  };
  const publish = () => {
    if (!publishId.trim()) return;
    setPublishBusy(true);
    fetch("/api/marketplace/publish", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ kind: publishKind, id: publishId.trim() }),
    }).then(r => r.json()).then(() => {
      setPublishBusy(false); setPublishOpen(false); setPublishId(""); load();
    }).catch(() => setPublishBusy(false));
  };
  const rows = data.addons || [];
  const featured = rows.filter(r => r.featured);
  const rest = rows.filter(r => !r.featured);
  const kindChips = Array.from(new Set(rows.map(r => r.kind).filter(Boolean)));
  return (
    <div>
      <h2>🏪 Marketplace</h2>
      <div className="settings-sub" style={{ marginBottom: 14 }}>
        Browse and install addons published across opted-in tenants. Each install
        is recorded in <code>forge/published/_stats.json</code>.
      </div>
      <div className="marketplace-controls">
        <input
          type="text" className="marketplace-search"
          placeholder="Search addons (name · description · id)…"
          value={q} onChange={e => setQ(e.target.value)}
        />
        <button className="go" onClick={() => setPublishOpen(true)}>+ publish own</button>
      </div>
      <div className="marketplace-chips">
        <span className={`marketplace-chip ${!kindFilter ? "on" : ""}`} onClick={() => setKindFilter("")}>all</span>
        {kindChips.map(k => (
          <span key={k} className={`marketplace-chip ${kindFilter === k ? "on" : ""}`} onClick={() => setKindFilter(k)}>{k}</span>
        ))}
      </div>
      {featured.length > 0 && (
        <div className="marketplace-section">
          <div className="marketplace-section-label">★ featured</div>
          <div className="marketplace-grid">
            {featured.map(row => (
              <MarketplaceCard key={`${row.kind}/${row.id}`} row={row}
                busy={!!installBusy[`${row.kind}/${row.id}`]} onInstall={() => install(row)} />
            ))}
          </div>
        </div>
      )}
      <div className="marketplace-section">
        <div className="marketplace-section-label">all published ({rest.length})</div>
        {rest.length === 0 ? (
          <div className="marketplace-empty">No addons published yet. Click <b>+ publish own</b> to share one.</div>
        ) : (
          <div className="marketplace-grid">
            {rest.map(row => (
              <MarketplaceCard key={`${row.kind}/${row.id}`} row={row}
                busy={!!installBusy[`${row.kind}/${row.id}`]} onInstall={() => install(row)} />
            ))}
          </div>
        )}
      </div>
      {publishOpen && (
        <div className="marketplace-modal" onClick={e => { if (e.target === e.currentTarget) setPublishOpen(false); }}>
          <div className="marketplace-modal-inner">
            <div className="marketplace-modal-head">Publish your addon</div>
            <div className="marketplace-modal-body">
              <label>Kind</label>
              <select value={publishKind} onChange={e => setPublishKind(e.target.value)}>
                {["mcp_tool","card","workflow","template","flow_step","prompt","tour","theme"].map(k =>
                  <option key={k} value={k}>{k}</option>)}
              </select>
              <label>Addon ID</label>
              <input type="text" placeholder="my_addon_id" value={publishId} onChange={e => setPublishId(e.target.value)} />
              <div className="marketplace-modal-note">
                Copies <code>addons/{publishKind}/{publishId || "<id>"}.json</code> →
                <code> forge/published/{publishKind}_{publishId || "<id>"}.json</code>
              </div>
            </div>
            <div className="marketplace-modal-actions">
              <button onClick={() => setPublishOpen(false)}>cancel</button>
              <button className="go" onClick={publish} disabled={publishBusy || !publishId.trim()}>
                {publishBusy ? "Publishing…" : "Publish"}
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

function MarketplaceCard({ row, busy, onInstall }) {
  const avgRating = row.ratings && row.ratings.length
    ? (row.ratings.reduce((a,b) => a + b, 0) / row.ratings.length).toFixed(1)
    : null;
  return (
    <div className={`marketplace-card ${row.featured ? "marketplace-card-featured" : ""}`}>
      <div className="marketplace-card-head">
        <span className="marketplace-card-kind">{row.kind}</span>
        {row.featured && <span className="marketplace-card-star">★</span>}
      </div>
      <div className="marketplace-card-name">{row.name}</div>
      <div className="marketplace-card-id">{row.id}</div>
      <div className="marketplace-card-desc">{row.description || <i>no description</i>}</div>
      <div className="marketplace-card-meta">
        <span>v{row.version}</span>
        {row.author && <span>· {row.author}</span>}
      </div>
      <div className="marketplace-card-stats">
        <span>{row.installs} installs</span>
        {avgRating && <span>· {avgRating}★ ({row.ratings.length})</span>}
      </div>
      <div className="marketplace-card-actions">
        <button className="go" onClick={onInstall} disabled={busy}>
          {busy ? "Installing…" : "install"}
        </button>
        <button className="marketplace-btn-stats" title="view stats"
                onClick={() => alert(`installs: ${row.installs}\nratings: ${(row.ratings||[]).join(", ") || "none"}`)}>
          stats
        </button>
      </div>
    </div>
  );
}

// Wire the three Phase 13d sections into SETTINGS_REGISTRY.
(function attachPhase13d() {
  const map = {
    proxy_gateway:      ProxyGatewaySection,
    reasoning_capture:  ReasoningCaptureSection,
    marketplace:        MarketplaceSection,
  };
  for (const id in map) {
    const row = SETTINGS_REGISTRY.find(r => r.id === id);
    if (row) row.component = map[id];
  }
})();

// ─── Phase 13e: IdentitySection — Wallet · Email+Password · API Keys ─────
function IdentitySection() {
  const [tab, setTab] = useState("wallet");
  const wallet = (typeof localStorage !== "undefined" && localStorage.getItem("atlas_wallet")) || null;

  // Wallet tab state
  const [walletStatus, setWalletStatus] = useState(null);
  const [walletBusy, setWalletBusy] = useState(false);
  useEffect(() => {
    if (tab !== "wallet" || !wallet) return;
    apiFetch("/api/identity/wallet/sessions.json?wallet=" + encodeURIComponent(wallet))
      .then(r => r.ok ? r.json() : null)
      .then(d => setWalletStatus(d || {}))
      .catch(() => setWalletStatus({}));
  }, [tab, wallet]);
  const logoutAll = async () => {
    if (!wallet) return;
    if (!window.confirm("Log out ALL sessions for this wallet?")) return;
    setWalletBusy(true);
    try {
      await apiFetch("/api/identity/wallet/logout_all", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({wallet}),
      });
      alert("Sessions cleared. You'll be logged out next request.");
    } finally { setWalletBusy(false); }
  };

  // Email+Password tab state
  const [pwEmail, setPwEmail] = useState("");
  const [pwPass, setPwPass] = useState("");
  const [pwConf, setPwConf] = useState("");
  const [pwBusy, setPwBusy] = useState(false);
  const [pwMsg, setPwMsg]   = useState(null);
  const setPassword = async () => {
    setPwBusy(true); setPwMsg(null);
    try {
      const r = await fetch("/api/identity/password/set", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({email: pwEmail, password: pwPass, confirm: pwConf}),
      });
      const d = await r.json().catch(() => ({}));
      setPwMsg({ok: r.ok && d.ok, text: d.ok ? `Saved — virtual wallet ${d.wallet}` : (d.error || "failed")});
    } finally { setPwBusy(false); }
  };

  // API Keys tab state
  const [keys, setKeys] = useState([]);
  const [newKeyName, setNewKeyName] = useState("");
  const [freshKey, setFreshKey] = useState(null);
  const [keysBusy, setKeysBusy] = useState(false);
  const refreshKeys = useCallback(() => {
    apiFetch("/api/identity/apikeys.json").then(r => r.json())
      .then(d => setKeys(d.api_keys || [])).catch(() => {});
  }, []);
  useEffect(() => { if (tab === "apikeys") refreshKeys(); }, [tab, refreshKeys]);
  const createKey = async () => {
    setKeysBusy(true); setFreshKey(null);
    try {
      const r = await apiFetch("/api/identity/apikey/create", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({name: newKeyName || "unnamed"}),
      });
      const d = await r.json();
      if (d.ok) { setFreshKey(d); setNewKeyName(""); refreshKeys(); }
      else alert(d.error || "create failed");
    } finally { setKeysBusy(false); }
  };
  const revokeKey = async (id) => {
    if (!window.confirm("Revoke this key? Any client using it will start failing.")) return;
    setKeysBusy(true);
    try {
      await apiFetch("/api/identity/apikey/revoke", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({id}),
      });
      refreshKeys();
    } finally { setKeysBusy(false); }
  };

  return (
    <div className="identity-section">
      <h2>🔐 Identity & Auth</h2>
      <div className="identity-tabs">
        {["wallet","password","apikeys"].map(t => (
          <button key={t}
            className={`identity-tab ${tab===t?"active":""}`}
            onClick={() => setTab(t)}>
            {t === "wallet" ? "Wallet" : t === "password" ? "Email + Password" : "API Keys"}
          </button>
        ))}
      </div>
      {tab === "wallet" && (
        <div className="identity-pane">
          {!wallet && <div className="identity-empty">No wallet logged in.</div>}
          {wallet && (
            <>
              <div className="identity-row"><b>Wallet address:</b> <code>{wallet}</code></div>
              <div className="identity-row"><b>Sessions:</b> {walletStatus ? (
                Array.isArray(walletStatus.sessions) && walletStatus.sessions.length > 0
                  ? walletStatus.sessions.length + " active"
                  : (walletStatus.active ? "1 active (this browser)" : "0")
              ) : "loading…"}</div>
              <button className="identity-danger" onClick={logoutAll} disabled={walletBusy}>
                {walletBusy ? "…" : "Log out all sessions"}
              </button>
            </>
          )}
        </div>
      )}
      {tab === "password" && (
        <div className="identity-pane">
          <div className="identity-form-row">
            <label>Email</label>
            <input type="email" value={pwEmail} onChange={e => setPwEmail(e.target.value)}
                   placeholder="you@example.com" />
          </div>
          <div className="identity-form-row">
            <label>Password</label>
            <input type="password" value={pwPass} onChange={e => setPwPass(e.target.value)}
                   placeholder="min 8 chars + letter + digit" />
          </div>
          <div className="identity-form-row">
            <label>Confirm</label>
            <input type="password" value={pwConf} onChange={e => setPwConf(e.target.value)}
                   placeholder="repeat password" />
          </div>
          <button onClick={setPassword} disabled={pwBusy || !pwEmail || !pwPass}>
            {pwBusy ? "saving…" : "Set password"}
          </button>
          {pwMsg && (
            <div className={`identity-msg ${pwMsg.ok ? "ok" : "err"}`}>{pwMsg.text}</div>
          )}
          <div className="identity-hint">
            Sets a virtual wallet derived from your email so polymorphic UER stays consistent.
          </div>
        </div>
      )}
      {tab === "apikeys" && (
        <div className="identity-pane">
          <div className="identity-form-row">
            <label>New key name</label>
            <input type="text" value={newKeyName} onChange={e => setNewKeyName(e.target.value)}
                   placeholder="e.g. ci-runner-prod" />
            <button onClick={createKey} disabled={keysBusy}>
              {keysBusy ? "…" : "Create"}
            </button>
          </div>
          {freshKey && (
            <div className="identity-fresh-key">
              <b>NEW KEY (shown once):</b>
              <code>{freshKey.key}</code>
              <div className="identity-hint">{freshKey.warning}</div>
            </div>
          )}
          <table className="identity-keys-table">
            <thead><tr><th>Name</th><th>Prefix</th><th>Created</th><th>Last used</th><th></th></tr></thead>
            <tbody>
              {keys.length === 0 && <tr><td colSpan={5} className="identity-empty">No keys yet.</td></tr>}
              {keys.map(k => (
                <tr key={k.id}>
                  <td>{k.name}</td>
                  <td><code>{k.prefix}…</code></td>
                  <td>{k.created_at ? new Date(k.created_at*1000).toLocaleDateString() : "—"}</td>
                  <td>{k.last_used ? new Date(k.last_used*1000).toLocaleString() : "never"}</td>
                  <td><button className="identity-danger" onClick={() => revokeKey(k.id)}>revoke</button></td>
                </tr>
              ))}
            </tbody>
          </table>
          <div className="identity-hint">
            Use as: <code>Authorization: Bearer atk_***</code>. Keys are stored hashed; the raw value is shown ONCE on creation.
          </div>
        </div>
      )}
    </div>
  );
}
(function attachIdentitySection() {
  const row = SETTINGS_REGISTRY.find(r => r.id === "identity_auth" || r.id === "identity");
  if (row) row.component = IdentitySection;
})();

// ─── Phase 13e: UsageTokensSection — provider tokens + cost + caps ──────
const _USAGE_PROVIDERS = ["claude","openai","hf","cloud_ollama","local_ollama"];
function UsageTokensSection() {
  const [timeframe, setTimeframe] = useState("this_week");
  const [data, setData] = useState(null);
  const [busy, setBusy] = useState(false);
  const [draftCaps, setDraftCaps] = useState({});
  const tenant = (typeof localStorage !== "undefined" && localStorage.getItem("atlas_tenant")) || "main";

  const refresh = useCallback(() => {
    setBusy(true);
    apiFetch("/api/usage/tokens.json?tenant=" + encodeURIComponent(tenant) +
             "&timeframe=" + encodeURIComponent(timeframe))
      .then(r => r.json()).then(d => { setData(d); setDraftCaps(d.caps || {}); })
      .finally(() => setBusy(false));
  }, [timeframe, tenant]);
  useEffect(() => { refresh(); }, [refresh]);

  const saveCaps = async () => {
    setBusy(true);
    try {
      await apiFetch("/api/usage/caps/save", {
        method: "POST", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({tenant, caps: draftCaps}),
      });
      refresh();
    } finally { setBusy(false); }
  };

  const setCap = (prov, field, val) => {
    setDraftCaps(c => ({...c, [prov]: {...(c[prov]||{}), [field]: val}}));
  };

  const exportUrl = "/api/usage/export.csv?tenant=" + encodeURIComponent(tenant) +
                    "&timeframe=" + encodeURIComponent(timeframe);

  const providers = (data && data.providers) || {};
  const totalCost = (data && data.total_cost) || 0;
  const topChats  = (data && data.top_chats) || [];

  return (
    <div className="usage-section">
      <h2>📊 Usage & Tokens</h2>
      <div className="usage-tabs">
        {["this_week","this_month","lifetime"].map(t => (
          <button key={t} className={`usage-tab ${timeframe===t?"active":""}`}
                  onClick={() => setTimeframe(t)}>
            {t.replace("_"," ")}
          </button>
        ))}
        <span className="usage-tab-spacer" />
        <a className="usage-export-btn" href={exportUrl} download>Export CSV</a>
      </div>
      <div className="usage-total-cost">
        <span className="usage-total-label">Total ({timeframe.replace("_"," ")}):</span>
        <span className="usage-total-value">${totalCost.toFixed(4)}</span>
      </div>
      <table className="usage-providers-table">
        <thead><tr><th>Provider</th><th>Tokens in</th><th>Tokens out</th><th>Calls</th><th>Cost</th>
                   <th>Daily cap</th><th>Monthly cap</th><th>Per-call cap</th></tr></thead>
        <tbody>
          {_USAGE_PROVIDERS.map(p => {
            const row = providers[p] || {tokens_in:0,tokens_out:0,calls:0,cost_estimate:0};
            const cap = draftCaps[p] || {};
            return (
              <tr key={p}>
                <td><b>{p}</b></td>
                <td>{row.tokens_in.toLocaleString()}</td>
                <td>{row.tokens_out.toLocaleString()}</td>
                <td>{row.calls}</td>
                <td>${(row.cost_estimate||0).toFixed(4)}</td>
                <td><input type="number" step="0.01" className="token-chart-cap"
                           value={cap.daily ?? ""} onChange={e => setCap(p,"daily",e.target.value)} /></td>
                <td><input type="number" step="0.01" className="token-chart-cap"
                           value={cap.monthly ?? ""} onChange={e => setCap(p,"monthly",e.target.value)} /></td>
                <td><input type="number" step="0.01" className="token-chart-cap"
                           value={cap.per_call ?? ""} onChange={e => setCap(p,"per_call",e.target.value)} /></td>
              </tr>
            );
          })}
        </tbody>
      </table>
      <button onClick={saveCaps} disabled={busy}>{busy ? "saving…" : "Save caps"}</button>
      <h3 className="usage-h3">Top consumers (this week)</h3>
      <table className="token-chart-leaderboard">
        <thead><tr><th>Chat</th><th>Tokens</th><th>Cost</th><th>Calls</th></tr></thead>
        <tbody>
          {topChats.length === 0 && <tr><td colSpan={4} className="usage-empty">No captures yet.</td></tr>}
          {topChats.map(c => (
            <tr key={c.chat_id}>
              <td>{c.chat_id}</td>
              <td>{c.tokens.toLocaleString()}</td>
              <td>${(c.cost||0).toFixed(4)}</td>
              <td>{c.calls}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
(function attachUsageTokens() {
  const row = SETTINGS_REGISTRY.find(r => r.id === "usage_tokens");
  if (row) row.component = UsageTokensSection;
})();

// ─── Phase 13g: InflowFeed / OutflowFeed — shared between SettingsSection
// and the central I/O tab (sidebar items also open the same view).
// Single source of truth: /api/inflow/list.json + /api/outflow/list.json.
function _ioStatusClass(s) {
  if (s === "received") return "io-feed-pill received";
  if (s === "routed")   return "io-feed-pill ok";
  if (s === "ignored")  return "io-feed-pill muted";
  if (s === "hooked")   return "io-feed-pill ok";
  if (s === "queued")   return "io-feed-pill received";
  if (s === "sent")     return "io-feed-pill ok";
  if (s === "finished") return "io-feed-pill ok";
  if (s === "failed")   return "io-feed-pill err";
  if (s === "paused")   return "io-feed-pill muted";
  if (s === "target_locked") return "io-feed-pill ok";
  return "io-feed-pill";
}
function _ioFmtTs(ts) {
  if (!ts) return "";
  try { return new Date(ts * 1000).toLocaleString(); } catch (e) { return String(ts); }
}

function InflowFeed({ embed }) {
  const [rows, setRows] = useState([]);
  const [channel, setChannel] = useState("");
  const [status, setStatus] = useState("");
  const [expanded, setExpanded] = useState(null);
  const [routeFor, setRouteFor] = useState(null);
  const [routeWf, setRouteWf] = useState("");

  const refresh = useCallback(() => {
    const qp = new URLSearchParams();
    if (channel) qp.set("channel", channel);
    if (status)  qp.set("status", status);
    qp.set("limit", "200");
    fetch("/api/inflow/list.json?" + qp.toString())
      .then(r => r.json())
      .then(d => setRows(d.rows || []))
      .catch(() => {});
  }, [channel, status]);
  useEffect(() => { refresh(); const t = setInterval(refresh, 5000); return () => clearInterval(t); }, [refresh]);

  const doAction = async (iid, action, route_to) => {
    const body = { inflow_id: iid, action };
    if (route_to) body.route_to = route_to;
    await fetch("/api/inflow/action", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    setRouteFor(null); setRouteWf("");
    refresh();
  };

  const channels = Array.from(new Set(rows.map(r => r.channel).filter(Boolean))).sort();
  const statuses = ["received","routed","ignored","hooked","failed"];

  return (
    <div className={"inflow-feed io-feed " + (embed ? "io-feed-embed" : "")}>
      <div className="io-feed-filters">
        <label>channel:
          <select value={channel} onChange={e => setChannel(e.target.value)}>
            <option value="">all</option>
            {channels.map(c => <option key={c} value={c}>{c}</option>)}
          </select>
        </label>
        <label>status:
          <select value={status} onChange={e => setStatus(e.target.value)}>
            <option value="">all</option>
            {statuses.map(s => <option key={s} value={s}>{s}</option>)}
          </select>
        </label>
        <span className="io-feed-count">{rows.length} rows</span>
        <button className="io-feed-refresh" onClick={refresh}>↻</button>
      </div>
      <div className="io-feed-table">
        {rows.length === 0 && <div className="io-feed-empty">No inflow rows yet — webhooks land here.</div>}
        {rows.map(r => (
          <div key={r.inflow_id} className="inflow-row io-feed-row">
            <div className="io-feed-row-head" onClick={() => setExpanded(expanded === r.inflow_id ? null : r.inflow_id)}>
              <span className={_ioStatusClass(r.status)}>{r.status}</span>
              <span className="io-feed-chip">📥 {r.channel}</span>
              <span className="io-feed-source" title={r.source}>{r.source || "(no source)"}</span>
              <span className="io-feed-ts">{_ioFmtTs(r.ts)}</span>
            </div>
            {expanded === r.inflow_id && (
              <div className="inflow-row-body io-feed-row-body">
                <pre className="io-feed-payload">{JSON.stringify(r.payload, null, 2).slice(0, 4000)}</pre>
                <div className="io-feed-actions">
                  {routeFor === r.inflow_id ? (
                    <>
                      <input type="text" placeholder="workflow_id"
                             value={routeWf} onChange={e => setRouteWf(e.target.value)} />
                      <button onClick={() => doAction(r.inflow_id, "route", routeWf)} disabled={!routeWf}>→ route</button>
                      <button onClick={() => { setRouteFor(null); setRouteWf(""); }}>cancel</button>
                    </>
                  ) : (
                    <>
                      <button onClick={() => setRouteFor(r.inflow_id)}>→ route to workflow…</button>
                      <button onClick={() => doAction(r.inflow_id, "ignore")}>✗ ignore</button>
                      <button onClick={() => doAction(r.inflow_id, "hook")}>⊕ hook to new workflow</button>
                    </>
                  )}
                  {r.chat_id && (
                    <span className="io-feed-chat">chat: {r.chat_id}</span>
                  )}
                </div>
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

function OutflowFeed({ embed }) {
  const [rows, setRows] = useState([]);
  const [destination, setDestination] = useState("");
  const [status, setStatus] = useState("");
  const [selected, setSelected] = useState({});

  const refresh = useCallback(() => {
    const qp = new URLSearchParams();
    if (destination) qp.set("destination", destination);
    if (status)      qp.set("status", status);
    qp.set("limit", "200");
    fetch("/api/outflow/list.json?" + qp.toString())
      .then(r => r.json())
      .then(d => setRows(d.rows || []))
      .catch(() => {});
  }, [destination, status]);
  useEffect(() => { refresh(); const t = setInterval(refresh, 5000); return () => clearInterval(t); }, [refresh]);

  const bulkAction = async (action) => {
    const ids = Object.keys(selected).filter(k => selected[k]);
    if (!ids.length) return;
    await fetch("/api/outflow/action", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ outflow_ids: ids, action }),
    });
    setSelected({});
    refresh();
  };

  const destinations = Array.from(new Set(rows.map(r => r.destination).filter(Boolean))).sort();
  const statuses = ["queued","sent","finished","failed","paused","target_locked"];
  const selectedCount = Object.values(selected).filter(Boolean).length;

  return (
    <div className={"outflow-feed io-feed " + (embed ? "io-feed-embed" : "")}>
      <div className="io-feed-filters">
        <label>destination:
          <select value={destination} onChange={e => setDestination(e.target.value)}>
            <option value="">all</option>
            {destinations.map(d => <option key={d} value={d}>{d}</option>)}
          </select>
        </label>
        <label>status:
          <select value={status} onChange={e => setStatus(e.target.value)}>
            <option value="">all</option>
            {statuses.map(s => <option key={s} value={s}>{s}</option>)}
          </select>
        </label>
        <span className="io-feed-count">{rows.length} rows · {selectedCount} selected</span>
        <button className="io-feed-refresh" onClick={refresh}>↻</button>
      </div>
      <div className="outflow-bulk io-feed-bulk">
        <button onClick={() => bulkAction("label_finished")} disabled={!selectedCount}>✓ finished</button>
        <button onClick={() => bulkAction("target_locked")} disabled={!selectedCount}>🎯 target locked</button>
        <button onClick={() => bulkAction("retry")} disabled={!selectedCount}>↻ retry</button>
        <button onClick={() => bulkAction("pause")} disabled={!selectedCount}>⏸ pause</button>
      </div>
      <div className="io-feed-table">
        {rows.length === 0 && <div className="io-feed-empty">No outflow rows yet — every send writes one row here.</div>}
        {rows.map(r => (
          <div key={r.outflow_id} className="outflow-row io-feed-row">
            <input type="checkbox"
                   checked={!!selected[r.outflow_id]}
                   onChange={e => setSelected(s => ({ ...s, [r.outflow_id]: e.target.checked }))} />
            <span className={_ioStatusClass(r.status)}>{r.status}</span>
            <span className="io-feed-chip">📤 {r.destination}</span>
            <span className="io-feed-source" title={r.target}>{r.target || "(no target)"}</span>
            <span className="io-feed-ts">{_ioFmtTs(r.ts)}</span>
            <span className="io-feed-retries">retries: {r.retries || 0}</span>
            {r.last_error && (
              <span className="io-feed-err" title={r.last_error}>err: {String(r.last_error).slice(0, 60)}…</span>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

function InflowSection() {
  return (
    <div>
      <h2>📥 Inflow</h2>
      <div className="settings-sub" style={{ marginBottom: 14 }}>
        Every inbound signal (webhook, email, push, MCP call) lands here first.
        Add a channel = add ONE adapter — bookkeeping is unified.
      </div>
      <InflowFeed />
    </div>
  );
}

function OutflowSection() {
  return (
    <div>
      <h2>📤 Outflow</h2>
      <div className="settings-sub" style={{ marginBottom: 14 }}>
        Every outbound side-effect (Telegram, SMS, email, LLM call) writes a row
        here before/after firing. Bulk-label, retry, or lock targets from one place.
      </div>
      <OutflowFeed />
    </div>
  );
}

(function attachInflowOutflow() {
  const inRow  = SETTINGS_REGISTRY.find(r => r.id === "inflow");
  const outRow = SETTINGS_REGISTRY.find(r => r.id === "outflow");
  if (inRow)  inRow.component  = InflowSection;
  if (outRow) outRow.component = OutflowSection;
})();

// ─── Phase 13g: IOCentralTab — central main-area view ────────────────────
// Rendered when CATEGORIES["io"] is active. Same feeds the settings sections
// surface; this view lets the user reach them without opening Settings.
function IOCentralTab() {
  const [pane, setPane] = useState("inflow");   // "inflow" | "outflow"
  return (
    <div className="io-central" style={{ padding: 16, overflow: "auto", height: "100%" }}>
      <div className="io-feed-tabs" style={{ display: "flex", gap: 8, marginBottom: 12 }}>
        <button className={`io-feed-tab ${pane === "inflow" ? "active" : ""}`}
                onClick={() => setPane("inflow")}>📥 Inflow</button>
        <button className={`io-feed-tab ${pane === "outflow" ? "active" : ""}`}
                onClick={() => setPane("outflow")}>📤 Outflow</button>
      </div>
      {pane === "inflow" ? <InflowFeed embed /> : <OutflowFeed embed />}
    </div>
  );
}

// ─── Phase 13g: IOSidebarItems — live-count sidebar pills ─────────────────
function IOSidebarItems({ onOpen }) {
  const [inflowN, setInflowN] = useState(0);
  const [outflowN, setOutflowN] = useState(0);
  const refresh = useCallback(() => {
    const since24 = Math.floor(Date.now() / 1000) - 86400;
    fetch(`/api/inflow/list.json?status=received&since=${since24}&limit=500`)
      .then(r => r.json()).then(d => setInflowN((d.rows || []).length)).catch(() => {});
    fetch("/api/outflow/list.json?status=queued&limit=500")
      .then(r => r.json()).then(d => setOutflowN((d.rows || []).length)).catch(() => {});
  }, []);
  useEffect(() => { refresh(); const t = setInterval(refresh, 8000); return () => clearInterval(t); }, [refresh]);
  return (
    <div className="io-sidebar" data-tour-id="sidebar-io" style={{ margin: "8px 0" }}>
      <div className="chat-list-group">📥📤 I/O</div>
      <div className="io-sidebar-item inflow-sidebar"
           data-tour-id="sidebar-inflow"
           onClick={() => onOpen && onOpen()}
           style={{ display: "flex", gap: 6, alignItems: "center", padding: "4px 8px",
                    fontSize: 12, cursor: "pointer", borderRadius: 4 }}>
        <span>📥</span>
        <span style={{ flex: 1 }}>Inflow</span>
        <span className="io-sidebar-count">({inflowN})</span>
      </div>
      <div className="io-sidebar-item outflow-sidebar"
           data-tour-id="sidebar-outflow"
           onClick={() => onOpen && onOpen()}
           style={{ display: "flex", gap: 6, alignItems: "center", padding: "4px 8px",
                    fontSize: 12, cursor: "pointer", borderRadius: 4 }}>
        <span>📤</span>
        <span style={{ flex: 1 }}>Outflow</span>
        <span className="io-sidebar-count">({outflowN})</span>
      </div>
    </div>
  );
}

// ─── Phase 13a: SettingsPlaceholder ─────────────────────────────────────────
// Rendered for any SETTINGS_REGISTRY row whose component is still null.
// Sub-phases 13b/c/d/e/g replace this with their real section components.
function SettingsPlaceholder({ section }) {
  const phase = section && section.sub_phase ? section.sub_phase : "13b";
  return (
    <div>
      <h2>{section.icon} {section.label}</h2>
      <div className="settings-placeholder">
        Coming in sub-phase {phase}.
      </div>
    </div>
  );
}


// ─── PHASE_13B_COMPONENTS_INSTALLED · Operations Sections ─────────────────────
// Five sections: Notifications, Team, Health, Voice, Webhooks.
// Each is a controlled card panel rendered inside .settings-pane via SETTINGS_REGISTRY.

const PHASE13B_EVENT_KINDS = [
  "approval_pending", "workflow_failed", "new_inbound_email",
  "daily_summary", "plan_complete", "health_alert",
];
const PHASE13B_CHANNELS = ["email", "sms", "telegram", "in_app"];

function _13bTenant() {
  try {
    const t = (localStorage.getItem("atlas_tenant") || "main");
    return t || "main";
  } catch (e) { return "main"; }
}

// ─── NotificationsSection ───────────────────────────────────────────────────
function NotificationsSection() {
  const tenant = _13bTenant();
  const [rules, setRules] = useState([]);
  const [quiet, setQuiet] = useState({ start: "22:00", end: "07:00" });
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState("");
  const [showAdd, setShowAdd] = useState(false);
  const [draft, setDraft] = useState({ event_kind: PHASE13B_EVENT_KINDS[0], channels: [], priority: "normal", quiet_hours: { start: "", end: "" } });

  useEffect(() => {
    fetch(`/api/notifications/rules.json?tenant=${tenant}`)
      .then(r => r.json())
      .then(d => { setRules(d.rules || []); setQuiet(d.default_quiet || quiet); })
      .catch(() => {});
  }, []); // eslint-disable-line

  const save = () => {
    setBusy(true); setMsg("");
    fetch("/api/notifications/save", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ tenant, rules, default_quiet: quiet }),
    }).then(r => r.json())
      .then(d => { setMsg(d.ok ? "saved" : "save failed"); setBusy(false); })
      .catch(e => { setMsg("error: " + e); setBusy(false); });
  };

  const removeRule = (i) => setRules(rs => rs.filter((_, idx) => idx !== i));
  const addDraft = () => {
    if (!draft.event_kind) return;
    setRules(rs => [...rs, draft]);
    setDraft({ event_kind: PHASE13B_EVENT_KINDS[0], channels: [], priority: "normal", quiet_hours: { start: "", end: "" } });
    setShowAdd(false);
  };
  const toggleChan = (c) => setDraft(d => ({ ...d, channels: d.channels.includes(c) ? d.channels.filter(x => x !== c) : [...d.channels, c] }));

  return (
    <div className="notif-section">
      <h2>🔔 Notifications</h2>
      <p className="settings-sub">Route events to channels. Per-rule quiet hours override the tenant default.</p>
      <div className="notif-quiet">
        <label>Default quiet hours: </label>
        <input className="notif-time" type="time" value={quiet.start} onChange={e => setQuiet({ ...quiet, start: e.target.value })} />
        <span> → </span>
        <input className="notif-time" type="time" value={quiet.end} onChange={e => setQuiet({ ...quiet, end: e.target.value })} />
      </div>
      <table className="notif-table">
        <thead><tr><th>Event</th><th>Channels</th><th>Priority</th><th>Quiet</th><th></th></tr></thead>
        <tbody>
          {rules.length === 0 && <tr><td colSpan="5" className="notif-empty">No rules yet. Click [+ Add Rule] to create one.</td></tr>}
          {rules.map((r, i) => (
            <tr key={i} className="notif-row">
              <td>{r.event_kind}</td>
              <td>{(r.channels || []).join(", ") || "—"}</td>
              <td>{r.priority || "normal"}</td>
              <td>{r.quiet_hours && r.quiet_hours.start ? `${r.quiet_hours.start}→${r.quiet_hours.end}` : "(use default)"}</td>
              <td><button className="notif-del" onClick={() => removeRule(i)}>✕</button></td>
            </tr>
          ))}
        </tbody>
      </table>
      <div className="notif-actions">
        <button className="notif-add" onClick={() => setShowAdd(true)}>+ Add Rule</button>
        <button className="notif-save" onClick={save} disabled={busy}>{busy ? "saving…" : "Save"}</button>
        {msg && <span className="notif-msg">{msg}</span>}
      </div>
      {showAdd && (
        <div className="settings-overlay" onClick={e => { if (e.target === e.currentTarget) setShowAdd(false); }}>
          <div className="settings-modal notif-modal">
            <h3>Add Notification Rule</h3>
            <label className="notif-label">Event kind</label>
            <select className="notif-input" value={draft.event_kind} onChange={e => setDraft({ ...draft, event_kind: e.target.value })}>
              {PHASE13B_EVENT_KINDS.map(k => <option key={k} value={k}>{k}</option>)}
            </select>
            <label className="notif-label">Channels (multi-select)</label>
            <div className="notif-chans">
              {PHASE13B_CHANNELS.map(c => (
                <label key={c} className="notif-chan-pill"><input type="checkbox" checked={draft.channels.includes(c)} onChange={() => toggleChan(c)} /> {c}</label>
              ))}
            </div>
            <label className="notif-label">Priority</label>
            <select className="notif-input" value={draft.priority} onChange={e => setDraft({ ...draft, priority: e.target.value })}>
              <option>low</option><option>normal</option><option>high</option><option>urgent</option>
            </select>
            <label className="notif-label">Quiet hours (optional)</label>
            <div className="notif-quiet-row">
              <input className="notif-time" type="time" value={draft.quiet_hours.start} onChange={e => setDraft({ ...draft, quiet_hours: { ...draft.quiet_hours, start: e.target.value } })} />
              <span> → </span>
              <input className="notif-time" type="time" value={draft.quiet_hours.end} onChange={e => setDraft({ ...draft, quiet_hours: { ...draft.quiet_hours, end: e.target.value } })} />
            </div>
            <div className="notif-modal-actions">
              <button onClick={() => setShowAdd(false)}>Cancel</button>
              <button className="notif-add" onClick={addDraft}>Add</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ─── TeamSection ─────────────────────────────────────────────────────────────
function TeamSection() {
  const tenant = _13bTenant();
  const [rows, setRows] = useState([]);
  const [showInvite, setShowInvite] = useState(false);
  const [draft, setDraft] = useState({ email_or_wallet: "", role: "operator" });
  const [lastInvite, setLastInvite] = useState(null);
  const [msg, setMsg] = useState("");

  const refresh = () => {
    fetch(`/api/team.json?tenant=${tenant}`).then(r => r.json())
      .then(d => setRows(d.rows || [])).catch(() => {});
  };
  useEffect(refresh, []); // eslint-disable-line

  const doInvite = () => {
    fetch("/api/team/invite", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ tenant, ...draft }),
    }).then(r => r.json())
      .then(d => {
        if (d.ok) { setLastInvite(d.invite); setMsg(""); setDraft({ email_or_wallet: "", role: "operator" }); refresh(); }
        else setMsg(d.msg || "invite failed");
      });
  };
  const doRevoke = (wallet) => {
    if (!wallet) return;
    if (!window.confirm(`Revoke ${wallet}?`)) return;
    fetch("/api/team/revoke", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ tenant, wallet }),
    }).then(() => refresh());
  };

  return (
    <div className="team-section">
      <h2>👥 Team / Users</h2>
      <p className="settings-sub">Invite by email or wallet. Active principals + pending invites listed together.</p>
      <table className="team-table">
        <thead><tr><th>Wallet</th><th>Email</th><th>Role</th><th>Last active</th><th>Status</th><th></th></tr></thead>
        <tbody>
          {rows.length === 0 && <tr><td colSpan="6" className="team-empty">No team members yet.</td></tr>}
          {rows.map((r, i) => (
            <tr key={i} className={`team-row team-status-${r.status}`}>
              <td className="team-wallet">{r.wallet ? r.wallet.slice(0, 8) + "…" : "—"}</td>
              <td>{r.email || "—"}</td>
              <td><span className="team-role">{r.role}</span></td>
              <td>{r.last_active ? new Date(r.last_active * 1000).toLocaleDateString() : "—"}</td>
              <td>{r.status}{r.invite_code ? ` (${r.invite_code})` : ""}</td>
              <td>{r.wallet && r.status === "active" ? <button className="team-revoke" onClick={() => doRevoke(r.wallet)}>revoke</button> : null}</td>
            </tr>
          ))}
        </tbody>
      </table>
      <div className="team-actions">
        <button className="team-invite-btn" onClick={() => setShowInvite(true)}>+ Invite</button>
      </div>
      {lastInvite && (
        <div className="team-invite-result">
          <strong>Invite minted:</strong> code <code>{lastInvite.code}</code> · share link: <code>{lastInvite.link}</code>
        </div>
      )}
      {showInvite && (
        <div className="settings-overlay" onClick={e => { if (e.target === e.currentTarget) setShowInvite(false); }}>
          <div className="settings-modal team-modal">
            <h3>Invite Team Member</h3>
            <label className="team-label">Email or wallet</label>
            <input className="team-input" placeholder="alice@example.com or 0xabcd…" value={draft.email_or_wallet} onChange={e => setDraft({ ...draft, email_or_wallet: e.target.value })} />
            <label className="team-label">Role</label>
            <select className="team-input" value={draft.role} onChange={e => setDraft({ ...draft, role: e.target.value })}>
              <option value="admin">admin</option>
              <option value="operator">operator</option>
              <option value="customer">customer</option>
            </select>
            {msg && <div className="team-msg">{msg}</div>}
            <div className="team-modal-actions">
              <button onClick={() => setShowInvite(false)}>Cancel</button>
              <button className="team-invite-btn" onClick={doInvite}>Send invite</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ─── HealthSection ───────────────────────────────────────────────────────────
function HealthSection() {
  const [data, setData] = useState({ services: [], updated_at: 0 });
  const [pinging, setPinging] = useState({});

  const refresh = () => fetch("/api/health.json").then(r => r.json()).then(setData).catch(() => {});
  useEffect(refresh, []);

  const pingOne = (svc) => {
    setPinging(p => ({ ...p, [svc]: true }));
    fetch(`/api/health/ping?service=${encodeURIComponent(svc)}`, { method: "POST" })
      .then(r => r.json())
      .then(d => { refresh(); setPinging(p => ({ ...p, [svc]: false })); })
      .catch(() => setPinging(p => ({ ...p, [svc]: false })));
  };

  const ago = (ts) => {
    if (!ts) return "never";
    const s = Math.max(0, Math.floor(Date.now() / 1000) - ts);
    if (s < 60) return `${s}s ago`;
    if (s < 3600) return `${Math.floor(s / 60)}m ago`;
    return `${Math.floor(s / 3600)}h ago`;
  };

  return (
    <div className="health-section">
      <h2>📡 Integrations Health</h2>
      <p className="settings-sub">Background pinger updates every 5 minutes. Click ↻ to ping immediately.</p>
      <div className="health-grid">
        {(data.services || []).length === 0 && <div className="health-empty">No services configured. Set up keys in Accounts.</div>}
        {(data.services || []).map((s, i) => (
          <div key={i} className={`health-card health-status-${s.status}`}>
            <div className="health-card-head">
              <span className={`health-dot health-dot-${s.status}`}>●</span>
              <span className="health-name">{s.service}</span>
              <button className="health-ping" onClick={() => pingOne(s.service)} disabled={pinging[s.service]}>{pinging[s.service] ? "…" : "↻"}</button>
            </div>
            <div className="health-msg">{s.last_msg || "—"}</div>
            <div className="health-meta">
              <span>last ping: {ago(s.last_ping_ts)}</span>
              <span>latency: {s.latency_ms || 0}ms</span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── VoiceSection ────────────────────────────────────────────────────────────
function VoiceSection() {
  const tenant = _13bTenant();
  const [cfg, setCfg] = useState({ provider: "kokoro", voice_id: "default", settings: { stability: 0.5, clarity: 0.75, similarity_boost: 0.75, style: 0.0 } });
  const [providers, setProviders] = useState(["elevenlabs", "openai", "kokoro"]);
  const [msg, setMsg] = useState("");
  const [cloneResult, setCloneResult] = useState(null);
  const fileRef = useRef(null);

  useEffect(() => {
    fetch(`/api/voice/config.json?tenant=${tenant}`).then(r => r.json())
      .then(d => { setCfg({ provider: d.provider, voice_id: d.voice_id, settings: d.settings }); if (d.providers) setProviders(d.providers); })
      .catch(() => {});
  }, []); // eslint-disable-line

  const save = () => {
    setMsg("saving…");
    fetch("/api/voice/save", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ tenant, ...cfg }),
    }).then(r => r.json()).then(d => setMsg(d.ok ? "saved" : "failed")).catch(e => setMsg("error: " + e));
  };

  const upload = () => {
    const f = fileRef.current && fileRef.current.files && fileRef.current.files[0];
    if (!f) return;
    const fd = new FormData();
    fd.append("file", f);
    fd.append("tenant", tenant);
    fetch("/api/voice/clone", { method: "POST", body: fd })
      .then(r => r.json()).then(setCloneResult).catch(e => setCloneResult({ ok: false, error: String(e) }));
  };

  const setStg = (k, v) => setCfg(c => ({ ...c, settings: { ...c.settings, [k]: parseFloat(v) } }));

  return (
    <div className="voice-section">
      <h2>🎤 Voice & TTS</h2>
      <p className="settings-sub">Pick a provider, set voice ID, upload a clone sample.</p>
      <div className="voice-provider-pick">
        {providers.map(p => (
          <label key={p} className={`voice-provider-pill voice-${cfg.provider === p ? "active" : ""}`}>
            <input type="radio" name="voice-provider" value={p} checked={cfg.provider === p} onChange={() => setCfg(c => ({ ...c, provider: p }))} />
            {p}{p === "kokoro" ? " (local)" : ""}
          </label>
        ))}
      </div>
      <label className="voice-label">Voice ID</label>
      <input className="voice-input" placeholder={cfg.provider === "elevenlabs" ? "ElevenLabs voice_id" : "voice id"} value={cfg.voice_id} onChange={e => setCfg(c => ({ ...c, voice_id: e.target.value }))} />
      <div className="voice-sliders">
        {["stability", "clarity", "similarity_boost", "style"].map(k => (
          <div key={k} className="voice-slider-row">
            <label>{k}</label>
            <input type="range" min="0" max="1" step="0.01" value={cfg.settings[k] || 0} onChange={e => setStg(k, e.target.value)} />
            <span className="voice-slider-val">{(cfg.settings[k] || 0).toFixed(2)}</span>
          </div>
        ))}
      </div>
      <div className="voice-actions">
        <button className="voice-save" onClick={save}>Save</button>
        {msg && <span className="voice-msg">{msg}</span>}
      </div>
      <div className="voice-clone">
        <h3>Clone a Voice</h3>
        <p className="settings-sub">Upload an audio sample (mp3/wav). Saved as Artifact (kind=audio).</p>
        <input ref={fileRef} type="file" accept="audio/*" className="voice-file" />
        <button className="voice-clone-btn" onClick={upload}>Upload sample</button>
        {cloneResult && (
          <div className={`voice-clone-result voice-clone-${cloneResult.ok ? "ok" : "err"}`}>
            {cloneResult.ok
              ? <>✓ saved <code>{cloneResult.path}</code> (sha8 <code>{cloneResult.sha8}</code>, artifact <code>{cloneResult.artifact_id || "(none)"}</code>)</>
              : <>✗ {cloneResult.error || cloneResult.msg || "upload failed"}</>}
          </div>
        )}
      </div>
    </div>
  );
}

// ─── WebhooksSection ─────────────────────────────────────────────────────────
function WebhooksSection() {
  const tenant = _13bTenant();
  const [data, setData] = useState({ outbound: [], inbound: [] });
  const [showOutModal, setShowOutModal] = useState(false);
  const [showInModal, setShowInModal] = useState(false);
  const [draftOut, setDraftOut] = useState({ url: "", signing_secret: "", subscribed_events: [], retries: 3 });
  const [draftIn, setDraftIn] = useState({ id: "", signing_required: false });
  const [testResults, setTestResults] = useState({});
  const [msg, setMsg] = useState("");

  const refresh = () => fetch(`/api/webhooks.json?tenant=${tenant}`).then(r => r.json()).then(d => setData({ outbound: d.outbound || [], inbound: d.inbound || [] })).catch(() => {});
  useEffect(refresh, []); // eslint-disable-line

  const save = (next) => {
    fetch("/api/webhooks/save", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ tenant, ...next }),
    }).then(r => r.json()).then(d => { setMsg(d.ok ? "saved" : "failed"); setData({ outbound: d.outbound || [], inbound: d.inbound || [] }); });
  };

  const addOutbound = () => {
    if (!draftOut.url) return;
    const next = { outbound: [...data.outbound, draftOut], inbound: data.inbound };
    save(next);
    setDraftOut({ url: "", signing_secret: "", subscribed_events: [], retries: 3 });
    setShowOutModal(false);
  };
  const delOutbound = (i) => save({ outbound: data.outbound.filter((_, idx) => idx !== i), inbound: data.inbound });
  const testOutbound = (url) => {
    fetch("/api/webhooks/test", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ url }),
    }).then(r => r.json()).then(d => setTestResults(t => ({ ...t, [url]: d })));
  };
  const toggleOutEvent = (e) => setDraftOut(d => ({ ...d, subscribed_events: d.subscribed_events.includes(e) ? d.subscribed_events.filter(x => x !== e) : [...d.subscribed_events, e] }));

  const addInbound = () => {
    if (!draftIn.id) return;
    const next = { outbound: data.outbound, inbound: [...data.inbound, draftIn] };
    save(next);
    setDraftIn({ id: "", signing_required: false });
    setShowInModal(false);
  };
  const delInbound = (i) => save({ outbound: data.outbound, inbound: data.inbound.filter((_, idx) => idx !== i) });

  return (
    <div className="webhook-section">
      <h2>🔌 Webhooks</h2>
      <p className="settings-sub">Outbound = ATLAS fires events to your URLs. Inbound = third parties POST into ATLAS.</p>

      <div className="webhook-block">
        <h3>Outbound</h3>
        <table className="webhook-table">
          <thead><tr><th>URL</th><th>Events</th><th>Retries</th><th>Test</th><th></th></tr></thead>
          <tbody>
            {data.outbound.length === 0 && <tr><td colSpan="5" className="webhook-empty">No outbound webhooks.</td></tr>}
            {data.outbound.map((ob, i) => (
              <tr key={i} className="webhook-row">
                <td className="webhook-url">{ob.url}</td>
                <td>{(ob.subscribed_events || []).join(", ") || "(none)"}</td>
                <td>{ob.retries}</td>
                <td>
                  <button className="webhook-test" onClick={() => testOutbound(ob.url)}>test</button>
                  {testResults[ob.url] && (
                    <span className={`webhook-test-result webhook-test-${testResults[ob.url].ok ? "ok" : "err"}`}>
                      {" "}{testResults[ob.url].code || "ERR"} {testResults[ob.url].msg}
                    </span>
                  )}
                </td>
                <td><button className="webhook-del" onClick={() => delOutbound(i)}>✕</button></td>
              </tr>
            ))}
          </tbody>
        </table>
        <button className="webhook-add" onClick={() => setShowOutModal(true)}>+ Add Outbound</button>
      </div>

      <div className="webhook-block">
        <h3>Inbound</h3>
        <table className="webhook-table">
          <thead><tr><th>ID</th><th>Path</th><th>Signing required</th><th></th></tr></thead>
          <tbody>
            {data.inbound.length === 0 && <tr><td colSpan="4" className="webhook-empty">No inbound receivers.</td></tr>}
            {data.inbound.map((ib, i) => (
              <tr key={i} className="webhook-row">
                <td>{ib.id}</td>
                <td><code>{ib.path}</code></td>
                <td>{ib.signing_required ? "yes" : "no"}</td>
                <td><button className="webhook-del" onClick={() => delInbound(i)}>✕</button></td>
              </tr>
            ))}
          </tbody>
        </table>
        <button className="webhook-add" onClick={() => setShowInModal(true)}>+ Add Inbound</button>
      </div>
      {msg && <div className="webhook-msg">{msg}</div>}

      {showOutModal && (
        <div className="settings-overlay" onClick={e => { if (e.target === e.currentTarget) setShowOutModal(false); }}>
          <div className="settings-modal webhook-modal">
            <h3>Add Outbound Webhook</h3>
            <label className="webhook-label">URL</label>
            <input className="webhook-input" placeholder="https://example.com/hook" value={draftOut.url} onChange={e => setDraftOut({ ...draftOut, url: e.target.value })} />
            <label className="webhook-label">Signing secret (optional)</label>
            <input className="webhook-input" placeholder="shared HMAC secret" value={draftOut.signing_secret} onChange={e => setDraftOut({ ...draftOut, signing_secret: e.target.value })} />
            <label className="webhook-label">Subscribed events</label>
            <div className="webhook-events">
              {PHASE13B_EVENT_KINDS.map(e => (
                <label key={e} className="webhook-event-pill"><input type="checkbox" checked={draftOut.subscribed_events.includes(e)} onChange={() => toggleOutEvent(e)} /> {e}</label>
              ))}
            </div>
            <label className="webhook-label">Retries</label>
            <input className="webhook-input" type="number" min="0" max="10" value={draftOut.retries} onChange={e => setDraftOut({ ...draftOut, retries: parseInt(e.target.value || "0", 10) })} />
            <div className="webhook-modal-actions">
              <button onClick={() => setShowOutModal(false)}>Cancel</button>
              <button className="webhook-add" onClick={addOutbound}>Add</button>
            </div>
          </div>
        </div>
      )}
      {showInModal && (
        <div className="settings-overlay" onClick={e => { if (e.target === e.currentTarget) setShowInModal(false); }}>
          <div className="settings-modal webhook-modal">
            <h3>Add Inbound Receiver</h3>
            <label className="webhook-label">ID (path slug — alphanumeric)</label>
            <input className="webhook-input" placeholder="postmark-doe" value={draftIn.id} onChange={e => setDraftIn({ ...draftIn, id: e.target.value })} />
            <label className="webhook-label"><input type="checkbox" checked={draftIn.signing_required} onChange={e => setDraftIn({ ...draftIn, signing_required: e.target.checked })} /> Require signature header on POSTs</label>
            <div className="webhook-modal-actions">
              <button onClick={() => setShowInModal(false)}>Cancel</button>
              <button className="webhook-add" onClick={addInbound}>Add</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// Register Phase 13b sections into SETTINGS_REGISTRY.
(function attachPhase13bSections() {
  if (typeof window === "undefined" || !window.setSettingsRegistry) return;
  window.setSettingsRegistry("notifications", NotificationsSection);
  window.setSettingsRegistry("team", TeamSection);
  window.setSettingsRegistry("health", HealthSection);
  window.setSettingsRegistry("voice", VoiceSection);
  window.setSettingsRegistry("webhooks", WebhooksSection);
})();


// ─── Phase 13c: PublicEndpointsSection ──────────────────────────────────────
// Lists every externally-addressable URL ATLAS exposes (mcp_tool addons +
// workflow slugs + public bundles). Per-row [copy URL] / [view captures] +
// global "Copy Cursor config" / "Copy Claude Desktop config" buttons that
// paste a ready-to-use JSON snippet for connecting external MCP clients.
function PublicEndpointsSection({ role }) {
  const [endpoints, setEndpoints] = useState([]);
  const [feedback, setFeedback] = useState("");
  const [captures, setCaptures] = useState(null);  // {addon_id, lines:[]}
  const refresh = useCallback(() => {
    fetch("/api/public-endpoints.json").then(r => r.json())
      .then(d => setEndpoints(d.endpoints || [])).catch(() => {});
  }, []);
  useEffect(() => { refresh(); }, [refresh]);
  useEffect(() => {
    const onUpd = () => refresh();
    window.addEventListener("addons-updated", onUpd);
    window.addEventListener("mcp-servers-updated", onUpd);
    return () => {
      window.removeEventListener("addons-updated", onUpd);
      window.removeEventListener("mcp-servers-updated", onUpd);
    };
  }, [refresh]);
  const copyToClipboard = (txt, label) => {
    try {
      navigator.clipboard.writeText(txt);
      setFeedback(`${label} copied`);
      setTimeout(() => setFeedback(""), 1800);
    } catch (e) { setFeedback("copy failed: " + e.message); }
  };
  const copyClientConfig = async (which) => {
    // Aggregate snippet across every mcp_tool endpoint — each addon registered
    // under its own key in `mcpServers`. Matches telegram-mcp's claude_desktop
    // config shape (see /home/tom/Projects/TrollzDotFun/telegram-mcp/).
    const mcpRows = endpoints.filter(e => e.kind === "mcp_tool");
    if (!mcpRows.length) { setFeedback("no mcp_tool addons installed"); return; }
    const merged = { mcpServers: {} };
    for (const row of mcpRows) {
      try {
        const r = await fetch(`/api/public-endpoints/clients?addon_id=${encodeURIComponent(row.id)}`);
        const d = await r.json();
        const conf = which === "cursor" ? d.cursor : d.claude_desktop;
        if (conf && conf.mcpServers) Object.assign(merged.mcpServers, conf.mcpServers);
      } catch (e) {}
    }
    copyToClipboard(JSON.stringify(merged, null, 2), which === "cursor" ? "Cursor config" : "Claude Desktop config");
  };
  const viewCaptures = (addon_id) => {
    // v0 placeholder — surfaces the capture-file path; the actual capture data
    // is populated by Phase 13d ReasoningCaptureSection.
    setCaptures({
      addon_id,
      lines: [
        `# Captures for /mcp/${addon_id}`,
        `# Path: dataset/llm_capture/mcp_${addon_id}-<YYYY-MM-DD>.jsonl`,
        `# Captures populated when Phase 13d ReasoningCaptureSection is enabled.`,
      ],
    });
  };
  return (
    <div>
      <h2>📡 Public Endpoints</h2>
      <div className="settings-sub" style={{ marginBottom: 14 }}>
        Externally-addressable URLs ATLAS exposes ({endpoints.length} total).
        Copy a Cursor / Claude Desktop config to wire every MCP tool into an
        external client in one paste.
      </div>
      <div className="endpoint-clipboard-row">
        <button onClick={() => copyClientConfig("cursor")}>📋 Copy Cursor config</button>
        <button onClick={() => copyClientConfig("claude_desktop")}>📋 Copy Claude Desktop config</button>
        {feedback && <span style={{ color: "var(--accent)", fontSize: 11, alignSelf: "center" }}>{feedback}</span>}
      </div>
      {endpoints.length === 0 ? (
        <div className="settings-placeholder">
          No public endpoints yet. Install an MCP tool addon, publish a
          workflow slug, or share a bundle to populate this list.
        </div>
      ) : (
        <table className="endpoint-table">
          <thead>
            <tr>
              <th>Kind</th><th>URL</th><th>Auth</th><th>Captures/day</th><th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {endpoints.map(e => (
              <tr key={`${e.kind}/${e.id}`}>
                <td><span className="endpoint-row-kind">{e.kind}</span><br/>
                    <span style={{ fontSize: 12 }}>{e.name}</span></td>
                <td>
                  <div className="endpoint-url">
                    <a href={e.url} target="_blank" rel="noopener noreferrer">{e.url}</a>
                  </div>
                  {e.discovery_url && (
                    <div className="endpoint-url" style={{ fontSize: 10.5, color: "var(--muted)" }}>
                      <a href={e.discovery_url} target="_blank" rel="noopener noreferrer">{e.discovery_url}</a>
                    </div>
                  )}
                </td>
                <td>
                  <span className={`endpoint-auth-pill endpoint-auth-${e.auth_required || "none"}`}>
                    {e.auth_required || "none"}
                  </span>
                </td>
                <td className="endpoint-captures">{e.captures_per_day}</td>
                <td>
                  <div className="endpoint-actions">
                    <button onClick={() => copyToClipboard(window.location.origin + e.url, "URL")}>copy</button>
                    {e.kind === "mcp_tool" && (
                      <button onClick={() => viewCaptures(e.id)}>captures</button>
                    )}
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
      {captures && (
        <div className="endpoint-captures-panel">
          {captures.lines.join("\n")}
          <div style={{ marginTop: 8, textAlign: "right" }}>
            <button onClick={() => setCaptures(null)}
                    style={{ background: "transparent", color: "var(--muted)", border: "1px solid var(--line2)", padding: "3px 9px", borderRadius: 4, cursor: "pointer", font: "inherit", fontSize: 11 }}>
              close
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
(function attachPublicEndpoints() {
  const row = SETTINGS_REGISTRY.find(r => r.id === "public_endpoints");
  if (row) row.component = PublicEndpointsSection;
})();

// ─── Phase 13c: MCPServersSection ───────────────────────────────────────────
// Different from "installed addons" (the catalog): this tracks which mcp_tool
// addons are ACTIVELY serving on /mcp/<id>. v0 is a flag-flip — no subprocess
// fork — so "start" enables the route and "pause" makes the discovery doc
// return 503.
function MCPServersSection({ role }) {
  const [servers, setServers] = useState([]);
  const [busy, setBusy] = useState({});
  const [feedback, setFeedback] = useState({});
  const refresh = useCallback(() => {
    fetch("/api/mcp-servers.json").then(r => r.json())
      .then(d => setServers(d.servers || [])).catch(() => {});
  }, []);
  useEffect(() => { refresh(); }, [refresh]);
  useEffect(() => {
    const onUpd = () => refresh();
    window.addEventListener("addons-updated", onUpd);
    window.addEventListener("mcp-servers-updated", onUpd);
    return () => {
      window.removeEventListener("addons-updated", onUpd);
      window.removeEventListener("mcp-servers-updated", onUpd);
    };
  }, [refresh]);
  const control = async (addon_id, action) => {
    setBusy(b => ({ ...b, [addon_id]: action }));
    try {
      const r = await fetch("/api/mcp-servers/control", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ addon_id, action }),
      });
      const d = await r.json();
      setFeedback(f => ({ ...f, [addon_id]: d.ok ? `→ ${d.status}` : `err: ${d.error}` }));
      refresh();
    } finally { setBusy(b => ({ ...b, [addon_id]: null })); }
  };
  return (
    <div>
      <h2>🛠 MCP Servers</h2>
      <div className="settings-sub" style={{ marginBottom: 14 }}>
        Per-addon route state for /mcp/&lt;id&gt;. Pause makes the discovery doc
        return 503 (clients reconnect when unpaused). v0 is in-process; PID
        slot reserved for the Phase 14 subprocess host.
      </div>
      {servers.length === 0 ? (
        <div className="settings-placeholder">
          No MCP tool addons installed. Install one from the 📦 Addons section
          (kind: mcp_tool) and it will appear here.
        </div>
      ) : (
        <table className="mcp-server-table">
          <thead>
            <tr>
              <th>Addon</th><th>Status</th><th>Log tail</th><th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {servers.map(s => (
              <tr key={s.addon_id}>
                <td>
                  <div style={{ fontSize: 13, color: "var(--fg)" }}>{s.name}</div>
                  <div style={{ fontSize: 10.5, color: "var(--muted)", fontFamily: "ui-monospace, monospace" }}>
                    /mcp/{s.addon_id}
                  </div>
                </td>
                <td>
                  <span className={`mcp-server-status mcp-server-status-${s.status}`}>{s.status}</span>
                  {s.pid != null && <div style={{ fontSize: 10, color: "var(--muted)", marginTop: 4 }}>pid:{s.pid}</div>}
                </td>
                <td>
                  <div className="mcp-server-log">{s.last_log_tail || "(no logs)"}</div>
                  {feedback[s.addon_id] && (
                    <div style={{ fontSize: 10, color: "var(--accent)", marginTop: 4 }}>{feedback[s.addon_id]}</div>
                  )}
                </td>
                <td>
                  <div className="mcp-server-actions">
                    <button onClick={() => control(s.addon_id, "start")} disabled={!!busy[s.addon_id] || s.status === "running"}>start</button>
                    <button className="mcp-server-btn-pause" onClick={() => control(s.addon_id, "pause")} disabled={!!busy[s.addon_id] || s.status === "paused"}>pause</button>
                    <button onClick={() => control(s.addon_id, "restart")} disabled={!!busy[s.addon_id]}>restart</button>
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}
(function attachMCPServers() {
  const row = SETTINGS_REGISTRY.find(r => r.id === "mcp_servers");
  if (row) row.component = MCPServersSection;
})();

// ─── Phase 13c: SystemPromptsSection ────────────────────────────────────────
// Editor for Phase 11 `prompt` addons. List view → editor pane. Auto-detected
// {{ variable }} placeholders shown as chips. [Save] reuses /api/addons/install
// via /api/system-prompts/save which writes body to forge/prompts/<id>.txt.
function SystemPromptsSection({ role }) {
  const [prompts, setPrompts] = useState([]);
  const [selectedId, setSelectedId] = useState(null);
  const [body, setBody] = useState("");
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [busy, setBusy] = useState(false);
  const [feedback, setFeedback] = useState(null);   // {ok, msg}
  const [showNew, setShowNew] = useState(false);
  const [newName, setNewName] = useState("");
  const [newBody, setNewBody] = useState("");
  const refresh = useCallback(() => {
    fetch("/api/system-prompts.json").then(r => r.json())
      .then(d => setPrompts(d.prompts || [])).catch(() => {});
  }, []);
  useEffect(() => { refresh(); }, [refresh]);
  useEffect(() => {
    const onUpd = () => refresh();
    window.addEventListener("addons-updated", onUpd);
    return () => window.removeEventListener("addons-updated", onUpd);
  }, [refresh]);
  // Load full body when selection changes.
  useEffect(() => {
    if (!selectedId) { setBody(""); setName(""); setDescription(""); return; }
    fetch(`/api/system-prompt.json?id=${encodeURIComponent(selectedId)}`)
      .then(r => r.json()).then(d => {
        setBody(d.body || "");
        setName(d.name || selectedId);
        setDescription(d.description || "");
      }).catch(() => {});
  }, [selectedId]);
  // Live placeholder detection on body changes — mirrors server-side regex.
  const placeholders = useMemo(() => {
    if (!body) return [];
    const re = /\{\{\s*([a-z_][a-z0-9_]*)\s*\}\}/gi;
    const seen = [];
    let m;
    while ((m = re.exec(body)) !== null) {
      if (!seen.includes(m[1])) seen.push(m[1]);
    }
    return seen;
  }, [body]);
  const save = async () => {
    if (!selectedId) return;
    setBusy(true); setFeedback(null);
    try {
      const r = await fetch("/api/system-prompts/save", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ id: selectedId, name, description, body }),
      });
      const d = await r.json();
      setFeedback({ ok: d.ok, msg: d.ok ? "saved" : (d.error || "save failed") });
      if (d.ok) refresh();
    } finally { setBusy(false); }
    setTimeout(() => setFeedback(null), 2400);
  };
  const createNew = async () => {
    if (!newName.trim()) return;
    setBusy(true);
    try {
      const r = await fetch("/api/system-prompts/save", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name: newName, body: newBody, description: "" }),
      });
      const d = await r.json();
      if (d.ok) {
        setShowNew(false); setNewName(""); setNewBody("");
        refresh();
        setSelectedId(d.id);
      } else {
        setFeedback({ ok: false, msg: d.error || "create failed" });
      }
    } finally { setBusy(false); }
  };
  return (
    <div>
      <h2>📝 System Prompts</h2>
      <div className="settings-sub" style={{ marginBottom: 14 }}>
        Edit Phase 11 prompt addons. {`{{ variable }}`} placeholders are
        detected automatically and rendered as chips.
      </div>
      <div className="sysprompt-layout">
        <div>
          <button className="sysprompt-list-add" onClick={() => setShowNew(true)}>
            + New prompt
          </button>
          <div className="sysprompt-list">
            {prompts.length === 0 ? (
              <div style={{ padding: 16, color: "var(--muted)", fontSize: 12 }}>
                No prompts yet. Click + New prompt to author one.
              </div>
            ) : prompts.map(p => (
              <div key={p.id}
                   className={`sysprompt-list-item ${selectedId === p.id ? "active" : ""}`}
                   onClick={() => setSelectedId(p.id)}>
                <div className="sysprompt-list-item-name">{p.name}</div>
                <div className="sysprompt-list-item-preview">{p.body_preview || "(empty)"}</div>
                <div className="sysprompt-list-item-meta">
                  <span>used by {p.used_by_count}</span>
                  <span>{(p.placeholders || []).length} vars</span>
                  <span>{p.body_len}b</span>
                </div>
              </div>
            ))}
          </div>
        </div>
        <div className="sysprompt-editor">
          {!selectedId ? (
            <div className="sysprompt-empty">
              Select a prompt on the left, or create a new one.
            </div>
          ) : (
            <>
              <div>
                <div className="sysprompt-label">Name</div>
                <input value={name} onChange={e => setName(e.target.value)} />
              </div>
              <div>
                <div className="sysprompt-label">Description</div>
                <input value={description} onChange={e => setDescription(e.target.value)} />
              </div>
              <div>
                <div className="sysprompt-label">Body</div>
                <textarea value={body} onChange={e => setBody(e.target.value)} />
              </div>
              <div>
                <div className="sysprompt-label">Placeholders ({placeholders.length})</div>
                {placeholders.length === 0 ? (
                  <div style={{ fontSize: 11, color: "var(--muted)" }}>
                    None detected. Insert {`{{ variable_name }}`} into the body to add one.
                  </div>
                ) : (
                  <div className="sysprompt-chips">
                    {placeholders.map(v => (
                      <span key={v} className="sysprompt-chip">{`{{ ${v} }}`}</span>
                    ))}
                  </div>
                )}
              </div>
              <div className="sysprompt-actions">
                <button onClick={save} disabled={busy}>{busy ? "saving…" : "💾 Save"}</button>
                <button className="sysprompt-btn-secondary" onClick={() => setSelectedId(null)} disabled={busy}>
                  close
                </button>
                {feedback && (
                  <span className={`sysprompt-feedback ${feedback.ok ? "ok" : "err"}`}>{feedback.msg}</span>
                )}
              </div>
            </>
          )}
        </div>
      </div>
      {showNew && (
        <div className="sysprompt-modal-back" onClick={e => { if (e.target === e.currentTarget) setShowNew(false); }}>
          <div className="sysprompt-modal">
            <div style={{ fontSize: 16, color: "var(--fg)" }}>New System Prompt</div>
            <div>
              <div className="sysprompt-label">Name</div>
              <input value={newName} onChange={e => setNewName(e.target.value)} autoFocus />
            </div>
            <div>
              <div className="sysprompt-label">Body</div>
              <textarea value={newBody} onChange={e => setNewBody(e.target.value)}
                        placeholder="You are…  use {{ variable }} for inputs."
                        style={{ minHeight: 140 }} />
            </div>
            <div className="sysprompt-actions">
              <button onClick={createNew} disabled={busy || !newName.trim()}>
                {busy ? "creating…" : "Create"}
              </button>
              <button className="sysprompt-btn-secondary" onClick={() => setShowNew(false)} disabled={busy}>cancel</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
(function attachSystemPrompts() {
  const row = SETTINGS_REGISTRY.find(r => r.id === "system_prompts");
  if (row) row.component = SystemPromptsSection;
})();

// ─── Phase 13.5: SettingsSectionWrapper ────────────────────────────────────
// Wraps the active section's component to inject:
//   1. The `shared_default` radio at the top (sections with scope:
//      "shared_default" — AI Connectors, Voice & TTS, Addons). Reads + writes
//      `use_platform_default` flag against /api/slug/<tenant>/<section>.json
//      via the same pattern as the per-tenant config endpoints. v0: stores
//      the flag in localStorage keyed by tenant+section to avoid blocking on
//      a per-section storage migration; downstream consumers can read it from
//      the same key. Default = true for new tenants.
//   2. The platform-rollup notice for sections flagged `platform_rollup: true`
//      (Health, Usage & Tokens) when viewingMode === "platform". v0: render
//      a small banner with tenant count + "Coming soon" placeholder, then the
//      single-tenant view below it (so we don't regress existing UX).
function SettingsSectionWrapper({ section, ActiveComponent, role, me,
                                  viewingMode, viewingTenant, isPlatformAdmin }) {
  const isShared = section.scope === "shared_default";
  const isRollup = section.platform_rollup === true && viewingMode === "platform";
  const tenantForFlag = viewingTenant || (me && me.tenant) || "main";
  const flagKey = `atlas_use_platform_default::${tenantForFlag}::${section.id}`;
  // Default true for new tenants (free shared path).
  const [usePlatformDefault, setUsePlatformDefault] = useState(() => {
    try {
      const v = localStorage.getItem(flagKey);
      if (v === null) return true;
      return v === "1";
    } catch (e) { return true; }
  });
  useEffect(() => {
    try {
      const v = localStorage.getItem(flagKey);
      setUsePlatformDefault(v === null ? true : v === "1");
    } catch (e) {}
  }, [flagKey]);
  const setFlag = (val) => {
    setUsePlatformDefault(val);
    try { localStorage.setItem(flagKey, val ? "1" : "0"); } catch (e) {}
  };
  // Rollup tenant count for the banner.
  const [tenantCount, setTenantCount] = useState(null);
  useEffect(() => {
    if (!isRollup) return;
    fetch("/api/tenants.json")
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d && typeof d.count === "number") setTenantCount(d.count); })
      .catch(() => {});
  }, [isRollup]);

  const inner = ActiveComponent
    ? <ActiveComponent role={role} section={section} me={me}
                       viewingMode={viewingMode} viewingTenant={viewingTenant}
                       isPlatformAdmin={isPlatformAdmin}
                       usePlatformDefault={isShared ? usePlatformDefault : false} />
    : <SettingsPlaceholder section={section}
                            viewingMode={viewingMode}
                            viewingTenant={viewingTenant}
                            isPlatformAdmin={isPlatformAdmin} />;

  return (
    <div>
      {/* Shared-default radio — only for shared_default sections when in tenant view. */}
      {isShared && viewingMode !== "platform" && (
        <div className="shared-default-gate"
             style={{ padding: "10px 14px", margin: "0 0 12px 0",
                      background: "var(--bg-soft)",
                      border: "1px solid var(--border)", borderRadius: 6,
                      fontSize: 12 }}
             data-tour-id={`shared-default-${section.id}`}>
          <div style={{ fontWeight: 600, marginBottom: 6, color: "var(--fg)" }}>
            {section.icon} {section.label} — choose source
          </div>
          <label style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 4, cursor: "pointer" }}>
            <input type="radio" name={`sd-${section.id}`}
                   checked={usePlatformDefault}
                   onChange={() => setFlag(true)} />
            <span>Use platform default <span style={{ color: "var(--fg2)" }}>(free, shared)</span></span>
          </label>
          <label style={{ display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }}>
            <input type="radio" name={`sd-${section.id}`}
                   checked={!usePlatformDefault}
                   onChange={() => setFlag(false)} />
            <span>Use my own key <span style={{ color: "var(--fg2)" }}>(BYOK; you pay)</span></span>
          </label>
          {usePlatformDefault && (
            <div style={{ marginTop: 6, padding: "6px 8px", background: "var(--bg)",
                          border: "1px dashed var(--border)", borderRadius: 4,
                          fontSize: 11, color: "var(--fg2)" }}>
              Platform default in use. The BYOK fields below are hidden.
              Platform admin configured: {section.id === "ai_connectors" ? "shared inference pool"
                : section.id === "voice" ? "shared TTS provider"
                : section.id === "addons" ? "curated marketplace addons"
                : "configured by platform admin"}.
            </div>
          )}
        </div>
      )}
      {/* Platform-rollup banner — shown above existing single-tenant view. */}
      {isRollup && (
        <div className="platform-rollup-banner"
             style={{ padding: "10px 14px", margin: "0 0 12px 0",
                      background: "var(--bg-soft)",
                      border: "1px solid var(--accent)", borderRadius: 6,
                      fontSize: 12 }}>
          <div style={{ fontWeight: 600, color: "var(--accent)", marginBottom: 4 }}>
            🌐 Platform rollup — {section.label}
          </div>
          <div style={{ color: "var(--fg2)" }}>
            Aggregating across <strong>{tenantCount == null ? "…" : tenantCount}</strong> tenant{tenantCount === 1 ? "" : "s"}.
            Per-tenant breakdown coming soon. Current view shows single-tenant data below.
          </div>
        </div>
      )}
      {/* Hide BYOK fields when platform default is in use — gated by section component.
          If the component doesn't honor the prop yet, we still render it (no regression). */}
      {isShared && usePlatformDefault && viewingMode !== "platform"
        ? <div style={{ opacity: 0.4, pointerEvents: "none" }}>{inner}</div>
        : inner}
    </div>
  );
}

// ─── Phase 13a: SettingsPanel — nav-rail + pane layout ──────────────────────
// Left rail (260px): 4 collapsible groups → 20+ nav items
// Right pane (flex 1, scrollable): the active section's component
// Backwards-compat: every existing acct-card lives under "Accounts & Auth".
// Phase 13.5: when `me.is_platform_admin === true`, render a viewingMode
// header toggle (Platform / Tenant:<slug>) and filter the nav by each entry's
// `scope` field. Non-platform-admins are pinned to their own tenant view.
function SettingsPanel({ onClose, role, me }) {
  const isPlatformAdmin = !!(me && me.is_platform_admin);
  const myTenant = (me && me.tenant) || "main";
  const [activeSection, setActiveSection] = useState("accounts");
  const [collapsed, setCollapsed] = useState({});   // group -> bool (default expanded)
  // viewingMode: "platform" | "tenant:<slug>" — controls scope filter.
  // Default: platform if platform_admin, else tenant:<current>.
  const [viewingMode, setViewingMode] = useState(
    isPlatformAdmin ? "platform" : `tenant:${myTenant}`
  );
  // Tenants list for the toggle dropdown (platform_admin only). Pulled from
  // /api/tenants.json — populated by enumerating slugs/*.json server-side.
  const [tenants, setTenants] = useState([]);
  useEffect(() => {
    if (!isPlatformAdmin) return;
    fetch("/api/tenants.json")
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d && Array.isArray(d.tenants)) setTenants(d.tenants); })
      .catch(() => {});
  }, [isPlatformAdmin]);

  // Phase 13.5 scope filter: viewingMode determines which entries are visible.
  //   "platform"        → scope ∈ {platform, both, shared_default}
  //   "tenant:<slug>"   → scope ∈ {tenant, shared_default, both}
  // Then role filter is applied on top (operator/customer still gated by roles[]).
  const inViewingScope = (r) => {
    const sc = r.scope || "tenant";
    if (viewingMode === "platform") {
      return sc === "platform" || sc === "both" || sc === "shared_default";
    }
    return sc === "tenant" || sc === "shared_default" || sc === "both";
  };

  const visible = SETTINGS_REGISTRY.filter(r => {
    if (!inViewingScope(r)) return false;
    if (!r.roles) return true;
    if (!role) return true;        // null role (admin-mode) sees everything
    return r.roles.includes(role);
  });
  const grouped = SETTINGS_GROUP_ORDER.map(g => ({
    group: g,
    label: SETTINGS_GROUP_LABELS[g] || g,
    items: visible.filter(r => r.group === g),
  })).filter(g => g.items.length > 0);
  const active = visible.find(r => r.id === activeSection)
              || visible[0]
              || null;
  // If the activeSection was filtered out (role OR viewingMode), fall back to
  // the first visible row so the pane always has something to render.
  useEffect(() => {
    if (!visible.find(r => r.id === activeSection) && visible[0]) {
      setActiveSection(visible[0].id);
    }
  }, [role, viewingMode]);  // eslint-disable-line react-hooks/exhaustive-deps
  const toggleGroup = (g) => setCollapsed(c => ({ ...c, [g]: !c[g] }));
  const ActiveComponent = active && active.component;
  // Tenant slug currently being viewed (for shared_default + rollup contexts).
  const viewingTenant = viewingMode.startsWith("tenant:")
    ? viewingMode.slice(7)
    : null;
  return (
    <div className="settings-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="settings-modal settings-modal-wide">
        <div className="settings-head">
          <div>
            <div className="settings-title">Settings</div>
            <div className="settings-sub">Connect accounts, run pipelines, capture reasoning.</div>
          </div>
          <button className="settings-close" onClick={onClose}>×</button>
        </div>
        {/* Phase 13.5: platform-mode toggle — only platform_admin sees this. */}
        {isPlatformAdmin && (
          <div className="settings-viewing-toggle"
               style={{ padding: "8px 14px", borderBottom: "1px solid var(--border)",
                        background: "var(--bg-soft)", fontSize: 12,
                        display: "flex", alignItems: "center", gap: 8 }}
               data-tour-id="settings-viewing-mode">
            <span style={{ color: "var(--fg2)" }}>👁 viewing:</span>
            <select value={viewingMode}
                    onChange={e => setViewingMode(e.target.value)}
                    style={{ padding: "3px 6px", background: "var(--bg)",
                             color: "var(--fg)", border: "1px solid var(--border)",
                             borderRadius: 4, fontSize: 12, fontWeight: 600 }}>
              <option value="platform">Platform</option>
              {tenants.map(t => (
                <option key={t.slug} value={`tenant:${t.slug}`}>
                  Tenant: {t.slug}
                </option>
              ))}
            </select>
            <span style={{ color: "var(--fg3)", marginLeft: 8 }}>
              {viewingMode === "platform"
                ? "Platform-scoped + shared-default sections"
                : "Tenant-scoped + shared-default sections"}
            </span>
          </div>
        )}
        <div className="settings-layout">
          <nav className="settings-nav">
            {grouped.map(g => (
              <div key={g.group} className="settings-group">
                <div className="settings-group-header"
                     onClick={() => toggleGroup(g.group)}
                     style={{ cursor: "pointer" }}
                     data-tour-id={`settings-group-${g.group.toLowerCase()}`}>
                  {g.label}
                </div>
                {!collapsed[g.group] && g.items.map(item => (
                  <div key={item.id}
                       className={`settings-nav-item ${activeSection === item.id ? "active" : ""}`}
                       onClick={() => setActiveSection(item.id)}
                       data-tour-id={`settings-nav-${item.id}`}>
                    <span className="icon">{item.icon}</span>
                    <span>{item.label}</span>
                    {item.scope === "platform" && (
                      <span style={{ marginLeft: "auto", fontSize: 9, color: "var(--accent)",
                                     border: "1px solid var(--accent)", borderRadius: 3,
                                     padding: "0 4px" }} title="Platform-only setting">PLT</span>
                    )}
                    {item.scope === "shared_default" && (
                      <span style={{ marginLeft: "auto", fontSize: 9, color: "var(--fg2)",
                                     border: "1px solid var(--border)", borderRadius: 3,
                                     padding: "0 4px" }} title="Shared platform default (opt-in)">SHD</span>
                    )}
                  </div>
                ))}
              </div>
            ))}
          </nav>
          <div className="settings-pane" key={active ? active.id : "empty"}>
            {active
              ? <SettingsSectionWrapper
                  section={active}
                  ActiveComponent={ActiveComponent}
                  role={role}
                  me={me}
                  viewingMode={viewingMode}
                  viewingTenant={viewingTenant}
                  isPlatformAdmin={isPlatformAdmin}
                />
              : <div className="settings-placeholder">No sections visible for this role.</div>}
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Phase 11.5: TourProvider + TourTooltip — LLM-guided UI walkthroughs ────
// The provider holds {active_tour, current_step_idx, advance, skip, done}.
// start_tour(script) glows the target via [data-tour-id], renders TourTooltip
// next to it, listens for `wait_for` event, advances. ms:NNNN timer auto-fires.
const TourContext = React.createContext(null);

function TourProvider({ children }) {
  const [activeTour, setActiveTour] = useState(null);   // {goal, steps, fallback_text}
  const [stepIdx, setStepIdx] = useState(0);
  const [targetRect, setTargetRect] = useState(null);
  const targetElRef = useRef(null);

  const cleanup = useCallback(() => {
    if (targetElRef.current) {
      try { targetElRef.current.classList.remove("tour-glow"); } catch (e) {}
    }
    targetElRef.current = null;
  }, []);

  const done = useCallback(() => {
    cleanup();
    setActiveTour(null);
    setStepIdx(0);
    setTargetRect(null);
  }, [cleanup]);

  const advance = useCallback(() => {
    setStepIdx(i => i + 1);
  }, []);

  const skip = useCallback(() => { done(); }, [done]);

  const start_tour = useCallback((script) => {
    if (!script || !Array.isArray(script.steps) || script.steps.length === 0) {
      // No steps → surface the fallback text in an alert-style modal via the tour state.
      if (script && script.fallback_text) {
        setActiveTour({ goal: script.goal || "", steps: [], fallback_text: script.fallback_text });
        setStepIdx(0);
      }
      return;
    }
    cleanup();
    setActiveTour({ goal: script.goal || "", steps: script.steps, fallback_text: script.fallback_text || "" });
    setStepIdx(0);
  }, [cleanup]);

  // Bind glow + tooltip + wait_for listener to the current step's element.
  useEffect(() => {
    if (!activeTour || !activeTour.steps || stepIdx >= activeTour.steps.length) {
      if (activeTour && activeTour.steps && stepIdx >= activeTour.steps.length) {
        // tour finished
        const t = setTimeout(done, 50);
        return () => clearTimeout(t);
      }
      return;
    }
    const step = activeTour.steps[stepIdx];
    const tid = step.target_tour_id;
    const el = document.querySelector(`[data-tour-id="${tid}"]`);
    cleanup();
    if (!el) {
      // Target not in DOM right now — skip after a short delay so the user sees the
      // tooltip's "missing element" message via TourTooltip's null-target render.
      targetElRef.current = null;
      setTargetRect(null);
      return;
    }
    el.classList.add("tour-glow");
    targetElRef.current = el;
    const rect = el.getBoundingClientRect();
    setTargetRect({ top: rect.top, left: rect.left, width: rect.width,
                    height: rect.height, bottom: rect.bottom, right: rect.right });
    // refresh tooltip position on scroll/resize
    const refresh = () => {
      if (!targetElRef.current) return;
      const r = targetElRef.current.getBoundingClientRect();
      setTargetRect({ top: r.top, left: r.left, width: r.width, height: r.height,
                      bottom: r.bottom, right: r.right });
    };
    window.addEventListener("scroll", refresh, true);
    window.addEventListener("resize", refresh);

    // Set up wait_for listener
    const wf = (step.wait_for || "click").toLowerCase();
    let cleanupFn = null;
    if (wf === "click") {
      const onClick = () => { advance(); };
      el.addEventListener("click", onClick, { once: true });
      cleanupFn = () => el.removeEventListener("click", onClick);
    } else if (wf === "hover") {
      const onHover = () => { advance(); };
      el.addEventListener("mouseenter", onHover, { once: true });
      cleanupFn = () => el.removeEventListener("mouseenter", onHover);
    } else if (wf === "input") {
      const onInput = () => { advance(); };
      el.addEventListener("input", onInput, { once: true });
      cleanupFn = () => el.removeEventListener("input", onInput);
    } else if (wf.startsWith("ms:")) {
      const ms = parseInt(wf.slice(3), 10) || 1500;
      const t = setTimeout(advance, ms);
      cleanupFn = () => clearTimeout(t);
    }
    return () => {
      window.removeEventListener("scroll", refresh, true);
      window.removeEventListener("resize", refresh);
      if (cleanupFn) cleanupFn();
    };
  }, [activeTour, stepIdx, advance, cleanup, done]);

  const currentStep = activeTour && activeTour.steps && stepIdx < activeTour.steps.length
    ? activeTour.steps[stepIdx] : null;
  const isLast = activeTour && activeTour.steps && stepIdx === activeTour.steps.length - 1;

  const value = { activeTour, stepIdx, advance, skip, done, start_tour };
  return (
    <TourContext.Provider value={value}>
      {children}
      {activeTour && (activeTour.steps.length === 0 || !currentStep) && activeTour.fallback_text && (
        <div className="tour-tooltip tour-fallback" style={{ top: 80, left: "50%", transform: "translateX(-50%)" }}>
          <div className="tour-tooltip-goal">{activeTour.goal || "Nixzlab Tour"}</div>
          <div className="tour-tooltip-body">{activeTour.fallback_text}</div>
          <div className="actions">
            <button className="tour-btn-text" onClick={done}>[done]</button>
          </div>
        </div>
      )}
      {currentStep && (
        <TourTooltip rect={targetRect} step={currentStep} stepIdx={stepIdx}
                     totalSteps={activeTour.steps.length} goal={activeTour.goal}
                     isLast={isLast} onNext={advance} onSkip={skip} onDone={done} />
      )}
    </TourContext.Provider>
  );
}

function TourTooltip({ rect, step, stepIdx, totalSteps, goal, isLast, onNext, onSkip, onDone }) {
  // Position: prefer below the target, fallback to above, then right.
  let top = 80, left = 24;
  if (rect) {
    const tooltipH = 140; // approximate
    const tooltipW = 280;
    const vh = window.innerHeight; const vw = window.innerWidth;
    if (rect.bottom + tooltipH + 16 < vh) {
      top = rect.bottom + 10; left = Math.max(8, Math.min(rect.left, vw - tooltipW - 8));
    } else if (rect.top - tooltipH - 16 > 0) {
      top = rect.top - tooltipH - 10; left = Math.max(8, Math.min(rect.left, vw - tooltipW - 8));
    } else {
      top = Math.max(8, rect.top); left = Math.min(vw - tooltipW - 8, rect.right + 10);
    }
  }
  return (
    <div className="tour-tooltip" style={{ top, left }}>
      <div className="tour-tooltip-goal">{goal || "Nixzlab Tour"}</div>
      <div className="tour-tooltip-step">Step {stepIdx + 1} / {totalSteps}</div>
      <div className="tour-tooltip-body">{step.instruction}</div>
      <div className="actions">
        {!isLast && <button className="tour-btn-text" onClick={onNext}>[next]</button>}
        <button className="tour-btn-text" onClick={onSkip}>[skip]</button>
        {isLast && <button className="tour-btn-text tour-btn-done" onClick={onDone}>[done]</button>}
      </div>
    </div>
  );
}

// Small modal for collecting the user's intent → POST /api/tour/start.
function TourPromptModal({ onClose, onScript, activeChat, activeFolder }) {
  const [intent, setIntent] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const submit = async () => {
    const m = intent.trim();
    if (!m) return;
    setBusy(true); setErr(null);
    try {
      const wallet = localStorage.getItem("atlas_wallet") || "";
      const r = await apiFetch("/api/tour/start", {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ intent: m, active_chat: activeChat || null,
                               active_folder: activeFolder || null, wallet }),
      });
      const d = await r.json().catch(() => ({}));
      if (!r.ok || !d) { setErr(d && d.error ? d.error : "tour generator failed"); return; }
      onScript(d);
      onClose();
    } catch (e) { setErr(String(e)); }
    finally { setBusy(false); }
  };
  return (
    <div className="settings-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="settings-modal" style={{ maxWidth: 480 }}>
        <div className="settings-head">
          <div>
            <div className="settings-title">💡 Show me how</div>
            <div className="settings-sub">Describe what you want to do — Nixzlab will glow the buttons in sequence.</div>
          </div>
          <button className="settings-close" onClick={onClose}>×</button>
        </div>
        <div style={{ padding: "12px 18px 16px" }}>
          <textarea
            autoFocus
            value={intent}
            onChange={e => setIntent(e.target.value)}
            onKeyDown={e => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) submit(); }}
            placeholder="e.g. how do I add my Telegram bot token"
            rows={3}
            style={{ width: "100%", background: "var(--bg)", border: "1px solid var(--line)",
                     color: "var(--fg)", padding: 10, borderRadius: 6, font: "inherit", fontSize: 13 }}
          />
          {err && <div style={{ color: "#ff7e7e", fontSize: 12, padding: "6px 0" }}>{err}</div>}
          <div style={{ display: "flex", gap: 6, marginTop: 8 }}>
            <button className="send-btn" onClick={submit} disabled={busy || !intent.trim()}
                    style={{ padding: "8px 14px", borderRadius: 8 }}>
              {busy ? "thinking…" : "start tour"}
            </button>
            <button onClick={onClose} style={{ padding: "8px 14px", borderRadius: 8,
                    background: "transparent", border: "1px solid var(--line)", color: "var(--fg2)" }}>cancel</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// TourBootstrap: lives inside <TourProvider>, so it can grab the tour context
// and call start_tour() whenever the App passes in a non-null pendingScript.
function TourBootstrap({ pendingScript, onConsumed }) {
  const tour = useContext(TourContext);
  useEffect(() => {
    if (pendingScript && tour && tour.start_tour) {
      tour.start_tour(pendingScript);
      onConsumed && onConsumed();
    }
  }, [pendingScript, tour, onConsumed]);
  return null;
}

function App() {
  // ─── Auth gate (Phase 3) ─── if no session token, render LoginScreen.
  const auth = useAuth();
  if (!auth.token) return <LoginScreen onLogin={auth.login} />;

  const tenant = useTenant();
  const tenantBrand = tenant.branding || {};
  const tenantCopy = tenantBrand.custom_copy || {};
  const allowedWorkflows = tenant.allowed_workflows || [];
  const hiddenChrome = tenant.hidden_chrome || [];

  // Phase 4: role-aware chrome overrides on top of per-tenant hidden_chrome.
  //   customer  → force-hide settings, picker, freeform-input
  //   operator  → force-hide picker, freeform-input (settings filtered inside)
  //   admin / null / undefined → no extra hides (current behavior)
  const role = auth.me && auth.me.role;
  const roleHides = role === "customer"
    ? ["settings", "picker", "freeform-input"]
    : role === "operator"
      ? ["picker", "freeform-input"]
      : [];
  const effectiveHidden = new Set([...hiddenChrome, ...roleHides]);
  const hideSettings = effectiveHidden.has("settings");
  const hidePicker = effectiveHidden.has("picker");
  const hideFreeform = effectiveHidden.has("freeform-input");

  // Filter CATEGORIES so only subtabs in allowed_workflows survive;
  // drop categories whose subtabs are entirely empty.
  // Phase 12a: keep `special` categories (e.g. library) even if subtabs is empty.
  const categories = CATEGORIES
    .map(c => ({ ...c, subtabs: c.subtabs.filter(s => allowedWorkflows.includes(s)) }))
    .filter(c => c.subtabs.length > 0 || c.special);

  const [workflows, setWorkflows] = useState({});
  // Phase 10: lineage drawer + slide-direction state
  const [lineageOpen, setLineageOpen] = useState(null); // message idx or null
  const [slideDir, setSlideDir] = useState(null);       // 'in' triggers translateX animation
  const [bcPulse, setBcPulse] = useState(false);        // breadcrumb pulse on new message
  const lastMsgLenRef = useRef(0);
  const [chats, setChats] = useState([]);
  const [folders, setFolders] = useState([]);
  const [activeChat, setActiveChat] = useState(null);
  const [messages, setMessages] = useState([]);
  const [category, setCategory] = useState(categories[0]?.id || "all");
  const [subtab, setSubtab] = useState(null);  // null = show all in category
  const [composer, setComposer] = useState("");
  const [sending, setSending] = useState(false);
  const [showSettings, setShowSettings] = useState(false);
  const [showNewFolder, setShowNewFolder] = useState(false);
  const [showSaveWorkflow, setShowSaveWorkflow] = useState(false);
  // Phase 11: card-template authoring modal
  const [showCardBuilder, setShowCardBuilder] = useState(false);
  // Phase 11.5: tour-prompt modal (💡) state — opens collector that POSTs /api/tour/start
  const [showTourPrompt, setShowTourPrompt] = useState(false);
  const [pendingTourScript, setPendingTourScript] = useState(null);
  // Phase 9: mount-drive modal target folder (null = closed)
  const [mountFolder, setMountFolder] = useState(null);
  const [folderExpanded, setFolderExpanded] = useState(() => {
    try { return JSON.parse(localStorage.getItem("atlas_folder_expanded") || "{}"); }
    catch (e) { return {}; }
  });
  const [moveMenuChat, setMoveMenuChat] = useState(null);  // chat name whose ▸ submenu is open
  const fileInputRef = useRef(null);
  const msgEnd = useRef(null);
  const toggleFolder = useCallback((fid) => {
    setFolderExpanded(prev => {
      const next = { ...prev, [fid]: !prev[fid] };
      try { localStorage.setItem("atlas_folder_expanded", JSON.stringify(next)); } catch (e) {}
      return next;
    });
  }, []);

  // If filtered categories shift (tenant loaded async), retarget category if current is gone.
  useEffect(() => {
    if (categories.length && !categories.find(c => c.id === category)) {
      setCategory(categories[0].id);
      setSubtab(null);
    }
  }, [tenant]);

  // initial load
  useEffect(() => {
    fetchJson("/api/workflows.json").then(d => { setWorkflows(d); if (typeof window !== "undefined") window.__ATLAS_WORKFLOWS = d; });
    refreshChats();
    const poll = setInterval(() => { if (activeChat) refreshMessages(activeChat); }, 4000);
    return () => clearInterval(poll);
  }, [activeChat]);

  // Phase 10: when activeChat changes, kick the slide-in animation.
  // CSS .messages-container.slide-in animates translateX(100% → 0).
  useEffect(() => {
    if (!activeChat) { setSlideDir(null); return; }
    setSlideDir("in");
    const t = setTimeout(() => setSlideDir(null), 320);
    return () => clearTimeout(t);
  }, [activeChat]);

  useEffect(() => { msgEnd.current && msgEnd.current.scrollIntoView({ behavior: "smooth" }); }, [messages]);

  // Phase 10: new-message landing → pulse the breadcrumb (450ms).
  useEffect(() => {
    if (messages.length > lastMsgLenRef.current && lastMsgLenRef.current > 0) {
      setBcPulse(true);
      const t = setTimeout(() => setBcPulse(false), 450);
      lastMsgLenRef.current = messages.length;
      return () => clearTimeout(t);
    }
    lastMsgLenRef.current = messages.length;
  }, [messages.length]);

  const refreshChats = useCallback(() => {
    apiFetch("/api/chats.json").then(r => r.json()).then(setChats);
    apiFetch("/api/folders.json").then(r => r.json()).then(d => setFolders(d.folders || []))
      .catch(() => {});
  }, []);

  const assignFolder = useCallback(async (chatName, folderId) => {
    const q = new URLSearchParams({ c: chatName, folder: folderId || "null" });
    await apiFetch("/api/folders/assign?" + q.toString(), { method: "POST" });
    setMoveMenuChat(null);
    refreshChats();
  }, [refreshChats]);

  const toggleTodo = useCallback(async (chatName, on) => {
    const q = new URLSearchParams({ c: chatName, on: on ? "1" : "0" });
    await apiFetch("/api/todo?" + q.toString(), { method: "POST" });
    refreshChats();
  }, [refreshChats]);

  // Phase 9: kick a background re-index of a folder's vector store.
  const reindexFolder = useCallback(async (folderId) => {
    if (!folderId) return;
    try {
      await apiFetch("/api/vector/index?folder=" + encodeURIComponent(folderId),
                     { method: "POST" });
      // poll stats a couple of times so the chunks count surfaces in the header
      setTimeout(refreshChats, 1500);
      setTimeout(refreshChats, 4000);
    } catch (e) {}
  }, [refreshChats]);

  // 📎 attachment upload — either satisfies the workflow upload step (if the
  // last message contains an upload:chat= token for this chat), or attaches the
  // file as a generic chat artifact via /api/upload-flow.
  const onAttachFile = useCallback(async (file) => {
    if (!activeChat || !file) return;
    const fd = new FormData();
    fd.append("c", activeChat);
    fd.append("file", file);
    setSending(true);
    try {
      await apiFetch("/api/upload-flow", { method: "POST", body: fd });
      refreshMessages(activeChat);
      refreshChats();
    } finally { setSending(false); }
  }, [activeChat, refreshChats]);

  const refreshMessages = useCallback((chat) => {
    if (!chat) return;
    apiFetch(`/api/chat.json?c=${encodeURIComponent(chat)}`).then(r => r.json()).then(setMessages);
  }, []);

  const selectChat = (chat) => {
    setActiveChat(chat);
    refreshMessages(chat);
  };

  const newChat = (flowId) => {
    const fd = new FormData(); fd.append("flow", flowId);
    apiFetch("/api/newchat", { method: "POST", body: new URLSearchParams(fd) })
      .then(() => apiFetch("/api/chats.json").then(r => r.json()))
      .then(list => {
        setChats(list);
        // newly minted chat will be the most recent of this flow
        const newest = list.find(c => c.flow === flowId);
        if (newest) selectChat(newest.name);
      });
  };

  const send = () => {
    const m = composer.trim();
    if (!m || !activeChat) return;
    setSending(true);
    const body = new URLSearchParams({ c: activeChat, m });
    setComposer("");
    apiFetch("/api/chat", { method: "POST", body })
      .then(() => { refreshMessages(activeChat); refreshChats(); })
      .finally(() => setSending(false));
  };

  // ─── filter chats for sidebar based on category + subtab
  const cat = categories.find(c => c.id === category) || categories[0] || { id:"all", label:"All", subtabs: [] };
  const allowedFlows = subtab ? [subtab] : cat.subtabs;
  const visibleChats = chats.filter(c => !c.flow || allowedFlows.includes(c.flow));

  // Phase 11.5: handler invoked from SystemCard's [💡 Show me how] button
  // (proactive trigger). info = {intent, service, action}. Composes the POST,
  // hands the script to TourProvider via window.__atlas_start_tour.
  const startTourFromHint = useCallback(async (info) => {
    const intent = (info && info.intent) || `set up ${info && info.service || ""}`.trim();
    if (!intent) return;
    try {
      const wallet = localStorage.getItem("atlas_wallet") || "";
      const r = await apiFetch("/api/tour/start", {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ intent, active_chat: activeChat || null, wallet }),
      });
      const d = await r.json().catch(() => ({}));
      if (r.ok && d) setPendingTourScript(d);
    } catch (e) {}
  }, [activeChat]);

  return (
    <TourProvider>
    <TourBootstrap pendingScript={pendingTourScript}
                   onConsumed={() => setPendingTourScript(null)} />
    <div className="app">
      {/* SIDEBAR */}
      <aside className="sidebar">
        <div className="sidebar-brand" style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <button className="tour-btn" data-tour-id="sidebar-tour-button"
                  onClick={() => setShowTourPrompt(true)}
                  title="Show me how (LLM tour)">
            💡
          </button>
          <span style={{ flex: 1 }}>
            {tenantBrand.name === "Nixzlab" ? (<>Nixz<span className="accent">lab</span></>) : (tenantBrand.name || "Nixzlab")}
          </span>
          {/* Phase 4: role indicator pill */}
          {role && (
            <span
              className="role-pill"
              title={"Signed in as " + role}
              style={{
                fontSize: 10, textTransform: "uppercase", letterSpacing: 0.6,
                padding: "2px 7px", borderRadius: 999,
                background: "var(--panel2)", color: "var(--accent)",
                border: "1px solid var(--line2)",
                fontWeight: 600,
              }}
            >
              {role}
            </span>
          )}
        </div>

        {/* Phase 4: customer-role welcome card REPLACES the chat list. */}
        {role === "customer" ? (
          <div className="customer-welcome" style={{
            padding: "14px 12px", margin: "10px 6px",
            background: "var(--panel2)", border: "1px solid var(--line2)",
            borderRadius: 10, display: "flex", flexDirection: "column", gap: 10,
          }}>
            <h3 style={{ margin: 0, fontSize: 16, color: "var(--fg)" }}>
              {tenantBrand.name || "Welcome"}
            </h3>
            <p style={{ margin: 0, fontSize: 13, color: "var(--fg2)", lineHeight: 1.4 }}>
              {tenantCopy.welcome || "Start a new request to get going."}
            </p>
            <button
              className="send-btn"
              style={{ borderRadius: 8, padding: "10px 12px", width: "100%" }}
              disabled={!(allowedWorkflows && allowedWorkflows[0])}
              onClick={() => {
                const first = allowedWorkflows && allowedWorkflows[0];
                if (first) newChat(first);
              }}
            >
              + Start a request
            </button>
          </div>
        ) : (
          <>
            <button className="new-chat" data-tour-id="sidebar-newchat" onClick={() => { setActiveChat(null); setMessages([]); }}>
              <span className="plus">+</span> New chat
            </button>

            {/* Phase 4: operator-role pending-cases section ABOVE the chat list.
                Phase 5 will populate chats with flow === "insurance-intake". */}
            {role === "operator" && (
              <div className="pending-cases">
                <div className="chat-list-group">--- Pending cases ---</div>
                {(() => {
                  const pending = chats.filter(c => c.flow === "insurance-intake");
                  if (pending.length === 0) {
                    return (
                      <div className="chat-list-group" style={{ fontStyle: "italic", color: "var(--fg2)" }}>
                        No pending cases
                      </div>
                    );
                  }
                  return pending.map(c => (
                    <div key={"pending-" + c.name}
                         className={`chat-row ${activeChat === c.name ? "active" : ""}`}
                         onClick={() => selectChat(c.name)}>
                      <span className="icon">{workflows[c.flow]?.icon || "📋"}</span>
                      <span className="name">{c.name}</span>
                    </div>
                  ));
                })()}
              </div>
            )}

            {/* Phase 7: ✓ Todo section ABOVE folders. */}
            {(() => {
              const todoChats = visibleChats.filter(c => c.in_todo);
              if (!todoChats.length) return null;
              return (
                <div className="todo-section" data-tour-id="sidebar-todo">
                  <div className="chat-list-group">✓ Todo ({todoChats.length})</div>
                  {todoChats.map(c => {
                    const status = computeChatStatus(c, workflows);
                    return (
                      <div key={"todo-" + c.name}
                           className={`chat-row ${activeChat === c.name ? "active" : ""}`}
                           onClick={() => selectChat(c.name)}>
                        <StatusDot status={status} />
                        <span className="icon">{workflows[c.flow]?.icon || "💬"}</span>
                        <span className="name">{c.name}</span>
                      </div>
                    );
                  })}
                </div>
              );
            })()}

            {/* Phase 13g: 📥📤 I/O sidebar items — clicking opens central I/O tab. */}
            <IOSidebarItems onOpen={() => { setCategory("io"); setSubtab(null); setActiveChat(null); }} />

            {/* Phase 12b: 🧬 Plans — admin/operator only. Lists recent plan rows. */}
            <PlansSection role={role} />

            {/* Phase 7: folder-render section — grouped chats by folder + Loose. */}
            <div className="chat-list folder-render" data-tour-id="sidebar-folders">
              {/* Folder header row: + New folder */}
              <div className="folder-actions">
                <button className="folder-add-btn" onClick={() => setShowNewFolder(true)}
                        title="Create new folder">
                  + folder
                </button>
              </div>
              {folders.map(f => {
                const folderChats = visibleChats.filter(c => c.folder === f.id);
                if (folderChats.length === 0 && !folderExpanded[f.id]) {
                  // still show empty folder header so user knows it exists
                }
                const childStatuses = folderChats.map(c => computeChatStatus(c, workflows));
                const folderStatus = rollupStatus(childStatuses);
                const expanded = !!folderExpanded[f.id];
                return (
                  <div key={"folder-" + f.id} className={`folder-block status-folder-${folderStatus}`}>
                    <div className={`folder-header ${expanded ? "expanded" : "collapsed"}`}
                         onClick={() => toggleFolder(f.id)}
                         style={f.color_hint ? { borderLeftColor: f.color_hint } : null}>
                      <span className="folder-caret">{expanded ? "▾" : "▸"}</span>
                      <span className="folder-icon">📁</span>
                      <span className="folder-name">{f.name}</span>
                      {!expanded && <span className="folder-count">({folderChats.length})</span>}
                      {/* Phase 9: drive + vector counts (only when populated) */}
                      {(f.artifact_count > 0 || f.vector_chunks > 0) && (
                        <span className="folder-stats" style={{ fontSize: 10, color: "var(--fg2)", marginLeft: 4 }}>
                          ({f.artifact_count} files · {f.vector_chunks} chunks)
                        </span>
                      )}
                      <StatusDot status={folderStatus} />
                      {/* Phase 9: mount drive — opens MountDriveModal for this folder */}
                      <button className="folder-mount-btn"
                              data-tour-id="folder-mount"
                              title="Mount drive (local folder) as a source"
                              onClick={(e) => { e.stopPropagation(); setMountFolder(f); }}
                              style={{ background: "transparent", border: "none", color: "var(--fg2)", cursor: "pointer", fontSize: 12, padding: "0 4px" }}>
                        ⊕
                      </button>
                      {/* Phase 9: reindex — visible only when sources exist */}
                      {f.sources && f.sources.length > 0 && (
                        <button className="folder-reindex-btn"
                                data-tour-id="folder-reindex"
                                title="Reindex vector store for this folder"
                                onClick={(e) => { e.stopPropagation(); reindexFolder(f.id); }}
                                style={{ background: "transparent", border: "none", color: "var(--fg2)", cursor: "pointer", fontSize: 12, padding: "0 4px" }}>
                          📚
                        </button>
                      )}
                    </div>
                    {expanded && folderChats.map(c => {
                      const status = computeChatStatus(c, workflows);
                      return (
                        <div key={c.name}
                             className={`chat-row in-folder ${activeChat === c.name ? "active" : ""}`}
                             onClick={() => selectChat(c.name)}
                             onContextMenu={(e) => {
                               e.preventDefault();
                               setMoveMenuChat(moveMenuChat === c.name ? null : c.name);
                             }}>
                          <StatusDot status={status} />
                          <span className="icon">{workflows[c.flow]?.icon || "💬"}</span>
                          <span className="name">{c.name}</span>
                          {moveMenuChat === c.name && (
                            <div className="move-menu" onClick={(e) => e.stopPropagation()}>
                              <div className="move-menu-head">Move to folder ▸</div>
                              {folders.map(ff => (
                                <button key={ff.id} className="move-menu-item"
                                        onClick={() => assignFolder(c.name, ff.id)}>
                                  📁 {ff.name}
                                </button>
                              ))}
                              <button className="move-menu-item"
                                      onClick={() => assignFolder(c.name, null)}>
                                — Loose
                              </button>
                              <button className="move-menu-item"
                                      onClick={() => { setMoveMenuChat(null); setShowNewFolder(true); }}>
                                + New folder…
                              </button>
                              <button className="move-menu-item"
                                      onClick={() => { toggleTodo(c.name, !c.in_todo); setMoveMenuChat(null); }}>
                                {c.in_todo ? "✓ Remove from Todo" : "✓ Add to Todo"}
                              </button>
                            </div>
                          )}
                        </div>
                      );
                    })}
                  </div>
                );
              })}

              {/* Loose section for unfoldered chats. */}
              {(() => {
                const loose = visibleChats.filter(c => !c.folder);
                if (loose.length === 0 && folders.length === 0) {
                  return <div className="chat-list-group">No chats in this view yet</div>;
                }
                if (loose.length === 0) return null;
                return (
                  <div className="loose-section">
                    <div className="chat-list-group">Loose ({loose.length})</div>
                    {loose.map(c => {
                      const status = computeChatStatus(c, workflows);
                      return (
                        <div key={c.name}
                             className={`chat-row ${activeChat === c.name ? "active" : ""}`}
                             onClick={() => selectChat(c.name)}
                             onContextMenu={(e) => {
                               e.preventDefault();
                               setMoveMenuChat(moveMenuChat === c.name ? null : c.name);
                             }}>
                          <StatusDot status={status} />
                          <span className="icon">{workflows[c.flow]?.icon || "💬"}</span>
                          <span className="name">{c.name}</span>
                          {moveMenuChat === c.name && (
                            <div className="move-menu" onClick={(e) => e.stopPropagation()}>
                              <div className="move-menu-head">Move to folder ▸</div>
                              {folders.map(ff => (
                                <button key={ff.id} className="move-menu-item"
                                        onClick={() => assignFolder(c.name, ff.id)}>
                                  📁 {ff.name}
                                </button>
                              ))}
                              <button className="move-menu-item"
                                      onClick={() => { setMoveMenuChat(null); setShowNewFolder(true); }}>
                                + New folder…
                              </button>
                              <button className="move-menu-item"
                                      onClick={() => { toggleTodo(c.name, !c.in_todo); setMoveMenuChat(null); }}>
                                {c.in_todo ? "✓ Remove from Todo" : "✓ Add to Todo"}
                              </button>
                            </div>
                          )}
                        </div>
                      );
                    })}
                  </div>
                );
              })()}
            </div>
          </>
        )}
        {showNewFolder && <NewFolderModal onClose={() => setShowNewFolder(false)} onCreated={() => refreshChats()} />}
        {mountFolder && <MountDriveModal folder={mountFolder} onClose={() => setMountFolder(null)} onMounted={() => { setMountFolder(null); refreshChats(); }} />}
        {showSaveWorkflow && (
          <SaveWorkflowModal
            onClose={() => setShowSaveWorkflow(false)}
            draftSteps={(chats.find(c => c.name === activeChat) || {}).draft_steps || []}
            onSaved={() => { apiFetch("/api/workflows.json").then(r => r.json()).then(setWorkflows); }}
          />
        )}
        {/* Phase 11: card-template authoring modal. Saving fires the
            `card-template-updated` window event, which mergeUserTemplates() above
            listens for + re-pulls /api/cards.json + splices into window.CARD_TYPES. */}
        {showCardBuilder && (
          <CardBuilderModal
            chat={activeChat}
            onClose={() => setShowCardBuilder(false)}
            onSaved={() => { /* card-template-updated event handles the merge */ }}
          />
        )}

        {!hideSettings && (
          <button className="settings-btn" data-tour-id="settings-btn" onClick={() => setShowSettings(true)} title="Connect accounts (Telegram, Twilio, Gmail, keys…)">
            <span className="gear">⚙</span> Settings
          </button>
        )}
        {/* Phase 3: logout — clears token + reloads to the LoginScreen */}
        <button className="settings-btn" onClick={() => { auth.logout(); window.location.reload(); }} title={auth.wallet ? ("Signed in as " + auth.wallet.slice(0, 10) + "…") : "Sign out"}>
          <span className="gear">⎋</span> Sign out
        </button>
      </aside>
      {showSettings && <SettingsPanel role={role} me={auth.me} onClose={() => setShowSettings(false)} />}

      {/* MAIN */}
      <div className="main">
        {/* top tabs */}
        <div className="tabs">
          {categories.map(c => (
            <button key={c.id}
                    className={`tab ${category === c.id ? "active" : ""}`}
                    data-tour-id={`tab-${c.id}`}
                    onClick={() => { setCategory(c.id); setSubtab(null); }}>
              {c.label}
            </button>
          ))}
        </div>

        {/* subtabs (per-workflow filters under the active category) */}
        {/* Phase 12a: hide subtabs row for special categories (library has no subtabs). */}
        {!cat.special && (
        <div className="subtabs">
          <button className={`subtab ${subtab === null ? "active" : ""}`}
                  onClick={() => setSubtab(null)}>
            <span className="icon">⌗</span> All in {cat.label}
          </button>
          {cat.subtabs.map(s => (
            <button key={s}
                    className={`subtab ${subtab === s ? "active" : ""}`}
                    onClick={() => setSubtab(s)}>
              <span className="icon">{workflows[s]?.icon || "·"}</span>
              {workflows[s]?.label || s}
            </button>
          ))}
        </div>
        )}

        {/* Phase 12a: 📦 Library special category renders the addons grid
            instead of the chat area / picker.
            Phase 13g: 📥📤 I/O special category renders the unified I/O feeds. */}
        {cat.special === "library" ? (
          <LibraryTab />
        ) : cat.special === "io" ? (
          <IOCentralTab />
        ) : activeChat ? (() => {
          const activeChatObj = chats.find(c => c.name === activeChat) || { name: activeChat };
          const stepCount = useStepCount(activeChatObj, workflows);
          const activeFolder = activeChatObj.folder ? folders.find(f => f.id === activeChatObj.folder) : null;
          const folderLabel = (activeFolder && activeFolder.name) || tenantBrand.name || "main";
          const flowLabel = (workflows[activeChatObj.flow] && workflows[activeChatObj.flow].label) || activeChatObj.name;
          const draftSteps = activeChatObj.draft_steps || [];
          const showSaveBtn = draftSteps.length >= 2;
          return (
          <div className="chat-area">
            <LineageProvider>
              <div className={`messages-container ${slideDir === "in" ? "slide-in" : ""}`}>
                <div className="messages">
                  {messages.map((m, i) => (
                    <MessageCard
                      key={i} msg={m} idx={i} chat={activeChat}
                      onUploaded={(chat) => refreshMessages(chat || activeChat)}
                      onMention={(mention) => setComposer(c => (mention + " " + (c || "")).trimStart())}
                      onOpenLineage={(idx) => setLineageOpen(idx)}
                      onAction={(kind, info) => {
                        if (kind === "reassigned") { refreshMessages(activeChat); refreshChats(); }
                        else if (kind === "spammed") { refreshMessages(activeChat); refreshChats(); }
                        else if (kind === "hook-new") {
                          // Phase 10: open the Save-Workflow modal pre-filled with the scaffold
                          window.__atlas_hook_scaffold = info;
                          setShowSaveWorkflow(true);
                        }
                        else if (kind === "tour-hint") {
                          // Phase 11.5: SystemCard footer [💡 Show me how] click.
                          // info is the tour_hint payload {intent, service, action}.
                          startTourFromHint(info);
                        }
                      }}
                    />
                  ))}
                  <div ref={msgEnd} />
                </div>
              </div>
              <LineageDrawer openFor={lineageOpen} messages={messages}
                             onClose={() => setLineageOpen(null)} />
            </LineageProvider>
            <div className="composer-wrap">
              {/* Phase 7: breadcrumb pill — \ folder : flow : # \ */}
              <div className={`breadcrumb-pill ${bcPulse ? "lineage-pulse" : ""}`} data-tour-id="breadcrumb-pill" title="folder : flow : step count">
                <span className="bc-slash">\</span>
                <span className="bc-folder">{folderLabel}</span>
                <span className="bc-sep">:</span>
                <span className="bc-flow">{flowLabel}</span>
                {stepCount !== null && (
                  <>
                    <span className="bc-sep">:</span>
                    <span className="bc-count">#{stepCount}</span>
                  </>
                )}
                <span className="bc-slash">\</span>
                {showSaveBtn && (
                  <button className="bc-save" onClick={() => setShowSaveWorkflow(true)}
                          title={`Save ${draftSteps.length} draft steps as workflow`}>
                    💾 Save as workflow
                  </button>
                )}
              </div>
              <div className="composer">
                <textarea
                  data-tour-id="composer-textarea"
                  placeholder="Message Nixzlab… (Enter to send, Shift+Enter for newline)"
                  value={composer}
                  onChange={e => setComposer(e.target.value)}
                  onKeyDown={e => {
                    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
                  }}
                  rows={1}
                />
              </div>
              {/* Phase 7: composer button row — 📎 / ✓ todo / ↑ send */}
              <div className="composer-buttons">
                <input type="file" ref={fileInputRef} style={{ display: "none" }}
                       onChange={(e) => { const f = e.target.files[0]; if (f) onAttachFile(f); e.target.value=""; }} />
                <button className="composer-btn attach-btn"
                        data-tour-id="composer-attach"
                        onClick={() => fileInputRef.current && fileInputRef.current.click()}
                        title="Attach a file (📎)" disabled={sending}>
                  📎
                </button>
                <button className={`composer-btn todo-btn ${activeChatObj.in_todo ? "on" : ""}`}
                        data-tour-id="composer-todo"
                        onClick={() => toggleTodo(activeChat, !activeChatObj.in_todo)}
                        title={activeChatObj.in_todo ? "Remove from Todo" : "Add to Todo"}>
                  {activeChatObj.in_todo ? "✓" : "✓ todo"}
                </button>
                {/* Phase 11: + Create card opens the CardBuilderModal — turns chat
                    context into a user-authored card template that splices into
                    window.CARD_TYPES at runtime via the card-template-updated event. */}
                <button className="composer-btn card-builder-btn"
                        data-tour-id="composer-create-card"
                        onClick={() => setShowCardBuilder(true)}
                        title="Create a card template from this chat">
                  + Create card
                </button>
                <div style={{ flex: 1 }} />
                <button className="send-btn" data-tour-id="composer-send" onClick={send} disabled={!composer.trim() || sending}>
                  {tenantCopy.send_button_label || "↑"}
                </button>
              </div>
            </div>
          </div>
          );
        })() : (
          <div className="empty">
            <h1>{tenantCopy.welcome || "Pick a workflow to start"}</h1>
            {!hidePicker && (
              <>
                <p>Each workflow walks you through a guided flow — answer the prompts, the GPU does the work, your result lands inline.</p>
                <div className="workflow-grid">
                  {allowedFlows.map(fid => {
                    const w = workflows[fid]; if (!w) return null;
                    return (
                      <button key={fid} className="wf-card" data-tour-id={`picker-${fid}`} onClick={() => newChat(fid)}>
                        <span className="icon">{w.icon}</span>
                        <span className="label">{w.label}</span>
                        <span className="desc">{w.tagline}</span>
                      </button>
                    );
                  })}
                </div>
              </>
            )}
            {!hideFreeform && (
              <div className="composer-wrap" style={{width:"100%",maxWidth:760,marginTop:24,borderTop:0}}>
                <div className="composer">
                  <textarea
                    placeholder="Message Nixzlab… (Enter to send, Shift+Enter for newline)"
                    value={composer}
                    onChange={e => setComposer(e.target.value)}
                    onKeyDown={e => {
                      if (e.key === "Enter" && !e.shiftKey) {
                        e.preventDefault();
                        // No active chat: surface a freeform studio chat
                        const m = composer.trim();
                        if (!m) return;
                        setSending(true);
                        const body = new URLSearchParams({ c: "studio", m });
                        setComposer("");
                        apiFetch("/api/chat", { method: "POST", body })
                          .then(() => { setActiveChat("studio"); refreshMessages("studio"); refreshChats(); })
                          .finally(() => setSending(false));
                      }
                    }}
                    rows={1}
                  />
                  <button className="send-btn"
                          onClick={() => {
                            const m = composer.trim(); if (!m) return;
                            setSending(true);
                            const body = new URLSearchParams({ c: "studio", m });
                            setComposer("");
                            apiFetch("/api/chat", { method: "POST", body })
                              .then(() => { setActiveChat("studio"); refreshMessages("studio"); refreshChats(); })
                              .finally(() => setSending(false));
                          }}
                          disabled={!composer.trim() || sending}>
                    {tenantCopy.send_button_label || "↑"}
                  </button>
                </div>
              </div>
            )}
          </div>
        )}
      </div>
      {showTourPrompt && (
        <TourPromptModal
          onClose={() => setShowTourPrompt(false)}
          activeChat={activeChat}
          activeFolder={(chats.find(c => c.name === activeChat) || {}).folder || null}
          onScript={(script) => setPendingTourScript(script)}
        />
      )}
    </div>
    </TourProvider>
  );
}

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