// ============ CITRUS DASHBOARD · ROOT ============
// Owns shared state (active tab, focused agent, modals) and dispatches
// to one section component at a time. Mounts ReactDOM.
//
// Every other component (Sidebar, Topbar, the 13 section components,
// ChatDrawer, ArchitectModal) is loaded as its own <script> and
// attached to window. See dashboard/index.html for load order.

// ---- ACTIVE ENVIRONMENT OVERRIDE ----
// If the user has switched to a non-default environment, override CITRUS_CONFIG
// with that environment's URL and secret before patchFetchAuth runs.
// citrus_active_env_data holds just the active env record so this IIFE can
// apply it synchronously at page load (env list now lives server-side).
(function applyActiveEnv() {
  try {
    // Capture the primary server URL BEFORE it may be overridden below.
    // Environments, auth, and team data always live on the primary server.
    window.__citrusPrimaryUrl = (window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) || window.location.origin;

    const env = JSON.parse(localStorage.getItem("citrus_active_env_data") || "null");
    if (!env) return;
    window.CITRUS_CONFIG = window.CITRUS_CONFIG || {};
    if (env.url)    window.CITRUS_CONFIG.SERVER_URL       = env.url;
    if (env.secret) window.CITRUS_CONFIG.DASHBOARD_SECRET = env.secret;
    // Expose active env name for the topbar badge
    window.__citrusActiveEnv = env;
  } catch {}
})();

// ---- TENANT IMPERSONATION (/t/<slug>/ URL) ----
// The URL is the source of truth. ?tenant=<slug> in the URL → impersonate
// that tenant. No URL param → no impersonation, period. We do NOT persist
// the slug in sessionStorage; doing so would silently carry impersonation
// across tabs/sessions and bleed one tenant's data into another's view.
// Patched fetch (below) adds X-Tenant-Slug on every API call; the server's
// applyTenantSlugOverride middleware honours it for MasterAdmin and for
// the tenant's own Owner.
// ---- FRESH-VISITOR FLAG ----
// `?fresh=1` means "treat this tab as if a brand-new visitor arrived" —
// wipe any session/identity so the dashboard's auth gate shows the
// LoginScreen instead of silently honouring whatever token is already in
// localStorage. The master-admin business-detail view appends this when
// the MasterAdmin clicks a tenant's dashboard URL, so they preview the
// owner's first-touch experience instead of accidentally impersonating
// the tenant via their MasterAdmin token + the slug header.
// MUST run BEFORE captureTenantSlug / patchFetchAuth, because those read
// values we're about to nullify.
(function applyFreshVisitor() {
  try {
    const params = new URLSearchParams(window.location.search);
    if (params.get("fresh") !== "1") return;
    [
      "citrus_auth_token",
      "citrus_identity",
      "citrus_profile",
      "citrus_voice",
      "citrus_active_env_data",
    ].forEach((k) => { try { localStorage.removeItem(k); } catch {} });
    // Strip the flag so a reload doesn't re-fire the wipe.
    params.delete("fresh");
    const qs = params.toString();
    window.history.replaceState(null, "", window.location.pathname + (qs ? "?" + qs : "") + window.location.hash);
  } catch { /* ignore */ }
})();

(function captureTenantSlug() {
  try {
    // Defensive: clear any stale sessionStorage slug from older builds
    // that persisted it. Without this, returning users would silently
    // keep impersonating until they manually cleared site data.
    try { sessionStorage.removeItem("citrus_tenant_slug"); } catch { /* ignore */ }
    // Canonical source: /dashboard/<slug> path segment. Fall back to the
    // legacy ?tenant=<slug> query string so old bookmarks keep working
    // (the server now redirects /dashboard/?tenant=... visits, but a
    // hand-typed query param still resolves here).
    let slug = "";
    const m = window.location.pathname.match(/^\/dashboard\/([a-z0-9-]+)\/?$/);
    if (m) slug = m[1].toLowerCase();
    if (!slug) {
      const params = new URLSearchParams(window.location.search);
      slug = (params.get("tenant") || "").trim().toLowerCase();
    }
    window.__citrusTenantSlug = slug || null;
  } catch { window.__citrusTenantSlug = null; }
})();

// ---- DASHBOARD AUTH ----
// Patch global fetch to inject the Authorization header on every same-server request.
// Priority: user session token (citrus_auth_token) > DASHBOARD_SECRET (legacy/master key).
//
// CRITICAL: SERVER_URL is re-read on every call. The active-env switcher
// rewrites window.CITRUS_CONFIG.SERVER_URL at runtime, so if we captured
// envBase once at IIFE time, fetches to the new env would fail the
// same-server check and the Bearer token wouldn't be attached → 401.
(function patchFetchAuth() {
  const origin = window.location.origin;
  const _orig  = window.fetch.bind(window);
  window.fetch = function (url, opts = {}) {
    const cfg     = window.CITRUS_CONFIG || {};
    const envBase = (cfg.SERVER_URL || "").replace(/\/$/, "");
    const urlStr  = typeof url === "string" ? url : (url instanceof URL ? url.href : String(url));
    // Inject auth for: active env server, primary server origin, or any relative path.
    const isSameServer = urlStr.startsWith("/") ||
      urlStr.startsWith(origin) ||
      (envBase && urlStr.startsWith(envBase));
    if (isSameServer) {
      // Don't override an explicitly-set Authorization header
      if (!(opts.headers && (opts.headers["Authorization"] || opts.headers["authorization"]))) {
        const token = localStorage.getItem("citrus_auth_token") || cfg.DASHBOARD_SECRET;
        if (token) opts = { ...opts, headers: { Authorization: `Bearer ${token}`, ...(opts.headers || {}) } };
      }
      // Inject the tenant slug header so the server scopes data to the
      // impersonated tenant when /t/<slug>/ landed the user here.
      const slug = window.__citrusTenantSlug;
      if (slug && !(opts.headers && opts.headers["X-Tenant-Slug"])) {
        opts = { ...opts, headers: { ...(opts.headers || {}), "X-Tenant-Slug": slug } };
      }
    }
    return _orig(url, opts);
  };
})();

const CharacterAvatar = window.CharacterAvatar;
const ArchitectModal  = window.ArchitectModal;
const Sidebar         = window.Sidebar;
const Topbar          = window.Topbar;
const ChatDrawer      = window.ChatDrawer;

// Section dispatch table: tab id → component (read off window).
const SECTIONS = {
  agents:    window.AgentsTab,
  inbox:     window.InboxTab,
  activity:  window.ActivityTab,
  approvals: window.ApprovalsTab,
  insights:  window.InsightsTab,
  templates: window.TemplatesTab,
  personas:  window.PersonasTab,
  knowledge: window.KnowledgeTab,
  tools:     window.ToolsTab,
  team:      window.TeamTab,
  billing:   window.BillingTab,
  settings:  window.SettingsTab,
};

function nowStamp() {
  const d = new Date();
  const h = d.getHours() % 12 || 12;
  const m = String(d.getMinutes()).padStart(2, "0");
  const ampm = d.getHours() < 12 ? "AM" : "PM";
  return `${h}:${m} ${ampm}`;
}
function shortTime(iso) {
  const d = iso ? new Date(iso) : new Date();
  const h = d.getHours() % 12 || 12;
  const m = String(d.getMinutes()).padStart(2, "0");
  return `${h}:${m}`;
}
function relativeTime(iso) {
  if (!iso) return "now";
  const diffMs = Date.now() - new Date(iso).getTime();
  const min = Math.floor(diffMs / 60000);
  if (min < 1) return "now";
  if (min < 60) return `${min}m`;
  const hr = Math.floor(min / 60);
  if (hr < 24) return `${hr}h`;
  return `${Math.floor(hr / 24)}d`;
}

// Display label for a server-side `from` identifier.
//   "whatsapp:+14155..."  → "+14155..."
function customerLabel(from) {
  if (!from) return "Unknown";
  if (from.startsWith("persona:")) return `🎭 Test persona`;
  return from.replace(/^whatsapp:/i, "");
}

// Build a dashboard-shaped agent from a server agent record. Used when the
// server has agents the dashboard hasn't seen yet (e.g., the seed ORI).
function stubAgentFromServer(s) {
  const palettes = Object.values(window.PALETTES || {});
  const idx = Math.abs((s.id || "").split("").reduce((a, c) => a + c.charCodeAt(0), 0)) % Math.max(1, palettes.length);
  const palette = palettes[idx] || { skin: "#E85D1A", skinDark: "#B84410", accent: "#FFCA7D", visor: "#1A120B" };
  return {
    id: s.id,
    name: (s.name || "AGENT").toUpperCase(),
    role: s.role || "Assistant",
    status: "live",
    rating: 0,
    palette,
    glyph: "🤖",
    brief: s.brief || "",
    tools: s.tools || [],
    metrics: { "Tasks done": "0", "Avg reply": "—", "Hours saved": "0" },
    spark: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    sparkLabel: "Tasks / day",
    accuracy: 0,
    runs: 0,
    personality: s.personality || "warm",
    approvalGate: s.approvalGate || "10000",
    channels: s.channels || {},
    learnings: s.learnings || [],
    learnedAt: s.learnedAt || null,
    status: s.status || "live",
    rating: s.rating || 0,
  };
}

// Convert a server conversation (with full messages array) into a dashboard
// thread. The `from` field doubles as a stable thread id. Each transcript
// entry keeps the ISO timestamp (`iso`) alongside the display string (`t`)
// so per-day metrics can bucket precisely.
function threadFromConversation(c) {
  const msgs = c.messages || [];
  const last = msgs[msgs.length - 1];
  const transcript = msgs.map((m) => ({
    from: m.role === "customer" ? "customer" : "agent",
    text: m.content,
    t: shortTime(m.t),
    iso: m.t,
  }));
  return {
    id: c.from,
    agent: c.agentId,
    customer: customerLabel(c.from),
    channel: c.channel || "WhatsApp",
    last: last ? last.content : "",
    time: last ? relativeTime(last.t) : "now",
    unread: !!last && last.role === "customer",
    status: c.wrappedUp ? "handled" : (c.takenOver ? "needs-you" : "active"),
    tag: c.wrappedUp ? "Wrapped up" : (c.takenOver ? "You're replying" : "Live"),
    transcript,
  };
}
// ============ LOGIN SCREEN ============
function LoginScreen({ onLogin, serverUrl, initialPhase, initialError }) {
  const S   = serverUrl || (window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) || "";
  // Always authenticate against the PRIMARY server. applyActiveEnv() may have
  // overwritten CITRUS_CONFIG.SERVER_URL with a sandbox URL, but
  // window.__citrusPrimaryUrl is captured before that override occurs.
  const authBase = window.__citrusPrimaryUrl || S;
  const cfg = window.CITRUS_CONFIG || {};
  const hasGoogle = !!cfg.GOOGLE_CLIENT_ID;

  // Detect a password-reset token in the URL on mount — that's how the
  // email link lands the user here. If present, we open into the reset
  // phase directly so the owner can pick a new password.
  const _initialResetToken = (() => {
    try {
      const params = new URLSearchParams(window.location.search);
      return (params.get("reset") || "").trim();
    } catch { return ""; }
  })();

  // phase: "login" | "setup" | "forgot" | "forgot-sent" | "reset" | "reset-done"
  const [phase, setPhase]     = useState(_initialResetToken ? "reset" : (initialPhase || "login"));
  const [email, setEmail]     = useState("");
  const [password, setPass]   = useState("");
  const [name, setName]       = useState("");
  const [resetToken, setResetToken] = useState(_initialResetToken);
  const [error, setError]     = useState(initialError || "");
  const [info,  setInfo]      = useState("");
  const [loading, setLoading] = useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setError(""); setInfo(""); setLoading(true);
    try {
      const endpoint = phase === "setup" ? `${authBase}/auth/setup` : `${authBase}/auth/login`;
      const body = phase === "setup" ? { email, password, name } : { email, password };
      const r = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
      const j = await r.json();
      if (!r.ok) { setError(j.error || "Something went wrong"); return; }
      localStorage.setItem("citrus_auth_token", j.token);
      onLogin(j.user);
    } catch {
      setError("Could not reach server. Check your connection.");
    } finally { setLoading(false); }
  };

  const submitForgot = async (e) => {
    e.preventDefault();
    setError(""); setInfo(""); setLoading(true);
    try {
      const r = await fetch(`${authBase}/auth/password-reset/start`, {
        method: "POST", headers: { "content-type": "application/json" },
        body: JSON.stringify({ email }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { setError(j.error || "Couldn't send reset email"); return; }
      setPhase("forgot-sent");
    } catch {
      setError("Could not reach server. Check your connection.");
    } finally { setLoading(false); }
  };

  const submitReset = async (e) => {
    e.preventDefault();
    setError(""); setInfo(""); setLoading(true);
    try {
      const r = await fetch(`${authBase}/auth/password-reset/complete`, {
        method: "POST", headers: { "content-type": "application/json" },
        body: JSON.stringify({ token: resetToken, newPassword: password }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { setError(j.error || "Reset failed — the link may have expired."); return; }
      // Strip the ?reset= param from the URL so a reload doesn't re-enter
      // the reset phase with an already-used token.
      try {
        const u = new URL(window.location.href);
        u.searchParams.delete("reset");
        window.history.replaceState(null, "", u.pathname + (u.search ? "?" + u.searchParams.toString() : "") + u.hash);
      } catch { /* ignore */ }
      setPass(""); setPhase("reset-done");
    } catch {
      setError("Could not reach server. Check your connection.");
    } finally { setLoading(false); }
  };

  const signInWithGoogle = () => {
    window.location.href = `${authBase}/auth/google`;
  };

  const inputStyle = { padding: "10px 14px", borderRadius: 10, border: "1px solid var(--border)", fontSize: 14, background: "var(--paper)", color: "var(--ink)", outline: "none", width: "100%", boxSizing: "border-box" };

  const linkStyle = { background: "none", border: "none", color: "var(--accent)", fontSize: 12, fontWeight: 500, cursor: "pointer", padding: 0, textDecoration: "underline", textUnderlineOffset: 2 };

  const titleFor = (p) => {
    if (p === "setup")        return "Set up your account";
    if (p === "forgot" ||
        p === "forgot-sent")  return "Reset your password";
    if (p === "reset" ||
        p === "reset-done")   return "Choose a new password";
    return "Welcome back";
  };
  const subFor = (p) => {
    if (p === "setup")        return "Create the first owner account to get started.";
    if (p === "forgot")       return "Enter your email and we'll send a reset link.";
    if (p === "forgot-sent")  return "Check your inbox. The link expires in 30 minutes.";
    if (p === "reset")        return "Pick a new password to finish resetting your account.";
    if (p === "reset-done")   return "Password updated. Sign in with your new password.";
    return "Sign in to your Citrus dashboard.";
  };

  return (
    <div style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", background: "var(--bg)", padding: 24 }}>
      <div style={{ width: "100%", maxWidth: 380, background: "var(--paper)", border: "1px solid var(--border)", borderRadius: 20, padding: "40px 36px", boxShadow: "0 8px 40px rgba(0,0,0,0.08)" }}>
        <div style={{ fontSize: 28, marginBottom: 8, textAlign: "center" }}>◍</div>
        <h2 style={{ fontSize: 20, fontWeight: 700, textAlign: "center", margin: "0 0 4px" }}>{titleFor(phase)}</h2>
        <p style={{ fontSize: 13, color: "var(--ink-2)", textAlign: "center", marginBottom: 28 }}>{subFor(phase)}</p>

        {error && <div style={{ fontSize: 13, color: "#C04545", background: "color-mix(in oklab, #C04545 8%, transparent)", border: "1px solid color-mix(in oklab, #C04545 20%, transparent)", borderRadius: 8, padding: "8px 12px", marginBottom: 16 }}>{error}</div>}
        {info  && <div style={{ fontSize: 13, color: "#3F8C3A", background: "color-mix(in oklab, #3F8C3A 8%, transparent)", border: "1px solid color-mix(in oklab, #3F8C3A 20%, transparent)", borderRadius: 8, padding: "8px 12px", marginBottom: 16 }}>{info}</div>}

        {/* ---- LOGIN / SETUP ---- */}
        {(phase === "login" || phase === "setup") && (
          <>
            {hasGoogle && (
              <>
                <button
                  type="button"
                  onClick={signInWithGoogle}
                  style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 10, width: "100%", padding: "10px 14px", borderRadius: 10, border: "1px solid var(--border)", background: "var(--paper)", color: "var(--ink)", fontSize: 14, fontWeight: 500, cursor: "pointer" }}
                >
                  <svg width="18" height="18" viewBox="0 0 18 18">
                    <path fill="#4285F4" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z"/>
                    <path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z"/>
                    <path fill="#FBBC05" d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"/>
                    <path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z"/>
                  </svg>
                  {phase === "setup" ? "Sign up with Google" : "Sign in with Google"}
                </button>
                <div style={{ display: "flex", alignItems: "center", gap: 12, margin: "20px 0" }}>
                  <div style={{ flex: 1, height: 1, background: "var(--border)" }} />
                  <span style={{ fontSize: 12, color: "var(--ink-3)" }}>or</span>
                  <div style={{ flex: 1, height: 1, background: "var(--border)" }} />
                </div>
              </>
            )}

            <form onSubmit={submit} style={{ display: "flex", flexDirection: "column", gap: 12 }}>
              {phase === "setup" && (
                <input className="st-input" type="text" placeholder="Your name" value={name} autoFocus onChange={(e) => setName(e.target.value)} style={inputStyle} />
              )}
              <input className="st-input" type="email" placeholder="Email address" value={email} required autoFocus={phase !== "setup" && !hasGoogle} onChange={(e) => setEmail(e.target.value)} style={inputStyle} />
              <input className="st-input" type="password" placeholder="Password" value={password} required onChange={(e) => setPass(e.target.value)} style={inputStyle} />
              <button type="submit" className="btn btn-primary" disabled={loading} style={{ marginTop: 4, padding: "11px", fontSize: 14, fontWeight: 600, borderRadius: 10 }}>
                {loading ? "…" : phase === "setup" ? "Create account" : "Sign in"}
              </button>
            </form>

            {phase === "login" && (
              <div style={{ marginTop: 16, textAlign: "center" }}>
                <button type="button" style={linkStyle}
                  onClick={() => { setPhase("forgot"); setError(""); setPass(""); }}>
                  Forgot your password?
                </button>
              </div>
            )}
          </>
        )}

        {/* ---- FORGOT: enter email ---- */}
        {phase === "forgot" && (
          <>
            <form onSubmit={submitForgot} style={{ display: "flex", flexDirection: "column", gap: 12 }}>
              <input className="st-input" type="email" placeholder="Your email" value={email} required autoFocus
                onChange={(e) => setEmail(e.target.value)} style={inputStyle} />
              <button type="submit" className="btn btn-primary" disabled={loading || !email.trim()}
                style={{ marginTop: 4, padding: "11px", fontSize: 14, fontWeight: 600, borderRadius: 10 }}>
                {loading ? "Sending…" : "Send reset link"}
              </button>
            </form>
            <div style={{ marginTop: 16, textAlign: "center" }}>
              <button type="button" style={linkStyle} onClick={() => { setPhase("login"); setError(""); }}>
                Back to sign in
              </button>
            </div>
          </>
        )}

        {/* ---- FORGOT-SENT: confirmation ---- */}
        {phase === "forgot-sent" && (
          <>
            <p style={{ fontSize: 13, color: "var(--ink-2)", textAlign: "center", lineHeight: 1.5, marginBottom: 20 }}>
              We sent a reset link to <b>{email}</b> if an account exists for that address. Check your inbox (and spam folder). The link expires in 30 minutes.
            </p>
            <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              <button type="button" className="btn btn-ghost"
                style={{ padding: "10px", fontSize: 13, borderRadius: 10 }}
                onClick={() => setPhase("forgot")}>
                Send to a different email
              </button>
              <button type="button" className="btn btn-primary"
                style={{ padding: "11px", fontSize: 14, fontWeight: 600, borderRadius: 10 }}
                onClick={() => { setPhase("login"); setError(""); setInfo(""); }}>
                Back to sign in
              </button>
            </div>
          </>
        )}

        {/* ---- RESET: token from email + new password ---- */}
        {phase === "reset" && (
          <>
            <form onSubmit={submitReset} style={{ display: "flex", flexDirection: "column", gap: 12 }}>
              <input className="st-input" type="password" placeholder="New password" value={password} required autoFocus
                onChange={(e) => setPass(e.target.value)} style={inputStyle} minLength={8} />
              <button type="submit" className="btn btn-primary" disabled={loading || password.length < 8}
                style={{ marginTop: 4, padding: "11px", fontSize: 14, fontWeight: 600, borderRadius: 10 }}>
                {loading ? "Saving…" : "Save new password"}
              </button>
            </form>
            <p style={{ fontSize: 11, color: "var(--ink-3)", textAlign: "center", marginTop: 12, lineHeight: 1.4 }}>
              Minimum 8 characters. After resetting, all your existing sessions are invalidated — you'll need to sign in again on every device.
            </p>
          </>
        )}

        {/* ---- RESET-DONE: bounce to login ---- */}
        {phase === "reset-done" && (
          <>
            <div style={{ textAlign: "center", marginBottom: 20 }}>
              <div style={{ width: 48, height: 48, borderRadius: "50%", background: "color-mix(in oklab, #3F8C3A 18%, transparent)", color: "#3F8C3A", display: "inline-grid", placeItems: "center", fontSize: 22, fontWeight: 700 }}>✓</div>
            </div>
            <button type="button" className="btn btn-primary" style={{ width: "100%", padding: "11px", fontSize: 14, fontWeight: 600, borderRadius: 10 }}
              onClick={() => { setPhase("login"); setError(""); setInfo(""); setPass(""); }}>
              Sign in with new password
            </button>
          </>
        )}
      </div>
    </div>
  );
}

// ============ AUTH GATE ============
function AuthGate({ children }) {
  const cfg = window.CITRUS_CONFIG || {};
  const S   = cfg.SERVER_URL || "";
  // "login" | "setup" | "ready" | "checking"
  const [authState, setAuthState] = useState("checking");
  const [authUser,  setAuthUser]  = useState(null);
  const [oauthError, setOauthError] = useState("");

  useEffect(() => {
    // Handle Google OAuth redirect: ?citrus_token=xxx or ?auth_error=xxx
    const params = new URLSearchParams(window.location.search);
    const oauthToken = params.get("citrus_token");
    const authErr    = params.get("auth_error");
    if (oauthToken || authErr) {
      // Clean up URL without a page reload
      const clean = window.location.pathname + window.location.hash;
      window.history.replaceState(null, "", clean);
      if (authErr) { setOauthError(decodeURIComponent(authErr)); }
      if (oauthToken) {
        localStorage.setItem("citrus_auth_token", oauthToken);
        // Fall through — the token verification below will pick it up
      }
    }

    // If AUTH_REQUIRED is false (no users set up yet) AND we have DASHBOARD_SECRET, skip login
    if (cfg.AUTH_REQUIRED === false && cfg.DASHBOARD_SECRET) {
      setAuthState("ready"); return;
    }
    // If no auth required at all (dev mode)
    if (!cfg.AUTH_REQUIRED && !cfg.DASHBOARD_SECRET) {
      setAuthState("ready"); return;
    }
    const token = localStorage.getItem("citrus_auth_token");
    if (!token) {
      // Check if this is first boot (no users) → show setup screen
      fetch(`${S}/auth/me`, { headers: { Authorization: `Bearer __probe__` } })
        .then((r) => r.status === 403 ? "setup" : "login")
        .catch(() => "login")
        .then((next) => setAuthState(next));
      return;
    }
    // Verify existing token
    fetch(`${S}/auth/me`, { headers: { Authorization: `Bearer ${token}` } })
      .then((r) => r.ok ? r.json() : null)
      .then((data) => {
        if (data?.user) {
          // Defensive cache clear: if the identity behind this token differs
          // from the one whose profile is sitting in localStorage, wipe the
          // tenant cache. Without this, users who re-login through a path
          // that bypasses handleLogin (e.g. silent token refresh, manual
          // localStorage swap, browser back/forward) see the previous
          // tenant's name/logo/voice until they touch Settings.
          try {
            const stored = localStorage.getItem("citrus_identity") || "";
            const ident  = `${data.user.id || data.user.email}|${data.user.businessId || "self"}`;
            if (stored && stored !== ident) {
              localStorage.removeItem("citrus_profile");
              localStorage.removeItem("citrus_voice");
              localStorage.removeItem("citrus_active_env_data");
              window.__citrusTenantSlug = null;
            }
            localStorage.setItem("citrus_identity", ident);
          } catch { /* ignore */ }
          setAuthUser(data.user); setAuthState("ready");
          // Keep the URL pinned to this user's canonical tenant. Non-master
          // Owners cannot impersonate, so if the URL says /t/<some-other>/
          // they're shown stale data with a misleading URL — redirect them
          // home. MasterAdmin keeps whatever URL they're at (impersonation
          // is intentional for them).
          try {
            const slug = data.user.businessSlug;
            const isMaster = data.user.role === "MasterAdmin";
            const urlSlug = (new URLSearchParams(window.location.search)).get("tenant") || null;
            const path    = window.location.pathname;

            if (!isMaster) {
              // Owner: force them to /dashboard/<their-slug> (or /dashboard/
              // if they don't have one yet — shouldn't happen, defensive).
              const target = slug && slug !== "self"
                ? `/dashboard/${encodeURIComponent(slug)}`
                : "/dashboard/";
              const pathSlug = (path.match(/^\/dashboard\/([a-z0-9-]+)\/?$/) || [])[1] || null;
              const onTarget = path === target || (pathSlug && pathSlug === slug);
              if (!onTarget) {
                // Hard navigation so any in-memory tenant context is reset.
                window.location.replace(target);
                return;
              }
              window.__citrusTenantSlug = slug && slug !== "self" ? slug : null;
            } else if (urlSlug) {
              // MasterAdmin with an explicit slug in the URL: honour it.
              window.__citrusTenantSlug = urlSlug;
            } else {
              // MasterAdmin without a slug: ensure no leftover impersonation.
              window.__citrusTenantSlug = null;
            }
          } catch { /* ignore */ }
        }
        else { localStorage.removeItem("citrus_auth_token"); setAuthState("login"); }
      })
      .catch(() => setAuthState("login"));
  }, []);

  // Check if server has no users (needs setup)
  useEffect(() => {
    if (authState !== "login") return;
    fetch(`${S}/auth/setup`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) })
      .then((r) => r.json())
      .then((j) => { if (j.error === "Setup already completed. Use login.") return; /* already has users */ })
      .catch(() => {});
  }, [authState]);

  // Wipe any cached per-tenant UI state when the active identity changes.
  // citrus_profile / citrus_voice are browser-wide caches that previously
  // bled between tenants — signing into Gabtic but showing Bahr's name
  // because the prior session had saved Bahr's profile to localStorage.
  // Clear on every login transition AND every logout; server is truth.
  const clearLocalTenantCache = () => {
    try {
      localStorage.removeItem("citrus_profile");
      localStorage.removeItem("citrus_voice");
      localStorage.removeItem("citrus_active_env_data"); // env override
    } catch { /* ignore */ }
    window.__citrusTenantSlug = null;
  };
  const handleLogin = (user) => {
    clearLocalTenantCache();
    setAuthUser(user);
    setAuthState("ready");
    // Force the URL to the canonical place for this identity, ALWAYS —
    // regardless of where the login form was rendered from. Without this,
    // a stale /t/<other-slug>/ URL would survive sign-in: my middleware
    // would refuse the slug impersonation for the new user, but the
    // visual URL would still say the wrong tenant and the X-Tenant-Slug
    // header on outbound fetches would keep getting silently ignored —
    // hugely confusing. window.location.replace forces a full page load
    // so every piece of in-memory state from the previous session is gone.
    try {
      const slug = user && user.businessSlug;
      const isMaster = user && user.role === "MasterAdmin";
      const target = isMaster || !slug || slug === "self"
        ? "/dashboard/"
        : `/dashboard/${encodeURIComponent(slug)}`;
      const here = window.location.pathname + window.location.search;
      if (here !== target) window.location.replace(target);
    } catch { /* ignore */ }
  };
  const handleLogout = () => {
    const token = localStorage.getItem("citrus_auth_token");
    if (token) fetch(`${S}/auth/logout`, { method: "POST", headers: { Authorization: `Bearer ${token}` } }).catch(() => {});
    localStorage.removeItem("citrus_auth_token");
    localStorage.removeItem("citrus_identity");
    clearLocalTenantCache();
    setAuthUser(null);
    setAuthState("login");
    // Strip any /t/<slug>/ path or ?tenant= query param. Without this,
    // the URL still says "tenant=bahr" on the login screen, and the
    // next sign-in's captureTenantSlug picks it back up → the new user
    // silently inherits the previous tenant's impersonation context.
    try {
      window.history.replaceState(null, "", "/dashboard/");
    } catch { /* ignore */ }
  };

  if (authState === "checking") return (
    <div style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", background: "var(--bg)" }}>
      <div style={{ fontSize: 28, opacity: 0.4 }}>◍</div>
    </div>
  );

  if (authState === "login" || authState === "setup") {
    return <LoginScreen onLogin={handleLogin} serverUrl={S} initialPhase={authState} initialError={oauthError} />;
  }

  return React.cloneElement(children, { authUser, onLogout: handleLogout });
}

function Dashboard({ authUser, onLogout }) {
  const [tab, setTab]           = useState("agents");
  const [archOpen, setArchOpen] = useState(false);
  const [archPreset, setArchPreset] = useState(null); // { flow, name } — seeds the architect
  const [agents, setAgents]     = useState(AGENTS);
  const [approvalCount, setApprovalCount] = useState(0);
  const [activity, setActivity] = useState(ACTIVITY);
  const [threads, setThreads]   = useState(INBOX_THREADS);
  const [focused, setFocused]   = useState(null); // agent id, for detail view
  const [chatId, setChatId]     = useState(null);
  const [agentLogs, setAgentLogs] = useState({}); // { [agentId]: [{level, text, ts}] }
  const [conflicts, setConflicts] = useState([]);
  const [globalLearnings, setGlobalLearnings] = useState(null); // null = not yet loaded; set on first SSE
  const [tourOpen, setTourOpen] = useState(false);
  const [onboardingOpen, setOnboardingOpen] = useState(false);
  // Listen for "Re-run onboarding" clicks from Settings → Company so the
  // user can re-open the wizard mid-session. Same dispatcher pattern the
  // landing page uses (citrus:open-signin etc.).
  useEffect(() => {
    const handler = () => setOnboardingOpen(true);
    window.addEventListener("citrus-open-onboarding", handler);
    return () => window.removeEventListener("citrus-open-onboarding", handler);
  }, []);
  const [mobileNavOpen, setMobileNavOpen] = useState(false); // sidebar drawer on mobile
  const [envs, setEnvs] = useState([]);
  const [activeEnvId, setActiveEnvId] = useState(() => localStorage.getItem("citrus_active_env") || "");
  const openArchitect = (preset) => {
    // Diagnostic — surfaces in the browser console so we can confirm the
    // click reached app.jsx when the user reports "Hire isn't working".
    try { console.debug("[citrus] openArchitect", preset || null); } catch {}
    setArchPreset(preset || null);
    setArchOpen(true);
  };

  // Close the mobile drawer whenever the user picks a tab or focuses an agent
  useEffect(() => { setMobileNavOpen(false); }, [tab, focused]);

  // Monotonic id for newly-injected activity entries
  const idRef = useRef(1000);
  const nextId = () => ++idRef.current;

  const updateRating = (id, rating) => {
    setAgents((prev) => prev.map((a) => (a.id === id ? { ...a, rating } : a)));
    const SERVER_URL = (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) || "";
    if (!SERVER_URL) return;
    fetch(`${SERVER_URL}/agents/${id}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ rating }),
    }).catch(() => {});
  };

  // Update arbitrary agent fields (channel bindings, brief edits, etc.) and
  // re-sync the agent to the runtime server so /whatsapp picks up the new
  // routing immediately.
  const updateAgent = (id, patch) => {
    setAgents((prev) => prev.map((a) => {
      if (a.id !== id) return a;
      const next = { ...a, ...patch, channels: { ...(a.channels || {}), ...(patch.channels || {}) } };
      syncAgentToServer(next);
      return next;
    }));
  };

  const addActivity = (entry) => {
    setActivity((prev) => [entry, ...prev].slice(0, 60));
    const SERVER_URL = (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) || "";
    if (!SERVER_URL) return;
    fetch(`${SERVER_URL}/activity`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ type: entry.k || "action", agentId: entry.agent, text: entry.text, k: entry.k }),
    }).catch(() => {});
  };

  // ---- LIVE INBOX BRIDGE ----
  // Mirror the runtime server's conversations into the dashboard. On mount:
  //   1. Pull the server's agents and merge any the dashboard hasn't seen.
  //   2. Pull every conversation + its full transcript and turn each into
  //      an Inbox thread.
  //   3. Open an SSE stream on /events for live updates: new messages get
  //      appended to the matching thread, new conversations get prepended,
  //      takeover/wrap-up flips status.
  useEffect(() => {
    const SERVER_URL = (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) || "";
    if (!SERVER_URL) return; // no runtime server → keep the local-only experience

    let cancelled = false;
    let es = null;

    // Functional updates — we never want to read stale `agents`/`threads` here.
    const upsertAgent = (server) => {
      setAgents((prev) => {
        const idx = prev.findIndex((a) => a.id === server.id);
        if (idx === -1) return [...prev, stubAgentFromServer(server)];
        const next = prev.slice();
        next[idx] = {
          ...next[idx],
          name:      server.name      || next[idx].name,
          role:      server.role      || next[idx].role,
          brief:     server.brief     || next[idx].brief,
          tools:     server.tools     || next[idx].tools,
          channels:  server.channels  || next[idx].channels,
          learnings: server.learnings ?? next[idx].learnings,
          learnedAt: server.learnedAt ?? next[idx].learnedAt,
          status:    server.status    ?? next[idx].status,
          rating:    server.rating    ?? next[idx].rating,
        };
        return next;
      });
    };

    const upsertThread = (thread) => {
      // Remember which agent owns this thread so subsequent SSE events
      // (message/lead/wrapup) can credit the right agent without refetching.
      const map = (window.__citrusThreadAgent ||= {});
      if (thread.agent) map[thread.id] = thread.agent;
      setThreads((prev) => {
        const idx = prev.findIndex((t) => t.id === thread.id);
        if (idx === -1) return [thread, ...prev];
        const next = prev.slice();
        next[idx] = { ...next[idx], ...thread };
        return next;
      });
    };

    const appendMessage = (from, message) => {
      setThreads((prev) => {
        const idx = prev.findIndex((t) => t.id === from);
        if (idx === -1) return prev; // thread arrives via new_conversation first
        const next = prev.slice();
        const t = next[idx];
        const isCustomer = message.role === "customer";
        const nowIso = new Date().toISOString();
        next[idx] = {
          ...t,
          last: message.content,
          time: "now",
          unread: t.unread || isCustomer,
          status: isCustomer && t.status === "active" ? "active" : t.status,
          transcript: [
            ...t.transcript,
            { from: isCustomer ? "customer" : "agent", text: message.content, t: shortTime(nowIso), iso: nowIso },
          ],
        };
        return next;
      });
    };

    const setThreadFlag = (from, patch) => {
      setThreads((prev) => prev.map((t) => t.id === from ? { ...t, ...patch } : t));
    };

    // Initial hydrate: agents + every conversation.
    (async () => {
      try {
        const agentsRes = await fetch(`${SERVER_URL}/agents`);
        if (agentsRes.ok) {
          const list = await agentsRes.json();
          if (cancelled) return;
          for (const s of list) upsertAgent(s);
        }
      } catch { /* server may be down — fall through */ }

      try {
        const convosRes = await fetch(`${SERVER_URL}/conversations`);
        if (convosRes.ok) {
          const list = await convosRes.json();
          if (cancelled) return;
          // Fetch each full transcript in parallel, oldest first so prepend
          // ordering matches recency when we drop them in.
          const fulls = await Promise.all(
            list.map((c) => fetch(`${SERVER_URL}/conversations/${encodeURIComponent(c.from)}`).then((r) => r.ok ? r.json() : null).catch(() => null))
          );
          if (cancelled) return;
          for (const c of fulls.filter(Boolean)) upsertThread(threadFromConversation(c));
        }
      } catch { /* tolerate */ }

      try {
        const actRes = await fetch(`${SERVER_URL}/activity`);
        if (actRes.ok && !cancelled) {
          const list = await actRes.json();
          if (list.length > 0) setActivity(list);
        }
      } catch { /* tolerate */ }

      try {
        const cfRes = await fetch(`${SERVER_URL}/conflicts`);
        if (cfRes.ok && !cancelled) setConflicts(await cfRes.json());
      } catch { /* tolerate */ }

      try {
        // Use a relative URL — always hits the primary server regardless of
        // which environment's SERVER_URL is active.
        const envRes = await fetch("/admin/environments");
        if (envRes.ok && !cancelled) setEnvs((await envRes.json()).environments || []);
      } catch { /* tolerate */ }

      // Fresh-tenant first-run flow. Now two-stage:
      //   1. OnboardingWizard — six-question intake → Claude turns it
      //      into a structured profile + recommended agents. Triggered
      //      when the tenant has no profile yet (server-side check via
      //      /onboarding/status). Wins over the spotlight tour because
      //      it captures real data instead of just narrating the UI.
      //   2. The legacy spotlight tour stays available from the topbar
      //      "Take a tour" button but no longer auto-fires.
      try {
        const me = window.__citrusUser || {};
        const dismissKey = `citrus_onboarding_dismissed:${me.email || "anon"}`;
        if (!localStorage.getItem(dismissKey)) {
          const r = await fetch(`${SERVER_URL}/onboarding/status`);
          if (r.ok) {
            const { needs } = await r.json();
            if (needs && !cancelled) setOnboardingOpen(true);
          }
        }
      } catch { /* tolerate */ }
    })();

    // Live updates.
    try {
      const _secret = window.CITRUS_CONFIG && window.CITRUS_CONFIG.DASHBOARD_SECRET;
      const _eventsUrl = _secret ? `${SERVER_URL}/events?token=${encodeURIComponent(_secret)}` : `${SERVER_URL}/events`;
      es = new EventSource(_eventsUrl);
      es.addEventListener("agent", (e) => {
        try { upsertAgent(JSON.parse(e.data).agent); } catch {}
      });
      es.addEventListener("conflicts_updated", (e) => {
        try { setConflicts(JSON.parse(e.data).conflicts || []); } catch {}
      });
      es.addEventListener("global_learnings", (e) => {
        try { setGlobalLearnings(JSON.parse(e.data).learnings || []); } catch {}
      });
      es.addEventListener("new_conversation", async (e) => {
        try {
          const { from, agentId } = JSON.parse(e.data);
          const r = await fetch(`${SERVER_URL}/conversations/${encodeURIComponent(from)}`);
          if (!r.ok) return;
          const c = await r.json();
          upsertThread(threadFromConversation(c));
          addActivity({
            id: nextId(), t: nowStamp(), agent: agentId,
            text: `New customer started a chat on ${c.channel}`,
            k: "ok",
          });
          window.toast && window.toast(`New conversation on ${c.channel}`, "good");
        } catch {}
      });
      es.addEventListener("message", (e) => {
        try {
          const { from, message } = JSON.parse(e.data);
          appendMessage(from, message);
          // Each agent reply counts as a run so the agent's stat cards (and
          // billing) reflect real volume. The agent id comes from the thread
          // lookup populated when threads hydrate.
          if (message.role === "agent") {
            const agentId = (window.__citrusThreadAgent ||= {})[from];
            if (agentId) {
              setAgents((prev) => prev.map((a) => a.id === agentId ? {
                ...a,
                runs: (a.runs || 0) + 1,
                spark: [...a.spark.slice(1), Math.min(20, (a.runs || 0) + 1)],
              } : a));
            }
          }
        } catch {}
      });
      es.addEventListener("lead", (e) => {
        try {
          const { from, lead } = JSON.parse(e.data);
          const lookup = (window.__citrusThreadAgent ||= {});
          const agentId = lookup[from];
          const bits = [];
          if (lead.name)    bits.push(`name: ${lead.name}`);
          if (lead.company) bits.push(`company: ${lead.company}`);
          if (bits.length === 0) return;
          addActivity({
            id: nextId(), t: nowStamp(), agent: agentId || null,
            text: `Captured lead — ${bits.join(", ")}`,
            k: "lead",
          });
        } catch {}
      });
      es.addEventListener("takeover", (e) => {
        try {
          const { from, takenOver } = JSON.parse(e.data);
          setThreadFlag(from, { status: takenOver ? "needs-you" : "active", tag: takenOver ? "You're replying" : "Live" });
        } catch {}
      });
      // Persona sessions clean up their conversation when stopped — drop it from the Inbox too.
      es.addEventListener("conversation_deleted", (e) => {
        try {
          const { from } = JSON.parse(e.data);
          setThreads((prev) => prev.filter((t) => t.from !== from));
        } catch {}
      });
      es.addEventListener("agent_log", (e) => {
        try {
          const d = JSON.parse(e.data);
          setAgentLogs((prev) => {
            const cur = prev[d.agentId] || [];
            return { ...prev, [d.agentId]: [...cur.slice(-199), { level: d.level, text: d.text, ts: d.ts }] };
          });
        } catch {}
      });
      es.addEventListener("learning", (e) => {
        try {
          const { agentId, agentName, lesson } = JSON.parse(e.data);
          // Push new lesson onto the agent's learnings array so the
          // "What I've learned" panel updates without a page refresh.
          setAgents((prev) => prev.map((a) => a.id === agentId ? {
            ...a,
            learnings: [...(a.learnings || []), lesson].slice(-30),
            learnedAt: lesson.at || new Date().toISOString(),
          } : a));
          addActivity({
            id: nextId(), t: nowStamp(), agent: agentId,
            text: `${agentName} learned: ${lesson.what}`,
            why: lesson.why || null,
            lessonId: lesson.id || null,
            k: "ok",
          });
          window.toast && window.toast(`${agentName} just got smarter`, "good");
        } catch {}
      });
      es.addEventListener("wrapup", (e) => {
        try {
          const { from, summary } = JSON.parse(e.data);
          setThreadFlag(from, { status: "handled", tag: "Wrapped up" });
          const lookup = (window.__citrusThreadAgent ||= {});
          const agentId = lookup[from];
          const status = summary?.status || "closed";
          const need = (summary?.need || "").slice(0, 80);
          addActivity({
            id: nextId(), t: nowStamp(), agent: agentId || null,
            text: `Wrapped up · ${status}${need ? " — " + need : ""}`,
            k: status === "lead" || status === "booked" ? "lead" : "ok",
          });
        } catch {}
      });
      es.onerror = () => { /* browser will auto-reconnect */ };
    } catch { /* EventSource unavailable — tolerate */ }

    return () => {
      cancelled = true;
      if (es) es.close();
    };
  }, []);

  // Called by the architect modal when the user clicks "Put it on shift".
  // The runtime server takes over from here — every real customer message
  // and wrap-up flows back as an activity entry via SSE. No fake demo data.
  const addAgent = (newAgent) => {
    const live = { ...newAgent, status: "live" };
    setAgents((prev) => [...prev, live]);
    setTab("agents");
    setFocused(null);
    addActivity({
      id: nextId(),
      t: nowStamp(),
      agent: newAgent.id,
      text: `${newAgent.name} joined the team`,
      k: "ok",
    });
    syncAgentToServer(live);
  };

  // Push an agent config to the runtime server. Reads company name + voice
  // rules from localStorage so the server's system prompt has real content
  // rather than the bare architect output.
  const syncAgentToServer = (agent) => {
    const SERVER_URL = (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) || "";
    if (!SERVER_URL) return;
    let profile = {}, voice = {};
    try { profile = JSON.parse(localStorage.getItem("citrus_profile") || "{}"); } catch {}
    try { voice   = JSON.parse(localStorage.getItem("citrus_voice")   || "{}"); } catch {}
    const payload = {
      id: agent.id,
      name: agent.name,
      role: agent.role,
      brief: agent.brief,
      tools: agent.tools,
      personality: agent.personality,
      approvalGate: agent.approvalGate,
      quietHours: agent.quietHours || "",
      escalateTo: agent.escalateTo || "",
      channels: agent.channels || {},
      company: profile.name || "the business",
      voice: voice.style || "Warm, direct, short sentences.",
      avoid: voice.avoid || "",
    };
    fetch(`${SERVER_URL}/agents`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    }).then((r) => {
      if (!r.ok) throw new Error(`status ${r.status}`);
      window.toast && window.toast(`${agent.name} synced to runtime server`, "good");
    }).catch((err) => {
      window.toast && window.toast(`Couldn't reach runtime server (${String(err.message || err).slice(0, 60)})`, "warn");
    });
  };

  const focusedAgent = focused ? agents.find((a) => a.id === focused) : null;
  const Section = SECTIONS[tab];

  // Section components receive whatever props they need; extras are ignored.
  const sectionProps = {
    agents,
    activity,
    threads,
    conflicts,
    globalLearnings,
    setThreads,
    onOpen:  setFocused,
    onNew:   openArchitect,
    onRate:  updateRating,
    onChat:  (id) => setChatId(id),
    onHire:  openArchitect,
    onSwitchTab: (t) => { setTab(t); setFocused(null); },
    onCountChange: setApprovalCount,
    onEnvsChange: setEnvs,
  };

  return (
    <div className={`dash${mobileNavOpen ? " dash-nav-open" : ""}`}>
      {mobileNavOpen ? (
        <div className="dash-nav-backdrop" onClick={() => setMobileNavOpen(false)} />
      ) : null}
      <Sidebar
        tab={tab}
        setTab={(t) => { setTab(t); setFocused(null); }}
        onNew={() => setArchOpen(true)}
        inboxBadge={threads.filter((t) => t.status === "needs-you" && t.unread).length}
        approvalsBadge={approvalCount}
      />
      <div className="dash-main">
        <Topbar
          tab={tab}
          focused={focusedAgent}
          agents={agents}
          onBack={() => setFocused(null)}
          onNew={() => setArchOpen(true)}
          onStartTour={() => setTourOpen(true)}
          onOpenAgent={(id) => setFocused(id)}
          onSwitchTab={(t) => { setTab(t); setFocused(null); }}
          onMenu={() => setMobileNavOpen((v) => !v)}
          activeEnvName={window.__citrusActiveEnv ? window.__citrusActiveEnv.name : null}
          envs={envs}
          activeEnvId={activeEnvId}
          authUser={authUser}
          onLogout={onLogout}
        />
        <div className="dash-body">
          {focusedAgent
            ? <window.AgentDetail
                agent={focusedAgent}
                activity={activity}
                threads={threads}
                setThreads={setThreads}
                agentLogs={agentLogs[focusedAgent.id] || []}
                conflicts={conflicts}
                onRate={(r) => updateRating(focusedAgent.id, r)}
                onUpdate={(patch) => updateAgent(focusedAgent.id, patch)}
              />
            : Section ? <Section {...sectionProps} /> : null}
        </div>
      </div>

      <ArchitectModal
        open={archOpen}
        preset={archPreset}
        onClose={() => { setArchOpen(false); setArchPreset(null); }}
        onCreate={addAgent}
      />
      <ChatDrawer
        agent={chatId ? agents.find(a => a.id === chatId) : null}
        threads={threads}
        onClose={() => setChatId(null)}
      />
      <window.Walkthrough
        open={tourOpen}
        onClose={() => {
          setTourOpen(false);
          // Remember dismissal so the auto-open trigger doesn't reopen on
          // every refresh. Per-user, so signing in as a different tenant
          // owner still gets their own first-run tour.
          try {
            const me = window.__citrusUser || {};
            localStorage.setItem(`citrus_tour_dismissed:${me.email || "anon"}`, new Date().toISOString());
          } catch {}
        }}
        onSwitchTab={(t) => { setTab(t); setFocused(null); }}
      />
      {window.OnboardingWizard && (
        <window.OnboardingWizard
          open={onboardingOpen}
          onClose={() => {
            setOnboardingOpen(false);
            // Per-user dismissal — same pattern as the tour. Re-run from
            // Settings → Danger zone if the owner wants to redo it.
            try {
              const me = window.__citrusUser || {};
              localStorage.setItem(`citrus_onboarding_dismissed:${me.email || "anon"}`, new Date().toISOString());
            } catch {}
          }}
          onComplete={() => {
            setOnboardingOpen(false);
            // Same dismissal flag — server has the canonical
            // onboardingCompletedAt timestamp so /onboarding/status
            // returns needs:false on the next mount.
            try {
              const me = window.__citrusUser || {};
              localStorage.setItem(`citrus_onboarding_dismissed:${me.email || "anon"}`, new Date().toISOString());
            } catch {}
            // Switch to Agents tab so the "Hire your first agent" CTA
            // is immediately in view, now with the saved profile.
            setTab("agents");
          }}
        />
      )}
      <window.ToastHost />
    </div>
  );
}

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