import { useState, useRef, useEffect, type KeyboardEvent, type MutableRefObject } from 'react';
import { createPortal } from 'react-dom';
import { ArrowRight, ArrowLeft, LoaderCircle, ExternalLink, ClipboardPaste, RefreshCw, Check, ChevronDown, Mic, Eye, EyeOff, Shield, ShieldCheck, ShieldOff, Copy, Smartphone, Globe, Wifi, WifiOff, TriangleAlert, X, Plus, KeyRound, Activity, Moon, Save, CalendarClock, Clock, Zap, FileText, Pause, Play, Trash2, Download, Info } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { authFetch } from './src/lib/auth';

/* ── Access detection ── */

type AccessMethod = 'tailscale' | 'lan' | 'localhost' | 'tunnel' | 'relay' | 'custom-domain';

function detectAccessMethod(hostname: string): AccessMethod {
  // Tailscale CGNAT range: 100.64.0.0 – 100.127.255.255
  const tailscaleMatch = hostname.match(/^100\.(\d+)\./);
  if (tailscaleMatch && +tailscaleMatch[1] >= 64 && +tailscaleMatch[1] <= 127) return 'tailscale';

  // LAN ranges
  if (/^192\.168\./.test(hostname) || /^10\./.test(hostname)) return 'lan';
  const m172 = hostname.match(/^172\.(\d+)\./);
  if (m172 && +m172[1] >= 16 && +m172[1] <= 31) return 'lan';

  // Localhost
  if (hostname === 'localhost' || hostname === '127.0.0.1') return 'localhost';

  // Cloudflare quick tunnel
  if (hostname.endsWith('.trycloudflare.com')) return 'tunnel';

  // Relay domain
  if (hostname.endsWith('.bloby.bot')) return 'relay';

  // Anything else is a custom domain (named tunnel)
  return 'custom-domain';
}

function isPrivateAccess(method: AccessMethod): boolean {
  return method === 'tailscale' || method === 'lan' || method === 'localhost';
}

const ACCESS_LABELS: Record<AccessMethod, string> = {
  tailscale: 'Tailscale',
  lan: 'Local network',
  localhost: 'Localhost',
  tunnel: 'Cloudflare tunnel',
  relay: 'Relay',
  'custom-domain': 'Custom domain',
};

/* ── Provider config ── */

// `iconHeight` is per-logo because each brand mark has different internal
// padding — Codex's glyph is small inside its viewBox, Pi has tight margins,
// etc. Tweak this number to optically balance the four logos at the same
// visual weight. Default base is 30px; pass undefined to inherit it.
const BASE_ICON_HEIGHT = 30;
const PROVIDERS = [
  { id: 'bloby',     name: 'Bloby',  subtitle: 'Coming Soon..',         icon: '/morphy.png',        comingSoon: true,  iconHeight: 30 },
  { id: 'anthropic', name: 'Claude', subtitle: 'By\nAnthropic',         icon: '/icons/claude.png', comingSoon: false, iconHeight: 30 },
  { id: 'openai',    name: 'Codex',  subtitle: 'By\nOpenAI',            icon: '/codex.svg',        comingSoon: false, iconHeight: 34 },
  { id: 'pi',        name: 'Pi',     subtitle: 'Bring your\nown model', icon: '/pi-logo.svg',      comingSoon: false, iconHeight: 33 },
] as const;

const MODELS: Record<string, { id: string; label: string }[]> = {
  anthropic: [
    { id: 'claude-opus-4-8[1m]', label: 'Opus 4.8 (1M context)' },
    { id: 'claude-opus-4-8', label: 'Opus 4.8' },
    { id: 'claude-opus-4-7[1m]', label: 'Opus 4.7 (1M context)' },
    { id: 'claude-opus-4-7', label: 'Opus 4.7' },
    { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6 (1M context)' },
    { id: 'claude-haiku-4-5', label: 'Haiku 4.5' },
  ],
  openai: [
    { id: 'gpt-5.5:high', label: 'GPT-5.5 High' },
    { id: 'gpt-5.5:medium', label: 'GPT-5.5 Medium' },
    { id: 'gpt-5.5:xhigh', label: 'GPT-5.5 Extra High' },
    { id: 'gpt-5.4:medium', label: 'GPT-5.4 Medium' },
    { id: 'gpt-5.4:high', label: 'GPT-5.4 High' },
    { id: 'gpt-5.4:xhigh', label: 'GPT-5.4 Extra High' },
    { id: 'gpt-5.4-mini:medium', label: 'GPT-5.4-Mini Medium' },
    { id: 'gpt-5.4-mini:high', label: 'GPT-5.4-Mini High' },
    { id: 'gpt-5.4-mini:xhigh', label: 'GPT-5.4-Mini Extra High' },
  ],
};

// TOTAL_STEPS is dynamic — set inside the component based on isInitialSetup

const HANDLES = [
  { tier: 'at', prefix: 'open.bloby.bot/', label: (n: string) => `open.bloby.bot/${n}`, badge: 'Free', badgeCls: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20', highlight: false },
  { tier: 'premium', prefix: 'bloby.bot/', label: (n: string) => `bloby.bot/${n}`, badge: '$5', badgeCls: 'bg-[#0069FE]/15 text-[#0069FE] border-[#0069FE]/20', highlight: true },
] as const;

/* ── Dropdown ── */

function ModelDropdown({ models, value, onChange, placeholder = 'Choose a model...', menuMaxPx = 320 }: { models: { id: string; label: string }[]; value: string; onChange: (id: string) => void; placeholder?: string; menuMaxPx?: number }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState<{ left: number; width: number; top?: number; bottom?: number; maxHeight: number } | null>(null);
  const btnRef = useRef<HTMLButtonElement>(null);
  const menuRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;
    const recalc = () => {
      const r = btnRef.current?.getBoundingClientRect();
      if (!r) return;
      const margin = 8;
      const spaceBelow = window.innerHeight - r.bottom - margin;
      const spaceAbove = r.top - margin;
      // Flip the menu upward when there isn't enough room below and there's more above; either way
      // cap its height to the space available so it never spills past the viewport edge.
      const openUp = spaceBelow < Math.min(menuMaxPx, 220) && spaceAbove > spaceBelow;
      const maxHeight = Math.max(120, Math.min(menuMaxPx, openUp ? spaceAbove : spaceBelow));
      setPos(openUp
        ? { left: r.left, width: r.width, bottom: window.innerHeight - r.top + 4, maxHeight }
        : { left: r.left, width: r.width, top: r.bottom + 4, maxHeight });
    };
    recalc();
    const handler = (e: MouseEvent) => {
      const t = e.target as Node;
      if (btnRef.current?.contains(t)) return;
      if (menuRef.current?.contains(t)) return;
      setOpen(false);
    };
    document.addEventListener('mousedown', handler);
    window.addEventListener('resize', recalc);
    window.addEventListener('scroll', recalc, true);
    return () => {
      document.removeEventListener('mousedown', handler);
      window.removeEventListener('resize', recalc);
      window.removeEventListener('scroll', recalc, true);
    };
  }, [open]);

  const selected = models.find((m) => m.id === value);

  return (
    <div className="relative min-w-0">
      <button
        ref={btnRef}
        type="button"
        onClick={() => setOpen((o) => !o)}
        className="w-full flex items-center justify-between gap-2 bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none hover:border-white/15 focus:border-[#0069FE]/30 transition-colors overflow-hidden"
      >
        <span className={`min-w-0 truncate text-left ${selected ? 'text-white' : 'text-white/20'}`}>
          {selected ? selected.label : placeholder}
        </span>
        <ChevronDown className={`h-4 w-4 shrink-0 text-white/30 transition-transform ${open ? 'rotate-180' : ''}`} />
      </button>
      {open && pos && createPortal(
        <div
          ref={menuRef}
          style={{ position: 'fixed', left: pos.left, width: pos.width, zIndex: 1000, maxHeight: pos.maxHeight, ...(pos.top !== undefined ? { top: pos.top } : { bottom: pos.bottom }) }}
          className="bg-[#222] border border-white/[0.08] rounded-xl shadow-xl py-1 overflow-y-auto"
        >
          {models.map((m) => (
            <button
              key={m.id}
              onClick={() => { onChange(m.id); setOpen(false); }}
              className={`w-full text-left px-4 py-2 text-[13px] transition-colors ${
                value === m.id
                  ? 'text-[#0069FE] bg-[#0069FE]/10'
                  : 'text-white/70 hover:bg-white/[0.04] hover:text-white'
              }`}
            >
              {m.label}
            </button>
          ))}
        </div>,
        document.body,
      )}
    </div>
  );
}

/* ── Settings mode: screen registry + jump menu ── */

// The jumpable settings screens (settings re-run only). `step` maps to the wizard step index.
// Environment Variables (step 6) reuses the slot the onboarding-only "All Set" screen occupies.
const SETTINGS_SCREENS: { step: number; label: string }[] = [
  { step: 1, label: 'Personal Info' },
  { step: 2, label: 'Agent Name & Access' },
  { step: 3, label: 'Security' },
  { step: 4, label: 'AI Provider' },
  { step: 5, label: 'Voice Messages' },
  { step: 6, label: 'Environment Variables' },
  { step: 7, label: 'Pulse & Crons' },
];

// Compact "Go to" dropdown shown in the settings header so the user can jump to any screen.
function GoToMenu({ onJump }: { onJump: (step: number) => void }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState<{ right: number; top?: number; bottom?: number; maxHeight: number } | null>(null);
  const btnRef = useRef<HTMLButtonElement>(null);
  const menuRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;
    const recalc = () => {
      const r = btnRef.current?.getBoundingClientRect();
      if (!r) return;
      const margin = 8;
      const spaceBelow = window.innerHeight - r.bottom - margin;
      const spaceAbove = r.top - margin;
      const openUp = spaceBelow < Math.min(188, 220) && spaceAbove > spaceBelow;
      const maxHeight = Math.max(120, Math.min(188, openUp ? spaceAbove : spaceBelow));
      setPos(openUp
        ? { right: window.innerWidth - r.right, bottom: window.innerHeight - r.top + 6, maxHeight }
        : { right: window.innerWidth - r.right, top: r.bottom + 6, maxHeight });
    };
    recalc();
    const handler = (e: MouseEvent) => {
      const t = e.target as Node;
      if (btnRef.current?.contains(t) || menuRef.current?.contains(t)) return;
      setOpen(false);
    };
    document.addEventListener('mousedown', handler);
    window.addEventListener('resize', recalc);
    window.addEventListener('scroll', recalc, true);
    return () => {
      document.removeEventListener('mousedown', handler);
      window.removeEventListener('resize', recalc);
      window.removeEventListener('scroll', recalc, true);
    };
  }, [open]);

  return (
    <>
      <button
        ref={btnRef}
        type="button"
        onClick={() => setOpen((o) => !o)}
        className="flex items-center gap-1 text-white/50 hover:text-white/80 text-[12px] font-medium px-2.5 py-1 rounded-lg hover:bg-white/[0.05] transition-colors"
      >
        Go to
        <ChevronDown className={`h-3.5 w-3.5 transition-transform ${open ? 'rotate-180' : ''}`} />
      </button>
      {open && pos && createPortal(
        <div
          ref={menuRef}
          style={{ position: 'fixed', right: pos.right, zIndex: 1000, maxHeight: pos.maxHeight, ...(pos.top !== undefined ? { top: pos.top } : { bottom: pos.bottom }) }}
          className="bg-[#222] border border-white/[0.08] rounded-xl shadow-xl py-1 min-w-[200px] overflow-y-auto"
        >
          {SETTINGS_SCREENS.map((s) => (
            <button
              key={s.step}
              onClick={() => { onJump(s.step); setOpen(false); }}
              className="w-full text-left px-4 py-2 text-[13px] text-white/70 hover:bg-white/[0.04] hover:text-white transition-colors"
            >
              {s.label}
            </button>
          ))}
        </div>,
        document.body,
      )}
    </>
  );
}

/* ── Settings mode: Environment Variables screen ── */

interface EnvVarRow { name: string; value: string }
interface EnvVarGroup { title: string; vars: EnvVarRow[] }
interface EnvCache {
  groups: EnvVarGroup[];
  values: Record<string, string>;
  originals: Record<string, string>;
  revealed: string[];
  removed: string[];
  newRows: { id: number; name: string; value: string }[];
  newRowSeq: number;
  saved: boolean;
}

const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;

// A read/write view of the agent-controlled `workspace/.env`, grouped by `# Section` headers.
// Reads via GET /api/env, writes changed/added keys via POST /api/env (which restarts the backend).
// Self-saving and self-contained — not part of the wizard's batched onboard payload.
// `cacheRef` lets the working state survive this screen's unmount/remount when the user
// navigates to another settings screen and back (the wizard keys each step's motion.div by
// `step`, so it remounts). It lives in the parent and dies with the wizard, so each fresh
// open re-fetches from /api/env.
function EnvSettings({ cacheRef }: { cacheRef: MutableRefObject<EnvCache | null> }) {
  // Whether we already have cached working state at mount — captured once per instance.
  const hadCache = useRef(cacheRef.current != null).current;
  const cached = cacheRef.current;
  const [groups, setGroups] = useState<EnvVarGroup[]>(cached?.groups ?? []);
  const [loading, setLoading] = useState(!hadCache);
  const [loadError, setLoadError] = useState('');
  const [values, setValues] = useState<Record<string, string>>(cached?.values ?? {});
  const [originals, setOriginals] = useState<Record<string, string>>(cached?.originals ?? {});
  const [revealed, setRevealed] = useState<Set<string>>(new Set(cached?.revealed ?? []));
  const [removed, setRemoved] = useState<Set<string>>(new Set(cached?.removed ?? []));
  const [newRows, setNewRows] = useState<{ id: number; name: string; value: string }[]>(cached?.newRows ?? []);
  const newRowId = useRef(cached?.newRowSeq ?? 0);
  const [saving, setSaving] = useState(false);
  const [saved, setSaved] = useState(cached?.saved ?? false);
  const [saveError, setSaveError] = useState('');

  useEffect(() => {
    if (hadCache) return; // hydrated from cache — don't clobber edits with a refetch
    let cancelled = false;
    authFetch('/api/env')
      .then((r) => (r.ok ? r.json() : Promise.reject(new Error('Failed to load environment variables'))))
      .then((data) => {
        if (cancelled) return;
        const gs: EnvVarGroup[] = Array.isArray(data.groups) ? data.groups : [];
        const init: Record<string, string> = {};
        for (const g of gs) for (const v of g.vars) init[v.name] = v.value;
        setGroups(gs);
        setValues(init);
        setOriginals(init);
        setLoading(false);
      })
      .catch((err) => { if (!cancelled) { setLoadError(err.message || 'Failed to load'); setLoading(false); } });
    return () => { cancelled = true; };
  }, [hadCache]);

  // Persist working state to the parent ref so navigating away and back doesn't lose edits.
  // Skip while loading so a navigate-away mid-fetch leaves the cache empty → refetch on return.
  useEffect(() => {
    if (loading) return;
    cacheRef.current = { groups, values, originals, revealed: Array.from(revealed), removed: Array.from(removed), newRows, newRowSeq: newRowId.current, saved };
  });

  const validNewRows = newRows.filter((r) => r.name.trim() && r.value.trim() && ENV_NAME_RE.test(r.name.trim()));
  const removedExisting = [...removed].filter((k) => k in originals);
  // An edit to a var that's also marked for removal doesn't count — removal wins.
  const changedExisting = Object.keys(values).filter((k) => values[k] !== originals[k] && !removed.has(k));
  const dirty = changedExisting.length > 0 || validNewRows.length > 0 || removedExisting.length > 0;

  const toggleReveal = (name: string) => {
    setRevealed((prev) => {
      const next = new Set(prev);
      if (next.has(name)) next.delete(name); else next.add(name);
      return next;
    });
  };

  const toggleRemoved = (name: string) => {
    setSaved(false);
    setRemoved((prev) => {
      const next = new Set(prev);
      if (next.has(name)) next.delete(name); else next.add(name);
      return next;
    });
  };

  const handleSave = async () => {
    if (!dirty || saving) return;
    setSaving(true); setSaveError(''); setSaved(false);
    const vars: Record<string, string> = {};
    for (const k of changedExisting) vars[k] = values[k];
    for (const r of validNewRows) vars[r.name.trim()] = r.value.trim();
    const removeList = removedExisting;
    try {
      const res = await authFetch('/api/env', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ vars, remove: removeList }),
      });
      if (!res.ok) {
        const d = await res.json().catch(() => ({ error: 'Save failed' }));
        throw new Error(d.error || 'Save failed');
      }
      // Snapshot of values including the just-added rows and minus the removed ones, so `dirty`
      // resets after save.
      const merged = { ...values };
      for (const r of validNewRows) merged[r.name.trim()] = r.value.trim();
      for (const k of removeList) delete merged[k];
      // Drop removed vars from the displayed groups; surface newly-added vars under a "Custom" group
      // (the writer appends them; on next load they'll re-group under whatever section precedes them).
      setGroups((prev) => {
        let next = prev.map((g) => ({ ...g, vars: g.vars.filter((v) => !removed.has(v.name)) }));
        const existing = new Set(next.flatMap((g) => g.vars.map((v) => v.name)));
        const toAdd = validNewRows.map((r) => r.name.trim()).filter((nm) => !existing.has(nm));
        if (toAdd.length) {
          let custom = next.find((g) => g.title === 'Custom');
          if (!custom) { custom = { title: 'Custom', vars: [] }; next.push(custom); }
          for (const nm of toAdd) custom.vars.push({ name: nm, value: merged[nm] });
        }
        return next.filter((g) => g.vars.length > 0); // hide groups emptied by removal
      });
      setValues(merged);
      setOriginals(merged);
      setNewRows([]);
      setRemoved(new Set());
      setSaved(true);
      setSaving(false);
    } catch (err: any) {
      setSaveError(err.message || 'Save failed');
      setSaving(false);
    }
  };

  return (
    <div>
      <h1 className="text-xl font-bold text-white tracking-tight">Environment Variables</h1>
      <p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
        API keys and secrets stored in your workspace{' '}
        <code className="text-white/55 font-mono text-[12px]">.env</code>. Your agent reads and writes these as you build.
      </p>

      {loading ? (
        <div className="flex items-center justify-center py-12 text-white/40">
          <LoaderCircle className="h-5 w-5 animate-spin" />
        </div>
      ) : loadError ? (
        <div className="mt-5 flex items-center gap-2 text-[13px] text-red-400">
          <TriangleAlert className="h-4 w-4 shrink-0" />{loadError}
        </div>
      ) : (
        <div className="mt-5 space-y-5">
          {groups.length === 0 && newRows.length === 0 && (
            <div className="rounded-xl border border-white/[0.06] bg-white/[0.02] px-4 py-7 text-center">
              <KeyRound className="h-5 w-5 text-white/25 mx-auto mb-2" />
              <p className="text-white/40 text-[13px]">No environment variables yet.</p>
              <p className="text-white/25 text-[12px] mt-1">Ask your agent to add an integration, or add one manually below.</p>
            </div>
          )}

          {groups.map((g) => (
            <div key={g.title || '__untitled'}>
              <div className="flex items-center gap-2 mb-2">
                <KeyRound className="h-3.5 w-3.5 text-[#0069FE]/70" />
                <span className="text-[12px] font-semibold text-white/70 uppercase tracking-wide">{g.title || 'Other'}</span>
              </div>
              <div className="space-y-2.5">
                {g.vars.map((v) => {
                  const isRemoved = removed.has(v.name);
                  const changed = !isRemoved && values[v.name] !== originals[v.name];
                  const show = revealed.has(v.name);
                  return (
                    <div key={v.name}>
                      <label className="flex items-center gap-1.5 text-[11px] text-white/40 mb-1">
                        <code className={`font-mono ${isRemoved ? 'text-white/30 line-through' : 'text-white/50'}`}>{v.name}</code>
                        {changed && <span className="text-[#0069FE] text-[10px] font-medium">• edited</span>}
                        {isRemoved && <span className="text-red-400/80 text-[10px] font-medium">• will be removed</span>}
                      </label>
                      {isRemoved ? (
                        <div className="flex items-center gap-2 px-4 py-2.5 rounded-xl border border-red-500/20 bg-red-500/[0.04]">
                          <span className="flex-1 text-[13px] text-white/30 font-mono">••••••••</span>
                          <button
                            type="button"
                            onClick={() => toggleRemoved(v.name)}
                            className="text-[11px] text-white/50 hover:text-white/80 transition-colors"
                          >
                            Undo
                          </button>
                        </div>
                      ) : (
                        <div className="flex items-center gap-2">
                          <div className="relative flex-1">
                            <input
                              type={show ? 'text' : 'password'}
                              value={values[v.name] ?? ''}
                              onChange={(e) => setValues((p) => ({ ...p, [v.name]: e.target.value }))}
                              autoComplete="off"
                              spellCheck={false}
                              data-1p-ignore
                              data-lpignore="true"
                              className="w-full bg-white/[0.03] border border-white/[0.08] text-white rounded-xl pl-4 pr-10 py-2.5 text-[13px] font-mono outline-none focus:border-[#0069FE]/30 transition-colors"
                            />
                            <button
                              type="button"
                              onClick={() => toggleReveal(v.name)}
                              className="absolute right-3 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors"
                              aria-label={show ? 'Hide value' : 'Reveal value'}
                            >
                              {show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
                            </button>
                          </div>
                          <button
                            type="button"
                            onClick={() => toggleRemoved(v.name)}
                            className="shrink-0 p-2 text-white/25 hover:text-red-400 hover:bg-red-500/[0.06] rounded-lg transition-colors"
                            title="Remove variable"
                            aria-label="Remove variable"
                          >
                            <Trash2 className="h-4 w-4" />
                          </button>
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>
            </div>
          ))}

          {newRows.length > 0 && (
            <div>
              <div className="flex items-center gap-2 mb-2">
                <Plus className="h-3.5 w-3.5 text-emerald-400/70" />
                <span className="text-[12px] font-semibold text-white/70 uppercase tracking-wide">New</span>
              </div>
              <div className="space-y-3">
                {newRows.map((r) => {
                  const nameInvalid = r.name.trim().length > 0 && !ENV_NAME_RE.test(r.name.trim());
                  return (
                    <div key={r.id} className="flex items-start gap-2">
                      <div className="flex-1 space-y-1.5">
                        <input
                          type="text"
                          value={r.name}
                          onChange={(e) => setNewRows((p) => p.map((x) => (x.id === r.id ? { ...x, name: e.target.value } : x)))}
                          placeholder="VARIABLE_NAME"
                          autoComplete="off"
                          spellCheck={false}
                          data-1p-ignore
                          data-lpignore="true"
                          className={`w-full bg-white/[0.03] border ${nameInvalid ? 'border-red-500/40' : 'border-white/[0.08]'} text-white rounded-xl px-4 py-2.5 text-[13px] font-mono outline-none focus:border-[#0069FE]/30 transition-colors`}
                        />
                        <input
                          type="text"
                          value={r.value}
                          onChange={(e) => setNewRows((p) => p.map((x) => (x.id === r.id ? { ...x, value: e.target.value } : x)))}
                          placeholder="value"
                          autoComplete="off"
                          spellCheck={false}
                          data-1p-ignore
                          data-lpignore="true"
                          className="w-full bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] font-mono outline-none focus:border-[#0069FE]/30 transition-colors"
                        />
                        {nameInvalid && (
                          <p className="text-red-400/80 text-[11px]">Use letters, numbers and underscores; can't start with a number.</p>
                        )}
                      </div>
                      <button
                        type="button"
                        onClick={() => setNewRows((p) => p.filter((x) => x.id !== r.id))}
                        className="mt-2 text-white/25 hover:text-red-400/80 transition-colors"
                        aria-label="Remove"
                      >
                        <X className="h-4 w-4" />
                      </button>
                    </div>
                  );
                })}
              </div>
            </div>
          )}

          <button
            type="button"
            onClick={() => setNewRows((p) => [...p, { id: newRowId.current++, name: '', value: '' }])}
            className="flex items-center gap-1.5 text-[12px] text-white/50 hover:text-white/80 transition-colors"
          >
            <Plus className="h-3.5 w-3.5" /> Add new
          </button>
        </div>
      )}

      {dirty && !saving && (
        <div className="mt-4 flex items-start gap-2.5 bg-amber-500/[0.06] border border-amber-500/20 rounded-xl px-4 py-3">
          <TriangleAlert className="h-4 w-4 text-amber-400 mt-0.5 shrink-0" />
          <p className="text-amber-300/80 text-[12px] leading-relaxed">
            Changes detected. Your workspace backend will be restarted when you save.
          </p>
        </div>
      )}

      {saved && !dirty && (
        <div className="mt-4 flex items-center gap-2.5 bg-emerald-500/[0.06] border border-emerald-500/20 rounded-xl px-4 py-3">
          <Check className="h-4 w-4 text-emerald-400 shrink-0" />
          <p className="text-emerald-300/90 text-[12px]">Saved — your workspace backend is restarting.</p>
        </div>
      )}

      {saveError && (
        <div className="mt-3 flex items-center gap-1.5 text-[12px] text-red-400">
          <TriangleAlert className="h-3.5 w-3.5 shrink-0" />{saveError}
        </div>
      )}

      <button
        onClick={handleSave}
        disabled={!dirty || saving}
        className="w-full mt-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40 disabled:cursor-not-allowed"
      >
        {saving ? (
          <><LoaderCircle className="h-4 w-4 animate-spin" />Saving...</>
        ) : (
          <><RefreshCw className="h-4 w-4" />Save &amp; restart backend</>
        )}
      </button>
    </div>
  );
}

/* ── Settings mode: Pulse & Crons screen ── */

interface PulseConfig {
  enabled: boolean;
  intervalMinutes: number;
  quietHours: { start: string; end: string };
}

interface CronView {
  id: string;
  schedule: string;
  task: string;
  oneShot?: boolean;
  paused?: boolean;
  hasTaskFile: boolean;
  nextRun: string | null; // ISO
  description: string;    // humanized schedule
}

interface PulseCronCache { pulse: PulseConfig; original: PulseConfig | null; crons: CronView[] }

const DEFAULT_PULSE: PulseConfig = { enabled: false, intervalMinutes: 60, quietHours: { start: '22:00', end: '07:00' } };

// 8 slider stops; the last (-1) is the "Custom" sentinel.
const PULSE_STOPS: { mins: number; label: string }[] = [
  { mins: 10, label: '10m' }, { mins: 30, label: '30m' }, { mins: 60, label: '1h' },
  { mins: 120, label: '2h' }, { mins: 180, label: '3h' }, { mins: 360, label: '6h' },
  { mins: 720, label: '12h' }, { mins: -1, label: 'Custom' },
];
const PULSE_CUSTOM_INDEX = PULSE_STOPS.length - 1;
const PULSE_TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;

function pulseIndexForMinutes(mins: number): number {
  const i = PULSE_STOPS.findIndex((s) => s.mins === mins);
  return i === -1 ? PULSE_CUSTOM_INDEX : i;
}

function humanizeInterval(mins: number): string {
  if (!Number.isFinite(mins) || mins <= 0) return 'Every —';
  if (mins % 60 === 0) { const h = mins / 60; return `Every ${h} hour${h === 1 ? '' : 's'}`; }
  if (mins < 60) return `Every ${mins} minute${mins === 1 ? '' : 's'}`;
  return `Every ${Math.floor(mins / 60)}h ${mins % 60}m`;
}

// nextRun ISO → short human string ("in 12m", "Today 3:00 PM", "Tomorrow 9:00 AM", "Jun 9, 3:00 PM").
function formatNextRun(iso: string | null): string | null {
  if (!iso) return null;
  const d = new Date(iso);
  if (isNaN(d.getTime())) return null;
  const now = new Date();
  const diffMs = d.getTime() - now.getTime();
  const time = d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
  if (diffMs > 0 && diffMs < 60 * 60 * 1000) return `in ${Math.max(1, Math.round(diffMs / 60000))}m`;
  if (diffMs > 0 && diffMs < 6 * 60 * 60 * 1000) return `in ${Math.round(diffMs / 3_600_000)}h`;
  const startOfDay = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate());
  const dayDiff = Math.round((startOfDay(d).getTime() - startOfDay(now).getTime()) / 86_400_000);
  if (dayDiff === 0) return `Today ${time}`;
  if (dayDiff === 1) return `Tomorrow ${time}`;
  if (dayDiff > 1 && dayDiff < 7) return `${d.toLocaleDateString([], { weekday: 'long' })} ${time}`;
  return `${d.toLocaleDateString([], { month: 'short', day: 'numeric' })}, ${time}`;
}

function PulseSection({ pulse, original, setPulse, onPulseSaved }: {
  pulse: PulseConfig;
  original: PulseConfig | null;
  setPulse: (next: PulseConfig) => void;
  onPulseSaved: (saved: PulseConfig) => void;
}) {
  const [saving, setSaving] = useState(false);
  const [saved, setSaved] = useState(false);
  const [saveError, setSaveError] = useState('');
  // Explicit "Custom" mode — NOT derived from the value, otherwise typing a value that happens to
  // equal a preset (e.g. 60) would snap the slider and unmount the input mid-keystroke. Seeded from
  // whether the loaded interval is a non-preset value.
  const [customMode, setCustomMode] = useState(pulseIndexForMinutes(pulse.intervalMinutes) === PULSE_CUSTOM_INDEX);

  const presetIdx = pulseIndexForMinutes(pulse.intervalMinutes);
  const onCustom = customMode;
  const sliderIdx = customMode ? PULSE_CUSTOM_INDEX : presetIdx;
  const quietValid = PULSE_TIME_RE.test(pulse.quietHours.start) && PULSE_TIME_RE.test(pulse.quietHours.end);
  const intervalValid = Number.isFinite(pulse.intervalMinutes) && pulse.intervalMinutes >= 1 && pulse.intervalMinutes <= 1440;
  const dirty = original != null && (
    pulse.enabled !== original.enabled ||
    pulse.intervalMinutes !== original.intervalMinutes ||
    pulse.quietHours.start !== original.quietHours.start ||
    pulse.quietHours.end !== original.quietHours.end
  );
  const canSave = dirty && intervalValid && quietValid && !saving;

  const patch = (p: Partial<PulseConfig>) => {
    if (saved) setSaved(false);
    if (saveError) setSaveError('');
    setPulse({ ...pulse, ...p });
  };

  const onSlider = (idx: number) => {
    if (idx === PULSE_CUSTOM_INDEX) {
      setCustomMode(true);
      // Keep the current value when already custom; seed 45 only when entering from a preset.
      const seed = (customMode || presetIdx === PULSE_CUSTOM_INDEX) ? pulse.intervalMinutes : 45;
      patch({ intervalMinutes: seed });
    } else {
      setCustomMode(false);
      patch({ intervalMinutes: PULSE_STOPS[idx].mins });
    }
  };

  const handleSave = async () => {
    if (!canSave) return;
    setSaving(true); setSaveError(''); setSaved(false);
    try {
      const res = await authFetch('/api/pulse', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ enabled: pulse.enabled, intervalMinutes: pulse.intervalMinutes, quietHours: pulse.quietHours }),
      });
      if (!res.ok) { const d = await res.json().catch(() => ({ error: 'Save failed' })); throw new Error(d.error || 'Save failed'); }
      onPulseSaved(pulse);
      setSaved(true);
      setSaving(false);
    } catch (err: any) {
      setSaveError(err?.message || 'Save failed');
      setSaving(false);
    }
  };

  return (
    <div>
      <div className="flex items-center gap-3">
        <div className="w-9 h-9 rounded-xl bg-[#0069FE]/10 flex items-center justify-center shrink-0">
          <Activity className="h-[18px] w-[18px] text-[#0069FE]" />
        </div>
        <div className="flex-1 min-w-0">
          <h2 className="text-[15px] font-bold text-white tracking-tight">Pulse</h2>
        </div>
        <button
          type="button" role="switch" aria-checked={pulse.enabled} aria-label="Enable pulse"
          onClick={() => patch({ enabled: !pulse.enabled })}
          className={`w-10 h-[22px] rounded-full transition-colors duration-200 flex items-center px-0.5 shrink-0 ${pulse.enabled ? 'bg-gradient-brand' : 'bg-white/[0.08]'}`}
        >
          <div className={`w-[18px] h-[18px] rounded-full bg-white shadow-sm transition-transform duration-200 ${pulse.enabled ? 'translate-x-[18px]' : 'translate-x-0'}`} />
        </button>
      </div>

      <p className="text-white/40 text-[13px] mt-2 leading-relaxed">
        Pulse periodically wakes the agent inside the main session, letting it surface anything that needs attention without spamming you.
      </p>

      <div className={`mt-5 transition-opacity duration-200 ${pulse.enabled ? '' : 'opacity-50 pointer-events-none select-none'}`} aria-disabled={!pulse.enabled}>
        <div className="flex items-center justify-between mb-2.5">
          <span className="text-[12px] font-semibold text-white/70 uppercase tracking-wide">Frequency</span>
          <span className="text-[11px] font-medium text-[#0069FE] bg-[#0069FE]/10 rounded-full px-2.5 py-1">
            {onCustom ? 'Custom interval' : humanizeInterval(pulse.intervalMinutes)}
          </span>
        </div>

        <input
          type="range" min={0} max={PULSE_CUSTOM_INDEX} step={1} value={sliderIdx}
          onChange={(e) => onSlider(Number(e.target.value))} aria-label="Pulse interval"
          className="w-full h-1.5 appearance-none rounded-full bg-white/[0.08] accent-[#0069FE] cursor-pointer
                     [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4
                     [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white
                     [&::-webkit-slider-thumb]:shadow-[0_0_0_3px_rgba(0,105,254,0.35)] [&::-webkit-slider-thumb]:cursor-pointer
                     [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full
                     [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:shadow-[0_0_0_3px_rgba(0,105,254,0.35)]"
        />

        <div className="flex justify-between mt-2 px-[1px]">
          {PULSE_STOPS.map((s, i) => (
            <button
              key={s.label} type="button" onClick={() => onSlider(i)}
              className={`text-[10px] leading-none transition-colors ${i === sliderIdx ? 'text-[#0069FE] font-semibold' : 'text-white/30 hover:text-white/55'}`}
            >
              {s.label}
            </button>
          ))}
        </div>

        {onCustom && (
          <div className="mt-3 flex items-center gap-2.5">
            <input
              type="number" min={1} max={1440}
              value={Number.isFinite(pulse.intervalMinutes) ? pulse.intervalMinutes : ''}
              onChange={(e) => { const n = parseInt(e.target.value, 10); patch({ intervalMinutes: Number.isFinite(n) ? n : NaN }); }}
              placeholder="45"
              className="w-28 bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none focus:border-[#0069FE]/30 transition-colors placeholder:text-white/20 font-mono"
            />
            <span className="text-[12px] text-white/40">minutes between pulses</span>
          </div>
        )}
        {onCustom && !intervalValid && (
          <p className="text-amber-400/80 text-[11px] mt-1.5">Enter a whole number of minutes (1–1440).</p>
        )}

        <div className="flex items-center gap-2 mt-6 mb-2.5">
          <Moon className="h-3.5 w-3.5 text-[#0069FE]/70" />
          <span className="text-[12px] font-semibold text-white/70 uppercase tracking-wide">Quiet hours</span>
        </div>
        <div className="flex items-center gap-3">
          <div className="flex-1">
            <label className="text-[11px] text-white/40 mb-1 block">Start</label>
            <input
              type="time" value={pulse.quietHours.start}
              onChange={(e) => patch({ quietHours: { ...pulse.quietHours, start: e.target.value } })}
              className="w-full bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none focus:border-[#0069FE]/30 transition-colors [color-scheme:dark]"
            />
          </div>
          <ArrowRight className="h-4 w-4 text-white/25 mt-5 shrink-0" />
          <div className="flex-1">
            <label className="text-[11px] text-white/40 mb-1 block">End</label>
            <input
              type="time" value={pulse.quietHours.end}
              onChange={(e) => patch({ quietHours: { ...pulse.quietHours, end: e.target.value } })}
              className="w-full bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none focus:border-[#0069FE]/30 transition-colors [color-scheme:dark]"
            />
          </div>
        </div>
        <p className="text-white/35 text-[12px] mt-2 leading-relaxed">No pulses fire between these times.</p>
      </div>

      {saved && !dirty && (
        <div className="mt-4 flex items-center gap-2.5 bg-emerald-500/[0.06] border border-emerald-500/20 rounded-xl px-4 py-3">
          <Check className="h-4 w-4 text-emerald-400 shrink-0" />
          <p className="text-emerald-300/90 text-[12px]">Saved — the scheduler picks this up within a minute.</p>
        </div>
      )}
      {saveError && (
        <div className="mt-3 flex items-center gap-1.5 text-[12px] text-red-400">
          <TriangleAlert className="h-3.5 w-3.5 shrink-0" />{saveError}
        </div>
      )}
      {dirty && (
        <button
          type="button" onClick={handleSave} disabled={!canSave}
          className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40 disabled:cursor-not-allowed"
        >
          {saving ? (<><LoaderCircle className="h-4 w-4 animate-spin" />Saving...</>) : (<><Save className="h-4 w-4" />Save pulse settings</>)}
        </button>
      )}
    </div>
  );
}

function CronsSection({ crons, onChanged, onLocalUpdate }: {
  crons: CronView[];
  onChanged: () => void;
  onLocalUpdate: (fn: (prev: CronView[]) => CronView[]) => void;
}) {
  const [busy, setBusy] = useState<Record<string, 'pause' | 'delete' | undefined>>({});
  const [confirmId, setConfirmId] = useState<string | null>(null);
  const [downloading, setDownloading] = useState<Record<string, boolean>>({});
  const [actionError, setActionError] = useState('');
  const [expanded, setExpanded] = useState<Set<string>>(new Set()); // collapsed by default

  const setRowBusy = (id: string, v: 'pause' | 'delete' | undefined) => setBusy((p) => ({ ...p, [id]: v }));
  const toggleExpanded = (id: string) => setExpanded((prev) => {
    const next = new Set(prev);
    if (next.has(id)) next.delete(id); else next.add(id);
    return next;
  });

  const handleTogglePause = async (c: CronView) => {
    if (busy[c.id]) return;
    setActionError(''); setRowBusy(c.id, 'pause');
    try {
      const res = await authFetch('/api/crons/pause', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id: c.id, paused: !c.paused }),
      });
      if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Failed to update'); }
      // Optimistic: reflect the change immediately so a failed background refetch can't leave a
      // stale row; onChanged() then reconciles against the on-disk truth.
      onLocalUpdate((prev) => prev.map((x) => (x.id === c.id ? { ...x, paused: !c.paused } : x)));
      onChanged();
    } catch (err: any) {
      setActionError(err.message || 'Failed to update');
    } finally {
      setRowBusy(c.id, undefined);
    }
  };

  const handleDelete = async (c: CronView) => {
    if (busy[c.id]) return;
    setActionError(''); setRowBusy(c.id, 'delete');
    try {
      const res = await authFetch('/api/crons/delete', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id: c.id }),
      });
      if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Failed to delete'); }
      setConfirmId(null);
      onLocalUpdate((prev) => prev.filter((x) => x.id !== c.id));
      onChanged();
    } catch (err: any) {
      setActionError(err.message || 'Failed to delete');
    } finally {
      setRowBusy(c.id, undefined);
    }
  };

  // Authed download: the endpoint requires a Bearer token, so a plain <a href> would 401.
  const handleDownloadTask = async (c: CronView) => {
    if (downloading[c.id]) return;
    setActionError(''); setDownloading((p) => ({ ...p, [c.id]: true }));
    try {
      const res = await authFetch(`/api/crons/task?id=${encodeURIComponent(c.id)}`);
      if (!res.ok) throw new Error('Could not download task file');
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url; a.download = `${c.id}.md`;
      document.body.appendChild(a); a.click(); a.remove();
      URL.revokeObjectURL(url);
    } catch (err: any) {
      setActionError(err.message || 'Download failed');
    } finally {
      setDownloading((p) => ({ ...p, [c.id]: false }));
    }
  };

  return (
    <div className="mt-8">
      <div className="flex items-center gap-2">
        <CalendarClock className="h-4 w-4 text-[#0069FE]/70 shrink-0" />
        <h2 className="text-[15px] font-bold text-white tracking-tight">Crons</h2>
      </div>
      <p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
        Crons let your agent schedule tasks inside the main session, running them daily, hourly, or on any schedule you choose. They can also be one-shot tasks that run once at a specific time.
      </p>
      <div className="mt-2 flex items-start gap-1.5 text-white/30 text-[12px] leading-relaxed">
        <Info className="h-3.5 w-3.5 mt-0.5 shrink-0" />
        <span>To change or add a cron, just ask your agent. To delete or pause, use the buttons below.</span>
      </div>

      {actionError && (
        <div className="mt-3 flex items-center gap-1.5 text-[12px] text-red-400">
          <TriangleAlert className="h-3.5 w-3.5 shrink-0" />{actionError}
        </div>
      )}

      <div className="mt-4 space-y-2.5">
        {crons.length === 0 ? (
          <div className="rounded-xl border border-white/[0.06] bg-white/[0.02] px-4 py-7 text-center">
            <CalendarClock className="h-5 w-5 text-white/25 mx-auto mb-2" />
            <p className="text-white/40 text-[13px]">No scheduled tasks yet.</p>
            <p className="text-white/25 text-[12px] mt-1">Ask your agent to schedule something.</p>
          </div>
        ) : (
          crons.map((c) => {
            const paused = !!c.paused;
            const rowBusy = busy[c.id];
            const isOpen = expanded.has(c.id);
            const nextStr = formatNextRun(c.nextRun);
            return (
              <div key={c.id} className={`rounded-xl border transition-colors ${paused ? 'border-white/[0.05] bg-white/[0.015]' : 'border-white/[0.06] bg-white/[0.02]'}`}>
                {/* Header row — title + actions (always visible) */}
                <div className="flex items-center gap-2 p-2.5">
                  <button
                    type="button"
                    onClick={() => toggleExpanded(c.id)}
                    className="flex items-center gap-2.5 flex-1 min-w-0 text-left"
                  >
                    <div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${paused ? 'bg-white/[0.04]' : c.oneShot ? 'bg-amber-500/10' : 'bg-[#0069FE]/10'}`}>
                      {c.oneShot
                        ? <Zap className={`h-4 w-4 ${paused ? 'text-white/40' : 'text-amber-400'}`} />
                        : <Clock className={`h-4 w-4 ${paused ? 'text-white/40' : 'text-[#0069FE]'}`} />}
                    </div>
                    <span className={`flex-1 min-w-0 truncate text-[13px] ${paused ? 'text-white/45' : 'text-white'}`}>{c.task || '(untitled task)'}</span>
                    {paused && (
                      <span className="shrink-0 text-[10px] font-medium text-white/40 bg-white/[0.05] rounded-full px-2 py-0.5">Paused</span>
                    )}
                  </button>

                  {confirmId === c.id ? (
                    <div className="flex items-center gap-1 shrink-0">
                      <span className="text-[11px] text-white/45 mr-0.5">Delete?</span>
                      <button
                        type="button" onClick={() => handleDelete(c)} disabled={rowBusy === 'delete'}
                        className="inline-flex items-center gap-1 rounded-md bg-red-500/15 hover:bg-red-500/25 border border-red-500/30 text-red-300 text-[11px] font-medium px-2 py-1 transition-colors disabled:opacity-50"
                      >
                        {rowBusy === 'delete' ? <LoaderCircle className="h-3 w-3 animate-spin" /> : <Trash2 className="h-3 w-3" />}Delete
                      </button>
                      <button
                        type="button" onClick={() => setConfirmId(null)} disabled={rowBusy === 'delete'}
                        className="rounded-md text-white/40 hover:text-white/70 text-[11px] px-1.5 py-1 transition-colors"
                      >
                        Cancel
                      </button>
                    </div>
                  ) : (
                    <div className="flex items-center gap-0.5 shrink-0">
                      <button
                        type="button" onClick={() => handleTogglePause(c)} disabled={!!rowBusy}
                        title={paused ? 'Resume' : 'Pause'} aria-label={paused ? 'Resume' : 'Pause'}
                        className="p-1.5 rounded-lg text-white/40 hover:text-white/80 hover:bg-white/[0.05] transition-colors disabled:opacity-50"
                      >
                        {rowBusy === 'pause' ? <LoaderCircle className="h-4 w-4 animate-spin" /> : paused ? <Play className="h-4 w-4" /> : <Pause className="h-4 w-4" />}
                      </button>
                      <button
                        type="button" onClick={() => setConfirmId(c.id)} disabled={!!rowBusy}
                        title="Delete" aria-label="Delete"
                        className="p-1.5 rounded-lg text-white/30 hover:text-red-400 hover:bg-red-500/[0.06] transition-colors disabled:opacity-50"
                      >
                        <Trash2 className="h-4 w-4" />
                      </button>
                      <button
                        type="button" onClick={() => toggleExpanded(c.id)}
                        title={isOpen ? 'Collapse' : 'Expand'} aria-label={isOpen ? 'Collapse' : 'Expand'}
                        className="p-1.5 rounded-lg text-white/30 hover:text-white/70 hover:bg-white/[0.05] transition-colors"
                      >
                        <ChevronDown className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
                      </button>
                    </div>
                  )}
                </div>

                {/* Expanded detail — schedule, next run, task file */}
                {isOpen && (
                  <div className="px-2.5 pb-3 pl-[52px] -mt-0.5">
                    <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[12px] text-white/45">
                      <span className="inline-flex items-center gap-1.5"><Clock className="h-3 w-3 text-white/30 shrink-0" />{c.description}</span>
                      {c.oneShot && <span className="text-amber-300/70">· One-time</span>}
                      {/* paused is already shown by the header pill — don't repeat it here */}
                      {!paused && nextStr && <span className="text-[#4AA8FF]">· Next: {nextStr}</span>}
                    </div>
                    {c.hasTaskFile && (
                      <button
                        type="button" onClick={() => handleDownloadTask(c)} disabled={downloading[c.id]}
                        className="group mt-2 inline-flex items-center gap-1.5 rounded-md bg-white/[0.04] hover:bg-white/[0.07] border border-white/[0.06] hover:border-white/[0.12] text-white/50 hover:text-white/80 text-[11px] font-mono px-2 py-1 transition-colors disabled:opacity-50"
                        title="Download task instructions"
                      >
                        {downloading[c.id] ? <LoaderCircle className="h-3 w-3 animate-spin" /> : <FileText className="h-3 w-3 shrink-0" />}
                        {c.id}.md
                        <Download className="h-3 w-3 opacity-0 group-hover:opacity-100 transition-opacity" />
                      </button>
                    )}
                  </div>
                )}
              </div>
            );
          })
        )}
      </div>
    </div>
  );
}

// Settings-only screen (step 7). Owns the single GET /api/schedule load; caches working state in a
// parent ref so edits survive the per-step remount (the wizard keys each step's motion.div by step).
function PulseCronSettings({ cacheRef }: { cacheRef: MutableRefObject<PulseCronCache | null> }) {
  const hadCache = useRef(cacheRef.current != null).current;
  const cached = cacheRef.current;
  const [loading, setLoading] = useState(!hadCache);
  const [loadError, setLoadError] = useState('');
  const [pulse, setPulse] = useState<PulseConfig>(cached?.pulse ?? DEFAULT_PULSE);
  const [originalPulse, setOriginalPulse] = useState<PulseConfig | null>(cached?.original ?? null);
  const [crons, setCrons] = useState<CronView[]>(cached?.crons ?? []);

  const fetchSchedule = async (withPulse: boolean) => {
    const res = await authFetch('/api/schedule');
    if (!res.ok) throw new Error('Failed to load schedule');
    const d = await res.json();
    setCrons(Array.isArray(d.crons) ? d.crons : []);
    if (withPulse) {
      const p: PulseConfig = {
        enabled: !!d.pulse?.enabled,
        intervalMinutes: Number(d.pulse?.intervalMinutes) || 60,
        quietHours: { start: d.pulse?.quietHours?.start ?? '22:00', end: d.pulse?.quietHours?.end ?? '07:00' },
      };
      setPulse(p);
      setOriginalPulse(p);
    }
  };

  useEffect(() => {
    if (hadCache) return;
    let cancelled = false;
    fetchSchedule(true)
      .then(() => { if (!cancelled) setLoading(false); })
      .catch((err) => { if (!cancelled) { setLoadError(err.message || 'Failed to load'); setLoading(false); } });
    return () => { cancelled = true; };
  }, [hadCache]);

  // Persist working state so navigating away and back doesn't lose pulse edits.
  useEffect(() => {
    if (loading) return;
    cacheRef.current = { pulse, original: originalPulse, crons };
  });

  return (
    <div>
      <h1 className="text-xl font-bold text-white tracking-tight">Pulse &amp; Crons</h1>
      <p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
        Background wake-ups and scheduled tasks your agent runs on its own.
      </p>

      {loading ? (
        <div className="flex items-center justify-center py-12 text-white/40"><LoaderCircle className="h-5 w-5 animate-spin" /></div>
      ) : loadError ? (
        <div className="mt-5 flex items-center gap-2 text-[13px] text-red-400"><TriangleAlert className="h-4 w-4 shrink-0" />{loadError}</div>
      ) : (
        <div className="mt-6">
          <PulseSection pulse={pulse} original={originalPulse} setPulse={setPulse} onPulseSaved={(saved) => setOriginalPulse(saved)} />
          <CronsSection crons={crons} onLocalUpdate={(fn) => setCrons(fn)} onChanged={() => { fetchSchedule(false).catch(() => {}); }} />
        </div>
      )}
    </div>
  );
}

/* ── Component ── */

interface Props {
  onComplete: () => void;
  isInitialSetup?: boolean;
  onSave?: (payload: any) => Promise<any>;
  onTunnelSwitch?: (newMode: 'off' | 'quick') => Promise<any>;
}

export default function OnboardWizard({ onComplete, isInitialSetup = false, onSave, onTunnelSwitch }: Props) {
  // 0..5 shared. Step 6 = "All Set" (onboarding) OR "Environment Variables" (settings re-run).
  const TOTAL_STEPS = 7;

  const [step, setStep] = useState(0);
  const [userName, setUserName] = useState('');
  const [provider, setProvider] = useState('anthropic');
  const [model, setModel] = useState('');
  const [saving, setSaving] = useState(false);

  // Auth state per provider
  const [authState, setAuthState] = useState<Record<string, 'idle' | 'authenticating' | 'connected'>>({
    anthropic: 'idle',
    openai: 'idle',
    pi: 'idle',
  });

  /* ── Bloby (pi) state ── */
  interface PiSubProviderInfo {
    id: string;
    name: string;
    subtitle: string;
    flavor: string;
    baseUrl?: string;
    needsBaseUrl: boolean;
    needsApiKey: boolean;
    apiKeyUrl?: string;
    models: { id: string; label: string }[] | 'dynamic';
    defaultModel?: string;
  }
  const [piSubProviders, setPiSubProviders] = useState<PiSubProviderInfo[]>([]);
  const [piSubProvider, setPiSubProvider] = useState<string>('');
  const [piApiKey, setPiApiKey] = useState('');
  const [piBaseUrl, setPiBaseUrl] = useState('');
  const [piModelId, setPiModelId] = useState('');
  const [piShowKey, setPiShowKey] = useState(false);
  const [piConnecting, setPiConnecting] = useState(false);
  const [piError, setPiError] = useState<string | undefined>();
  const [piSavedStatus, setPiSavedStatus] = useState<{ subProvider?: string; modelId?: string; baseUrl?: string } | null>(null);

  // Anthropic/Claude-specific
  const [oauthStarted, setOauthStarted] = useState(false);
  const [anthropicCode, setAnthropicCode] = useState('');
  const [isExchanging, setIsExchanging] = useState(false);
  const [anthropicError, setAnthropicError] = useState<string | undefined>();
  const [anthropicChecking, setAnthropicChecking] = useState(false);

  // OpenAI/Codex-specific
  // Two flows are supported: device-code (default, headless-friendly) and
  // paste-back (fallback for users who prefer the browser-callback flow).
  const [codexFlow, setCodexFlow] = useState<'device' | 'paste'>('device');

  // Device-code flow
  const [codexDeviceState, setCodexDeviceState] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
  const [codexUserCode, setCodexUserCode] = useState('');
  const [codexVerificationUrl, setCodexVerificationUrl] = useState('');
  const [codexDeviceStarting, setCodexDeviceStarting] = useState(false);
  const [codexCodeCopied, setCodexCodeCopied] = useState(false);

  // Paste-back flow
  const [codexOauthStarted, setCodexOauthStarted] = useState(false);
  const [codexCode, setCodexCode] = useState('');
  const [codexExchanging, setCodexExchanging] = useState(false);

  const [openaiError, setOpenaiError] = useState<string | undefined>();
  const [codexChecking, setCodexChecking] = useState(false);

  // Bot name + Handle (step 2)
  const [botName, setBotName] = useState('');
  const [handleStatus, setHandleStatus] = useState<null | 'checking' | 'ready' | 'invalid'>(null);
  const [handleError, setHandleError] = useState('');
  const [tierAvailability, setTierAvailability] = useState<Record<string, boolean>>({});
  const [selectedTier, setSelectedTier] = useState('');
  const [registering, setRegistering] = useState(false);
  const [registered, setRegistered] = useState(false);
  const [registeredUrl, setRegisteredUrl] = useState('');
  const handleDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);

  // Tunnel mode (step 2 branching)
  const [tunnelMode, setTunnelMode] = useState<'quick' | 'named' | 'off'>('quick');
  const [tunnelDomain, setTunnelDomain] = useState('');
  const [tunnelUrl, setTunnelUrl] = useState('');
  const [handleChoice, setHandleChoice] = useState<'tunnel' | 'relay'>('relay');

  // Existing handle (for re-run / change flow)
  const [existingHandle, setExistingHandle] = useState<{ username: string; tier: string; url: string } | null>(null);
  const [showChangeConfirm, setShowChangeConfirm] = useState(false);
  const [changingHandle, setChangingHandle] = useState(false);

  // Reserved handle claim flow
  const [tierReserved, setTierReserved] = useState<Record<string, boolean>>({});
  const [showClaimInput, setShowClaimInput] = useState(false);
  const [claimCode, setClaimCode] = useState('');
  const [claimError, setClaimError] = useState('');
  const [claiming, setClaiming] = useState(false);

  // Access detection + Tailscale-only switch
  const [accessMethod, setAccessMethod] = useState<AccessMethod>('tunnel');
  const [showTailscaleSwitch, setShowTailscaleSwitch] = useState(false);
  const [tailscaleConfirmText, setTailscaleConfirmText] = useState('');
  const [tailscaleSwitching, setTailscaleSwitching] = useState(false);
  const [tailscaleSwitchError, setTailscaleSwitchError] = useState('');
  const [showReEnableTunnel, setShowReEnableTunnel] = useState(false);
  const [reEnabling, setReEnabling] = useState(false);
  const [reEnableError, setReEnableError] = useState('');

  // Portal credentials (step 3)
  const [portalUser, setPortalUser] = useState('admin');
  const [portalPass, setPortalPass] = useState('');
  const [portalPassConfirm, setPortalPassConfirm] = useState('');
  const [portalCopied, setPortalCopied] = useState(false);
  const [portalExists, setPortalExists] = useState(false);
  const [showPassAllSet, setShowPassAllSet] = useState(false);
  const [acceptedTerms, setAcceptedTerms] = useState(false);

  // Portal old password (for changing existing credentials)
  const [portalOldPass, setPortalOldPass] = useState('');
  const [portalOldPassError, setPortalOldPassError] = useState('');
  const [portalOldPassVerified, setPortalOldPassVerified] = useState(false);
  const [portalVerifying, setPortalVerifying] = useState(false);
  // When the portal already has credentials, password fields are hidden by
  // default so the user doesn't think they must re-authenticate to proceed.
  // Toggling this opens the change-password sub-form.
  const [portalChangeMode, setPortalChangeMode] = useState(false);

  // Whisper (step 5)
  const [whisperEnabled, setWhisperEnabled] = useState(false);
  const [whisperKey, setWhisperKey] = useState('');

  // TOTP 2FA (step 3)
  const [totpEnabled, setTotpEnabled] = useState(false);
  const [totpSecret, setTotpSecret] = useState('');
  const [totpQrUri, setTotpQrUri] = useState('');
  const [totpOtpauthUri, setTotpOtpauthUri] = useState('');
  const [totpCode, setTotpCode] = useState('');
  const [totpVerified, setTotpVerified] = useState(false);
  const [totpVerifying, setTotpVerifying] = useState(false);
  const [totpError, setTotpError] = useState('');
  const [recoveryCodes, setRecoveryCodes] = useState<string[]>([]);
  const [recoveryCodesCopied, setRecoveryCodesCopied] = useState(false);
  const [totpDisabling, setTotpDisabling] = useState(false);
  const [totpDisableCode, setTotpDisableCode] = useState('');
  const [totpDisableError, setTotpDisableError] = useState('');
  const [isMobileDevice, setIsMobileDevice] = useState(false);
  // Sub-phase within step 3: 'password' | 'totp-setup' | 'recovery'
  const [step3Phase, setStep3Phase] = useState<'password' | 'totp-setup' | 'recovery'>('password');

  // Clipboard feedback for TOTP secret copy
  const [totpSecretCopied, setTotpSecretCopied] = useState(false);

  // Pre-fill guard
  const prefillDone = useRef(false);

  // Environment Variables screen working-state cache (settings mode). Survives the per-step
  // remount so edits aren't lost when navigating between settings screens; dies with the wizard.
  const envCacheRef = useRef<EnvCache | null>(null);
  // Same pattern for the Pulse & Crons screen (step 7).
  const pulseCacheRef = useRef<PulseCronCache | null>(null);
  // Scrollable content region (card is viewport-capped); reset to top on each step change.
  const contentScrollRef = useRef<HTMLDivElement>(null);

  // Settings mode (hub-and-spoke): each screen saves itself. `settingsOrig` is the last-saved
  // snapshot used both to show a Save button only when a screen actually changed, and to build a
  // payload that touches only the active screen's fields (the worker's /api/onboard is a full
  // overwrite, so other screens' fields must be sent at their saved values).
  const [settingsOrig, setSettingsOrig] = useState({ userName: '', botName: '', provider: '', model: '', whisperEnabled: false, whisperKey: '' });
  const [settingsSaving, setSettingsSaving] = useState(false);
  const [settingsSaved, setSettingsSaved] = useState(false);
  const [settingsSaveError, setSettingsSaveError] = useState('');

  const isConnected = authState[provider] === 'connected';

  // Persist/restore TOTP setup state across page reloads (mobile: OS suspends PWA on app-switch)
  const TOTP_STORAGE_KEY = 'bloby_totp_setup';

  function saveTotpState() {
    try {
      sessionStorage.setItem(TOTP_STORAGE_KEY, JSON.stringify({
        secret: totpSecret, qrUri: totpQrUri, otpauthUri: totpOtpauthUri, phase: step3Phase,
        // Also persist password fields so user doesn't re-type after returning
        portalPass, portalPassConfirm,
      }));
    } catch {}
  }

  function restoreTotpState() {
    try {
      const raw = sessionStorage.getItem(TOTP_STORAGE_KEY);
      if (!raw) return false;
      const saved = JSON.parse(raw);
      if (saved.secret && saved.phase === 'totp-setup') {
        setTotpSecret(saved.secret);
        setTotpQrUri(saved.qrUri || '');
        setTotpOtpauthUri(saved.otpauthUri || '');
        setTotpEnabled(true);
        setStep3Phase('totp-setup');
        setStep(3);
        if (saved.portalPass) { setPortalPass(saved.portalPass); setPortalPassConfirm(saved.portalPassConfirm || ''); }
        return true;
      }
    } catch {}
    return false;
  }

  function clearTotpStorage() {
    try { sessionStorage.removeItem(TOTP_STORAGE_KEY); } catch {}
  }

  // Pre-fill from existing settings (re-run wizard)
  useEffect(() => {
    fetch('/api/onboard/status')
      .then((r) => r.json())
      .then((data) => {
        if (data.userName) setUserName(data.userName);
        if (data.handle) {
          setBotName(data.handle.username);
          setSelectedTier(data.handle.tier || 'at');
          setExistingHandle({ username: data.handle.username, tier: data.handle.tier, url: data.handle.url });
          setRegistered(true);
          setRegisteredUrl(data.handle.url);
        } else if (data.agentName) {
          // Private-network mode has no handle — prefill the agent name so it isn't blank.
          setBotName(data.agentName);
        }
        if (data.portalUser) setPortalUser(data.portalUser);
        if (data.portalConfigured) setPortalExists(true);
        if (data.provider) setProvider(data.provider);
        if (data.model) setModel(data.model);
        if (data.whisperEnabled) { setWhisperEnabled(true); setWhisperKey(data.whisperKey || ''); }
        if (data.totpEnabled) { setTotpEnabled(true); setTotpVerified(true); }
        if (data.tunnelMode) setTunnelMode(data.tunnelMode);
        if (data.tunnelDomain) setTunnelDomain(data.tunnelDomain);
        if (data.tunnelUrl) setTunnelUrl(data.tunnelUrl);
        // If user has existing handle, default to 'relay'; otherwise default to 'tunnel'
        if (!data.handle) setHandleChoice('tunnel');
        // Snapshot the saved values so settings-mode per-screen Save buttons can detect changes
        // and so a single-screen save never clobbers another screen's saved value.
        setSettingsOrig({
          userName: data.userName || '',
          botName: (data.handle && data.handle.username) || data.agentName || '',
          provider: data.provider || '',
          model: data.model || '',
          whisperEnabled: !!data.whisperEnabled,
          whisperKey: data.whisperKey || '',
        });
        prefillDone.current = true;

        // Restore TOTP setup state if returning from authenticator app
        if (!data.totpEnabled) restoreTotpState();
      })
      .catch(() => { prefillDone.current = true; });
  }, []);

  // Detect access method on mount
  useEffect(() => {
    setAccessMethod(detectAccessMethod(window.location.hostname));
  }, []);

  // Mobile detection for TOTP setup
  useEffect(() => {
    setIsMobileDevice(window.matchMedia('(max-width: 768px)').matches || 'ontouchstart' in window);
  }, []);

  // Check if Claude is already authenticated when selecting Anthropic
  useEffect(() => {
    if (provider !== 'anthropic' || authState.anthropic === 'connected') return;
    fetch('/api/auth/claude/status')
      .then((r) => r.json())
      .then((data) => {
        if (data.authenticated) setAuthState((s) => ({ ...s, anthropic: 'connected' }));
      })
      .catch(() => {});
  }, [provider]);

  // Check if Codex is already authenticated when selecting OpenAI
  useEffect(() => {
    if (provider !== 'openai' || authState.openai === 'connected') return;
    fetch('/api/auth/codex/status')
      .then((r) => r.json())
      .then((data) => {
        if (data.authenticated) setAuthState((s) => ({ ...s, openai: 'connected' }));
      })
      .catch(() => {});
  }, [provider]);


  // Handle availability check (debounced, per-tier)
  // Skip when the current botName matches the existing registered handle
  useEffect(() => {
    if (handleDebounce.current) clearTimeout(handleDebounce.current);

    // Skip reset if this is the initial pre-fill setting the existing handle
    if (!prefillDone.current) return;

    // Private network mode — no handle registration needed
    if (tunnelMode === 'off') return;

    // Don't reset state if this is the already-registered handle
    if (existingHandle && registered && botName === existingHandle.username) {
      return;
    }

    setHandleStatus(null);
    setHandleError('');
    setTierAvailability({});
    setTierReserved({});
    setShowClaimInput(false);
    setClaimCode('');
    setClaimError('');
    setRegistered(false);
    setRegisteredUrl('');

    const trimmed = botName.trim();
    if (!trimmed) return;
    if (trimmed.length < 3) {
      setHandleStatus('invalid');
      setHandleError('At least 3 characters');
      return;
    }

    setHandleStatus('checking');
    handleDebounce.current = setTimeout(async () => {
      try {
        const res = await fetch(`/api/handle/check/${encodeURIComponent(trimmed)}`);
        const data = await res.json();
        if (!data.valid) {
          setHandleStatus('invalid');
          setHandleError(data.error);
        } else {
          const avail: Record<string, boolean> = {};
          const reserved: Record<string, boolean> = {};
          for (const h of data.handles) {
            avail[h.tier] = h.available;
            if (h.reserved) reserved[h.tier] = true;
          }
          setTierAvailability(avail);
          setTierReserved(reserved);
          setHandleStatus('ready');
          // Reset selection — user must explicitly choose
          setSelectedTier('');
        }
      } catch {
        setHandleStatus(null);
      }
    }, 400);

    return () => { if (handleDebounce.current) clearTimeout(handleDebounce.current); };
  }, [botName]);

  // Re-check availability when user returns from external purchase page
  useEffect(() => {
    if (!botName || botName.length < 3 || handleStatus !== 'ready') return;
    const onFocus = async () => {
      try {
        const res = await fetch(`/api/handle/check/${encodeURIComponent(botName.trim())}`);
        const data = await res.json();
        if (data.valid) {
          const avail: Record<string, boolean> = {};
          const reserved: Record<string, boolean> = {};
          for (const h of data.handles) {
            avail[h.tier] = h.available;
            if (h.reserved) reserved[h.tier] = true;
          }
          setTierAvailability(avail);
          setTierReserved(reserved);
        }
      } catch {}
    };
    window.addEventListener('focus', onFocus);
    return () => window.removeEventListener('focus', onFocus);
  }, [botName, handleStatus]);

  const onBotNameInput = (val: string) => {
    setBotName(val.toLowerCase().replace(/[^a-z0-9-]/g, ''));
  };

  const onClaimHandle = async () => {
    if (!botName || handleStatus !== 'ready' || !tierAvailability[selectedTier]) return;
    setRegistering(true);
    try {
      const res = await fetch('/api/handle/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username: botName, tier: selectedTier }),
      });
      const data = await res.json();
      if (data.ok) {
        setRegistered(true);
        setRegisteredUrl(data.url);
      } else {
        setHandleError(data.error || 'Registration failed');
        setHandleStatus('invalid');
      }
    } catch {
      setHandleError('Could not reach server');
      setHandleStatus('invalid');
    } finally {
      setRegistering(false);
    }
  };

  const onChangeHandle = async () => {
    if (!botName || handleStatus !== 'ready' || !tierAvailability[selectedTier]) return;
    setChangingHandle(true);
    try {
      const res = await fetch('/api/handle/change', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username: botName, tier: selectedTier }),
      });
      const data = await res.json();
      if (data.ok) {
        setRegistered(true);
        setRegisteredUrl(data.url);
        setExistingHandle({ username: botName, tier: selectedTier, url: data.url });
        setShowChangeConfirm(false);
      } else {
        setHandleError(data.error || 'Handle change failed');
        setHandleStatus('invalid');
      }
    } catch {
      setHandleError('Could not reach server');
      setHandleStatus('invalid');
    } finally {
      setChangingHandle(false);
    }
  };

  const onClaimReserved = async () => {
    if (!botName || !claimCode) return;
    setClaiming(true);
    setClaimError('');
    try {
      const res = await fetch('/api/handle/claim-reserved', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ handle: botName, hash: claimCode }),
      });
      const data = await res.json();
      if (data.ok) {
        setRegistered(true);
        setRegisteredUrl(data.url);
        setShowClaimInput(false);
        setClaimCode('');
        setSelectedTier('premium');
        setHandleChoice('relay');
      } else {
        setClaimError(data.error || 'Invalid activation code');
      }
    } catch {
      setClaimError('Could not reach server');
    } finally {
      setClaiming(false);
    }
  };

  const handleProviderChange = (id: string) => {
    if (provider === 'openai' && id !== 'openai') {
      if (codexOauthStarted) fetch('/api/auth/codex/cancel', { method: 'POST' });
      if (codexDeviceState === 'pending') fetch('/api/auth/codex/device/cancel', { method: 'POST' });
    }
    setProvider(id);
    setModel(id === 'openai' ? 'gpt-5.5:high' : '');
    setOauthStarted(false);
    setAnthropicCode('');
    setAnthropicError(undefined);
    setCodexOauthStarted(false);
    setCodexCode('');
    setCodexDeviceState('idle');
    setCodexUserCode('');
    setCodexVerificationUrl('');
    setCodexFlow('device');
    setOpenaiError(undefined);
    // Pi flow cleanup is per-sub-provider; the load effect handles re-hydration.
    setPiError(undefined);
  };

  /* ── Bloby (pi) handlers ── */

  // Load the sub-provider catalog + saved status when the user picks Bloby
  useEffect(() => {
    if (provider !== 'pi') return;
    let cancelled = false;
    (async () => {
      try {
        const [provRes, statusRes] = await Promise.all([
          fetch('/api/auth/pi/providers'),
          fetch('/api/auth/pi/status'),
        ]);
        const provData = await provRes.json();
        const statusData = await statusRes.json();
        if (cancelled) return;
        const list: PiSubProviderInfo[] = provData?.providers || [];
        setPiSubProviders(list);
        if (statusData?.configured) {
          setPiSubProvider(statusData.subProvider || '');
          setPiModelId(statusData.modelId || '');
          setPiBaseUrl(statusData.baseUrl || '');
          setAuthState((s) => ({ ...s, pi: 'connected' }));
          setPiSavedStatus({ subProvider: statusData.subProvider, modelId: statusData.modelId, baseUrl: statusData.baseUrl });
          setModel(`${statusData.subProvider}/${statusData.modelId || ''}`);
        } else if (!piSubProvider && list[0]) {
          setPiSubProvider(list[0].id);
          setPiBaseUrl(list[0].baseUrl || '');
          setPiModelId(list[0].defaultModel || '');
        }
      } catch (err: any) {
        if (!cancelled) setPiError(err?.message || 'Failed to load Bloby providers');
      }
    })();
    return () => { cancelled = true; };
  }, [provider]);

  const selectedPiSub = piSubProviders.find((p) => p.id === piSubProvider);

  const choosePiSubProvider = (id: string) => {
    const next = piSubProviders.find((p) => p.id === id);
    setPiSubProvider(id);
    setPiBaseUrl(next?.baseUrl || '');
    setPiModelId(next?.defaultModel || '');
    setPiError(undefined);
    // Picking a new sub-provider invalidates the previously saved auth.
    if (authState.pi === 'connected') {
      setAuthState((s) => ({ ...s, pi: 'idle' }));
      setPiSavedStatus(null);
    }
  };

  const handlePiConnect = async () => {
    if (!piSubProvider) return;
    setPiError(undefined);
    setPiConnecting(true);
    try {
      const payload = {
        subProvider: piSubProvider,
        apiKey: piApiKey.trim() || undefined,
        baseUrl: piBaseUrl.trim() || undefined,
        modelId: piModelId.trim() || undefined,
      };
      const testRes = await fetch('/api/auth/pi/test', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      const testData = await testRes.json();
      if (!testData?.ok) {
        setPiError(testData?.error || 'Connection test failed');
        return;
      }
      const saveRes = await fetch('/api/auth/pi/save', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      const saveData = await saveRes.json();
      if (!saveData?.ok) {
        setPiError(saveData?.error || 'Failed to save credentials');
        return;
      }
      setAuthState((s) => ({ ...s, pi: 'connected' }));
      setPiSavedStatus(saveData.status || null);
      setModel(`${piSubProvider}/${piModelId || selectedPiSub?.defaultModel || ''}`);
      setPiApiKey('');
    } catch (err: any) {
      setPiError(err?.message || 'Connection failed');
    } finally {
      setPiConnecting(false);
    }
  };

  const handlePiDisconnect = async () => {
    try { await fetch('/api/auth/pi', { method: 'DELETE' }); } catch {}
    setAuthState((s) => ({ ...s, pi: 'idle' }));
    setPiSavedStatus(null);
    setModel('');
  };

  /* ── Auth handlers: Anthropic/Claude ── */

  const openExternal = (url: string) => {
    const isStandalone = window.matchMedia('(display-mode: standalone)').matches
      || (navigator as any).standalone === true;
    if (isStandalone) {
      // iOS PWA standalone mode blocks window.open — use a temp anchor with target _blank
      const a = document.createElement('a');
      a.href = url;
      a.target = '_blank';
      a.rel = 'noopener noreferrer';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    } else {
      window.open(url, '_blank', 'noopener,noreferrer');
    }
  };

  const handleAnthropicAuth = async () => {
    setAnthropicError(undefined);
    try {
      const res = await fetch('/api/auth/claude/start', { method: 'POST' });
      const data = await res.json();
      if (data.success && data.authUrl) {
        openExternal(data.authUrl);
        setOauthStarted(true);
      } else {
        setAnthropicError(data.error || 'Failed to start authentication');
      }
    } catch (err: any) {
      setAnthropicError(err.message);
    }
  };

  const handleAnthropicConnect = async () => {
    if (!anthropicCode.trim()) return;
    setIsExchanging(true);
    setAnthropicError(undefined);
    try {
      const res = await fetch('/api/auth/claude/exchange', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code: anthropicCode.trim() }),
      });
      const data = await res.json();
      if (data.success) {
        setAuthState((s) => ({ ...s, anthropic: 'connected' }));
      } else {
        setAnthropicError(data.error || 'Code exchange failed');
      }
    } catch (err: any) {
      setAnthropicError(err.message);
    } finally {
      setIsExchanging(false);
    }
  };

  const handleAnthropicPaste = async () => {
    try {
      const text = await navigator.clipboard.readText();
      if (text) setAnthropicCode(text.trim());
    } catch { /* clipboard denied */ }
  };

  const handleAnthropicCheckAuth = async () => {
    setAnthropicChecking(true);
    setAnthropicError(undefined);
    try {
      const res = await fetch('/api/auth/claude/status');
      const data = await res.json();
      if (data.authenticated) {
        setAuthState((s) => ({ ...s, anthropic: 'connected' }));
      } else {
        setAnthropicError('No active session found. Please authenticate first.');
      }
    } catch {} finally {
      setAnthropicChecking(false);
    }
  };

  /* ── Auth handlers: OpenAI/Codex (device-code flow — default) ── */

  const handleOpenAIDeviceStart = async () => {
    setOpenaiError(undefined);
    setCodexDeviceStarting(true);
    setCodexDeviceState('pending');
    try {
      const res = await fetch('/api/auth/codex/device/start', { method: 'POST' });
      const data = await res.json();
      if (data.success) {
        setCodexUserCode(data.userCode || '');
        setCodexVerificationUrl(data.verificationUrl || '');
        if (data.verificationUrl) openExternal(data.verificationUrl);
      } else {
        setCodexDeviceState('error');
        setOpenaiError(data.error || 'Failed to start device-code login');
      }
    } catch (err: any) {
      setCodexDeviceState('error');
      setOpenaiError(err.message);
    } finally {
      setCodexDeviceStarting(false);
    }
  };

  const handleOpenAIDeviceCancel = async () => {
    try { await fetch('/api/auth/codex/device/cancel', { method: 'POST' }); } catch {}
    setCodexDeviceState('idle');
    setCodexUserCode('');
    setCodexVerificationUrl('');
  };

  const handleCopyUserCode = async () => {
    try {
      await navigator.clipboard.writeText(codexUserCode);
      setCodexCodeCopied(true);
      setTimeout(() => setCodexCodeCopied(false), 1500);
    } catch { /* clipboard denied */ }
  };

  // Poll device-code status while pending
  useEffect(() => {
    if (codexDeviceState !== 'pending' || !codexUserCode) return;
    const interval = setInterval(async () => {
      try {
        const res = await fetch('/api/auth/codex/device/status');
        const data = await res.json();
        if (data.state === 'success') {
          setCodexDeviceState('success');
          setAuthState((s) => ({ ...s, openai: 'connected' }));
        } else if (data.state === 'error') {
          setCodexDeviceState('error');
          setOpenaiError(data.error || 'Device-code login failed');
        }
      } catch {}
    }, 2000);
    return () => clearInterval(interval);
  }, [codexDeviceState, codexUserCode]);

  /* ── Auth handlers: OpenAI/Codex (paste-back flow — fallback) ── */

  const handleOpenAIAuth = async () => {
    setOpenaiError(undefined);
    try {
      const res = await fetch('/api/auth/codex/start', { method: 'POST' });
      const data = await res.json();
      if (data.success && data.authUrl) {
        openExternal(data.authUrl);
        setCodexOauthStarted(true);
      } else {
        setOpenaiError(data.error || 'Failed to start authentication');
      }
    } catch (err: any) {
      setOpenaiError(err.message);
    }
  };

  const handleOpenAIConnect = async () => {
    if (!codexCode.trim()) return;
    setCodexExchanging(true);
    setOpenaiError(undefined);
    try {
      const res = await fetch('/api/auth/codex/exchange', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code: codexCode.trim() }),
      });
      const data = await res.json();
      if (data.success) {
        setAuthState((s) => ({ ...s, openai: 'connected' }));
        setCodexCode('');
      } else {
        setOpenaiError(data.error || 'Code exchange failed');
      }
    } catch (err: any) {
      setOpenaiError(err.message);
    } finally {
      setCodexExchanging(false);
    }
  };

  const handleOpenAIPaste = async () => {
    try {
      const text = await navigator.clipboard.readText();
      if (text) setCodexCode(text.trim());
    } catch { /* clipboard denied */ }
  };

  const handleOpenAICheckAuth = async () => {
    setCodexChecking(true);
    setOpenaiError(undefined);
    try {
      const res = await fetch('/api/auth/codex/status');
      const data = await res.json();
      if (data.authenticated) {
        setAuthState((s) => ({ ...s, openai: 'connected' }));
      } else {
        setOpenaiError('No active session found. Please authenticate first.');
      }
    } catch {} finally {
      setCodexChecking(false);
    }
  };

  /* ── Navigation ── */

  // Steps: 0=Welcome, 1=Name, 2=Bot name + Handle, 3=Password, 4=Provider, 5=Whisper+Complete, 6=All Set (initial only)
  const portalPassMatch = portalPass === portalPassConfirm;
  const portalValid = portalPass.length >= 6 && portalPassMatch;
  // When portal exists: can continue if no password fields touched, or old pass verified + new pass valid
  const portalCanContinue = portalExists
    ? (portalPass.length === 0 || (portalOldPassVerified && portalValid))
    : portalValid;

  const canNext = (() => {
    switch (step) {
      case 0: return true;
      case 1: return userName.trim().length > 0;
      case 2: {
        if (showTailscaleSwitch || showReEnableTunnel) return false;
        if (tunnelMode === 'off') return botName.trim().length >= 3;
        if (tunnelMode === 'named') return botName.trim().length >= 3;
        if (handleChoice === 'tunnel') return botName.trim().length >= 3;
        return registered;
      }
      case 3: {
        // Block "Continue" while in a TOTP sub-phase
        if (step3Phase !== 'password') return false;
        if (!portalCanContinue) return false;
        if (totpEnabled && !totpVerified) return false;
        return true;
      }
      case 4: return !!(provider && model && isConnected);
      case 5: return true;
      case 6: return true; // Environment Variables (settings) — self-saving
      case 7: return true; // Pulse & Crons (settings) — self-saving
      default: return false;
    }
  })();

  // Sequential advance is onboarding-only. Settings mode is hub-and-spoke: the user navigates via
  // the header dropdown / "Go to" menu and each screen saves itself, so there is no "Continue".
  const next = () => { if (isInitialSetup && canNext && step < TOTAL_STEPS - 1) setStep((s) => s + 1); };
  const back = () => { if (step > 0) setStep((s) => s - 1); };

  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === 'Enter' && canNext) next();
  };

  // ── Settings mode: per-screen change detection + save ──
  // Only the currently-shown settings screen reports "dirty"; the footer Save appears only then.
  const settingsScreenDirty = (() => {
    if (isInitialSetup) return false;
    switch (step) {
      case 1: return userName.trim() !== settingsOrig.userName;
      case 2: return botName.trim() !== settingsOrig.botName;
      case 3: return step3Phase === 'password' && portalPass.length > 0 && portalValid && (portalExists ? portalOldPassVerified : true);
      case 4: return (provider !== settingsOrig.provider || model !== settingsOrig.model) && isConnected && !!model;
      case 5: return (whisperEnabled !== settingsOrig.whisperEnabled || (whisperEnabled && whisperKey !== settingsOrig.whisperKey))
        && !(whisperEnabled && (!whisperKey.startsWith('sk-') || whisperKey.length < 20));
      default: return false; // 0 = hub, 6 = env (self-saving)
    }
  })();

  // Clear the transient saved/error banners whenever the user moves to another settings screen.
  useEffect(() => {
    setSettingsSaved(false); setSettingsSaveError('');
    contentScrollRef.current?.scrollTo({ top: 0 });
  }, [step]);

  const saveSettings = async () => {
    if (!settingsScreenDirty || settingsSaving) return;
    setSettingsSaving(true); setSettingsSaveError(''); setSettingsSaved(false);
    // Base the payload on the last-saved values, then override ONLY the active screen's fields —
    // so saving one screen never persists another screen's unsaved, in-progress edits.
    const payload: any = {
      userName: settingsOrig.userName,
      agentName: settingsOrig.botName || 'Bloby',
      provider: settingsOrig.provider,
      model: settingsOrig.model,
      apiKey: '',
      whisperEnabled: settingsOrig.whisperEnabled,
      whisperKey: settingsOrig.whisperEnabled ? settingsOrig.whisperKey : '',
      portalUser: portalUser.trim(),
      portalPass: '',
    };
    switch (step) {
      case 1: payload.userName = userName.trim(); break;
      case 2: payload.agentName = botName.trim() || 'Bloby'; break;
      case 3: payload.portalPass = portalPass; break;
      case 4: payload.provider = provider; payload.model = model; break;
      case 5: payload.whisperEnabled = whisperEnabled; payload.whisperKey = whisperEnabled ? whisperKey : ''; break;
    }
    try {
      if (onSave) await onSave(payload);
      else await fetch('/api/onboard', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
      // Advance the saved snapshot so the Save button hides (screen no longer dirty).
      setSettingsOrig((o) => {
        switch (step) {
          case 1: return { ...o, userName: userName.trim() };
          case 2: return { ...o, botName: botName.trim() };
          case 4: return { ...o, provider, model };
          case 5: return { ...o, whisperEnabled, whisperKey };
          default: return o;
        }
      });
      if (step === 3) {
        // Password persisted — reset the change-password UI so it's no longer dirty.
        setPortalPass(''); setPortalPassConfirm(''); setPortalOldPass(''); setPortalOldPassVerified(false); setPortalChangeMode(false);
        setPortalExists(true);
      }
      setSettingsSaved(true);
      setSettingsSaving(false);
    } catch (err: any) {
      setSettingsSaveError(err.message || 'Save failed');
      setSettingsSaving(false);
    }
  };

  const handleComplete = async () => {
    setSaving(true);
    const payload = {
      userName: userName.trim(),
      agentName: botName.trim() || 'Bloby',
      provider,
      model,
      apiKey: '',
      whisperEnabled,
      whisperKey: whisperEnabled ? whisperKey : '',
      portalUser: portalUser.trim(),
      portalPass,
    };
    try {
      if (onSave) {
        // Chat context: save via WebSocket (bypasses relay POST issues)
        await onSave(payload);
      } else {
        // Initial onboard: direct POST
        await fetch('/api/onboard', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
        });
      }
      if (isInitialSetup) {
        setSaving(false);
        setStep(6);
      } else {
        onComplete();
      }
    } catch (err) {
      console.error('[OnboardWizard] Onboard failed:', err);
      setSaving(false);
    }
  };

  /* ── Styles ── */

  const inputCls =
    'w-full bg-white/[0.05] border border-white/[0.08] text-white rounded-xl px-4 py-3 text-base outline-none input-glow placeholder:text-white/20 transition-all';
  const inputSmCls =
    'w-full bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none input-glow placeholder:text-white/20 transition-all';

  return (
    <div className="fixed inset-0 z-[200] flex items-center justify-center p-4">
      <div className="absolute inset-0 bg-black/85 backdrop-blur-md" />

      <motion.div
        initial={{ opacity: 0, scale: 0.95 }}
        animate={{ opacity: 1, scale: 1 }}
        transition={{ duration: 0.3 }}
        className="relative w-full max-w-[480px] max-h-[calc(100dvh_-_2rem)] flex flex-col bg-[#181818] border border-white/[0.06] rounded-[24px] shadow-2xl overflow-hidden"
      >
        {/* Onboarding: step dots. Settings: a header with hub-back, screen jump, and close. */}
        {isInitialSetup ? (
          <div className="flex justify-center gap-2 pt-6 shrink-0">
            {Array.from({ length: TOTAL_STEPS }, (_, i) => (
              <div
                key={i}
                className={`h-1.5 rounded-full transition-all duration-300 ${
                  i === step
                    ? 'w-7 bg-gradient-brand'
                    : i < step
                      ? 'w-1.5 bg-gradient-brand opacity-60'
                      : 'w-1.5 bg-white/10'
                }`}
              />
            ))}
          </div>
        ) : (
          <div className="flex items-center justify-between px-5 pt-4 pb-1 shrink-0">
            {step === 0 ? (
              <span className="text-[13px] font-semibold text-white/80 pl-1">Settings</span>
            ) : (
              <button
                onClick={() => setStep(0)}
                className="flex items-center gap-1 text-white/50 hover:text-white/80 text-[13px] font-medium pl-1 pr-2 py-1 rounded-lg hover:bg-white/[0.05] transition-colors"
              >
                <ArrowLeft className="h-3.5 w-3.5" /> Settings
              </button>
            )}
            <div className="flex items-center gap-0.5">
              {step !== 0 && <GoToMenu onJump={(s) => setStep(s)} />}
              <button
                onClick={onComplete}
                className="text-white/40 hover:text-white/80 p-1.5 rounded-lg hover:bg-white/[0.05] transition-colors"
                aria-label="Close settings"
              >
                <X className="h-4 w-4" />
              </button>
            </div>
          </div>
        )}

        {/* Content — scrolls within the viewport-capped card so tall screens stay reachable. */}
        <div ref={contentScrollRef} className="flex-1 min-h-0 overflow-y-auto">
        <AnimatePresence mode="wait">
          <motion.div
            key={step}
            initial={{ opacity: 0, x: 30 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: -30 }}
            transition={{ duration: 0.2, ease: 'easeOut' }}
            className="px-8 pt-6 pb-8"
          >
            {/* ── Step 0: Welcome (onboarding) ── */}
            {step === 0 && isInitialSetup && (
              <div className="flex flex-col items-center text-center">
                <video autoPlay loop muted playsInline className="h-[180px] mb-4">
                  <source src="/morphy_hi.mov" type='video/mp4; codecs="hvc1"' />
                  <source src="/morphy_hi.webm" type="video/webm" />
                </video>
                <h1 className="text-2xl font-bold text-white tracking-tight">
                  Welcome to Bloby
                </h1>
                <p className="text-white/40 text-[14px] mt-2 leading-relaxed max-w-[320px]">
                  Let's set up your AI assistant in just a few steps.
                </p>
                <button
                  onClick={next}
                  className="mt-6 px-7 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center gap-2"
                >
                  Get Started
                  <ArrowRight className="h-4 w-4" />
                </button>
              </div>
            )}

            {/* ── Step 0: Settings hub (settings re-run) ── */}
            {step === 0 && !isInitialSetup && (
              <div className="flex flex-col items-center text-center">
                <video autoPlay loop muted playsInline className="h-[150px] mb-4">
                  <source src="/morphy_hi.mov" type='video/mp4; codecs="hvc1"' />
                  <source src="/morphy_hi.webm" type="video/webm" />
                </video>
                <h1 className="text-2xl font-bold text-white tracking-tight">
                  Settings
                </h1>
                <p className="text-white/40 text-[14px] mt-2 leading-relaxed max-w-[320px]">
                  What would you like to change?
                </p>
                <div className="w-full mt-5">
                  <ModelDropdown
                    models={SETTINGS_SCREENS.map((s) => ({ id: String(s.step), label: s.label }))}
                    value=""
                    placeholder="Select a setting…"
                    menuMaxPx={188}
                    onChange={(id) => setStep(Number(id))}
                  />
                </div>
              </div>
            )}

            {/* ── Step 1: Your name ── */}
            {step === 1 && (
              <div>
                <h1 className="text-xl font-bold text-white tracking-tight">
                  What's your name?
                </h1>
                <p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
                  This is how your agent will address you.
                </p>
                <div className="mt-5 flex items-center gap-3">
                  <input
                    type="text"
                    value={userName}
                    onChange={(e) => setUserName(e.target.value)}
                    onKeyDown={handleKeyDown}
                    placeholder="Enter your name"
                    autoFocus
                    autoComplete="off"
                    data-1p-ignore
                    data-lpignore="true"
                    className={inputCls + ' flex-1'}
                  />
                  {isInitialSetup && (
                  <button
                    onClick={next}
                    disabled={!canNext}
                    className="shrink-0 h-12 w-12 flex items-center justify-center rounded-full bg-gradient-brand hover:opacity-90 text-white transition-colors disabled:opacity-30"
                  >
                    <ArrowRight className="h-5 w-5" />
                  </button>
                  )}
                </div>
              </div>
            )}

            {/* ── Step 2: Name your bot + Claim handle ── */}
            {step === 2 && tunnelMode === 'off' && (
              <div>
                <h1 className="text-xl font-bold text-white tracking-tight">
                  Agent Name & Access
                </h1>
                <p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
                  Give your bot a name. This is used throughout the app as your bot's identity.
                </p>

                <div className="relative mt-5">
                  <input
                    type="text"
                    value={botName}
                    onChange={(e) => onBotNameInput(e.target.value)}
                    maxLength={30}
                    placeholder="your-bot-name"
                    spellCheck={false}
                    autoCapitalize="none"
                    autoCorrect="off"
                    autoComplete="off"
                    data-1p-ignore
                    data-lpignore="true"
                    autoFocus
                    className={inputCls + ' pr-10 font-mono'}
                  />
                </div>

                <div className="mt-4 bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-3">
                  <p className="text-white/40 text-[12px] leading-relaxed">
                    Private network mode — your bot is only accessible via your local network or VPN. No public URL will be created.
                  </p>
                </div>

                {/* Access badge */}
                {!isInitialSetup && (
                  <div className="mt-3 flex items-center gap-2">
                    <span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-medium border ${
                      isPrivateAccess(accessMethod)
                        ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'
                        : 'bg-blue-500/10 text-blue-400 border-blue-500/20'
                    }`}>
                      {isPrivateAccess(accessMethod) ? <WifiOff className="h-3 w-3" /> : <Globe className="h-3 w-3" />}
                      Accessing via {ACCESS_LABELS[accessMethod]}
                    </span>
                  </div>
                )}

                {/* Re-enable tunnel option */}
                {!isInitialSetup && !showReEnableTunnel && (
                  <button
                    onClick={() => { setShowReEnableTunnel(true); setReEnableError(''); }}
                    className="w-full mt-4 flex items-center justify-between px-4 py-3 rounded-xl border border-white/[0.06] bg-white/[0.02] hover:border-white/10 hover:bg-white/[0.04] transition-all text-left"
                  >
                    <div>
                      <p className="text-[13px] text-white/70 font-medium">Re-enable public tunnel access</p>
                      <p className="text-[11px] text-white/30 mt-0.5">Start a Cloudflare tunnel to make your bot accessible from anywhere</p>
                    </div>
                    <Globe className="h-4 w-4 text-white/30 shrink-0 ml-3" />
                  </button>
                )}

                {/* Re-enable confirmation */}
                {showReEnableTunnel && (
                  <div className="mt-4 space-y-3">
                    <div className="bg-amber-500/8 border border-amber-500/20 rounded-xl px-4 py-3">
                      <div className="flex items-start gap-2">
                        <TriangleAlert className="h-4 w-4 text-amber-400 shrink-0 mt-0.5" />
                        <div>
                          <p className="text-amber-400/90 text-[13px] font-medium">Enable public access?</p>
                          <p className="text-amber-400/60 text-[12px] mt-1 leading-relaxed">
                            This will start a Cloudflare tunnel, making your bot reachable from the internet.
                            {existingHandle && ' Your handle will reconnect automatically.'}
                          </p>
                        </div>
                      </div>
                    </div>
                    {reEnableError && (
                      <p className="text-red-400/70 text-[12px]">{reEnableError}</p>
                    )}
                    <div className="flex gap-2">
                      <button
                        onClick={async () => {
                          if (!onTunnelSwitch) return;
                          setReEnabling(true);
                          setReEnableError('');
                          try {
                            await onTunnelSwitch('quick');
                            setTunnelMode('quick');
                            setShowReEnableTunnel(false);
                          } catch (err: any) {
                            setReEnableError(err.message || 'Failed to start tunnel');
                          } finally {
                            setReEnabling(false);
                          }
                        }}
                        disabled={reEnabling || !onTunnelSwitch}
                        className="flex-1 py-2.5 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
                      >
                        {reEnabling ? <><LoaderCircle className="h-4 w-4 animate-spin" />Starting tunnel...</> : 'Enable Tunnel'}
                      </button>
                      <button
                        onClick={() => { setShowReEnableTunnel(false); setReEnableError(''); }}
                        disabled={reEnabling}
                        className="px-5 py-2.5 bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.08] text-white/60 text-[13px] font-medium rounded-full transition-colors"
                      >
                        Cancel
                      </button>
                    </div>
                  </div>
                )}

                {isInitialSetup && (
                <button
                  onClick={next}
                  disabled={!canNext}
                  className="w-full mt-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
                >
                  Continue
                  <ArrowRight className="h-4 w-4" />
                </button>
                )}
              </div>
            )}

            {step === 2 && tunnelMode === 'named' && (
              <div>
                <h1 className="text-xl font-bold text-white tracking-tight">
                  Agent Name & Access
                </h1>
                <p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
                  This is your bot's identity. Your named tunnel domain is already configured.
                </p>

                <div className="relative mt-5">
                  <input
                    type="text"
                    value={botName}
                    onChange={(e) => onBotNameInput(e.target.value)}
                    maxLength={30}
                    placeholder="your-bot-name"
                    spellCheck={false}
                    autoCapitalize="none"
                    autoCorrect="off"
                    autoComplete="off"
                    data-1p-ignore
                    data-lpignore="true"
                    autoFocus
                    className={inputCls + ' pr-10 font-mono'}
                  />
                </div>

                <div className="mt-4 flex items-center gap-2 bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-3">
                  <span className="font-mono text-[13px] text-white/70 truncate flex-1 text-left">https://{tunnelDomain}</span>
                  <button
                    onClick={() => {
                      navigator.clipboard.writeText(`https://${tunnelDomain}`);
                      setPortalCopied(true);
                      setTimeout(() => setPortalCopied(false), 2000);
                    }}
                    className="shrink-0 text-white/30 hover:text-white/60 transition-colors"
                  >
                    {portalCopied ? (
                      <Check className="h-4 w-4 text-emerald-400" />
                    ) : (
                      <ClipboardPaste className="h-4 w-4" />
                    )}
                  </button>
                </div>
              </div>
            )}

            {step === 2 && tunnelMode === 'quick' && (
              <div>
                <h1 className="text-xl font-bold text-white tracking-tight">
                  Agent Name & Access
                </h1>
                <p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
                  This is your bot's name and permanent handle — access it from anywhere.
                </p>

                {/* Existing handle banner */}
                {existingHandle && registered && !showChangeConfirm && (
                  <>
                    <div className="mt-4 bg-emerald-500/8 border border-emerald-500/15 rounded-xl px-4 py-3">
                      <div className="flex items-center gap-2">
                        <Check className="h-4 w-4 text-emerald-400" />
                        <p className="text-emerald-400/90 text-[13px] font-medium">Current handle</p>
                      </div>
                      <p className="text-emerald-400/60 text-[12px] mt-1 font-mono">{registeredUrl}</p>
                    </div>
                    <div className="flex gap-2 mt-4">
                      {isInitialSetup && (
                      <button
                        onClick={next}
                        className="flex-1 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
                      >
                        Continue
                        <ArrowRight className="h-4 w-4" />
                      </button>
                      )}
                      <button
                        onClick={() => {
                          setShowChangeConfirm(true);
                          setRegistered(false);
                          setBotName('');
                          setHandleStatus(null);
                          setTierAvailability({});
                          setHandleChoice('relay');
                        }}
                        className="px-5 py-3 bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.08] text-white/60 text-[13px] font-medium rounded-full transition-colors"
                      >
                        Change
                      </button>
                    </div>
                  </>
                )}

                {/* Change confirmation alert */}
                {showChangeConfirm && !registered && (
                  <div className="mt-4 bg-amber-500/8 border border-amber-500/20 rounded-xl px-4 py-3">
                    <p className="text-amber-400/90 text-[13px] font-medium">Changing your handle</p>
                    <p className="text-amber-400/60 text-[12px] mt-1">
                      Your current handle <span className="font-mono">{existingHandle?.url}</span> will be released and become available for others.
                    </p>
                  </div>
                )}

                {/* Input + handle flow — shown for new claim or change flow */}
                {(!existingHandle || showChangeConfirm || !registered) && !(existingHandle && registered && !showChangeConfirm) && (
                  <>
                    <div className="relative mt-5">
                      <input
                        type="text"
                        value={botName}
                        onChange={(e) => onBotNameInput(e.target.value)}
                        maxLength={30}
                        placeholder="your-bot-name"
                        spellCheck={false}
                        autoCapitalize="none"
                        autoCorrect="off"
                        autoComplete="off"
                        data-1p-ignore
                        data-lpignore="true"
                        autoFocus
                        disabled={registered}
                        className={inputCls + ' pr-10 font-mono' + (registered ? ' opacity-50' : '')}
                      />
                      {handleStatus && botName.length > 0 && !registered && (
                        <div className="absolute right-4 top-1/2 -translate-y-1/2">
                          {handleStatus === 'checking' && (
                            <div className="w-5 h-5 border-2 border-white/10 border-t-[#0069FE] rounded-full animate-spin" />
                          )}
                          {handleStatus === 'invalid' && (
                            <div className="w-6 h-6 rounded-full bg-amber-500/15 flex items-center justify-center">
                              <svg className="w-3.5 h-3.5 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
                                <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3m0 4h.01" />
                              </svg>
                            </div>
                          )}
                        </div>
                      )}
                    </div>

                    {/* Status messages */}
                    {handleStatus === 'invalid' && handleError && (
                      <p className="text-amber-400 text-[12px] mt-2">{handleError}</p>
                    )}

                    {/* Handle tier options — shown when name is valid */}
                    {handleStatus === 'ready' && botName.length > 0 && !registered && (
                      <div className="space-y-3 mt-4">
                        {/* ── Free tier block ── */}
                        {(() => {
                          const freeAvail = tierAvailability['at'];
                          const freeTaken = freeAvail === false;
                          const freeSelected = handleChoice === 'relay' && selectedTier === 'at';
                          return (
                            <button
                              onClick={() => { if (!freeTaken) { setSelectedTier('at'); setHandleChoice('relay'); } }}
                              disabled={freeTaken}
                              className={`w-full rounded-xl border transition-all duration-200 text-left px-4 py-3 ${
                                freeTaken
                                  ? 'border-white/[0.04] opacity-50 cursor-not-allowed'
                                  : freeSelected
                                    ? 'border-[#0069FE]/30 bg-white/[0.04]'
                                    : 'border-white/[0.06] hover:border-white/10 hover:bg-white/[0.02]'
                              }`}
                            >
                              <div className="flex items-center justify-between">
                                <span className="text-[12px] font-semibold text-white/60 uppercase tracking-wider">Free</span>
                                <span className={`text-[11px] font-medium px-2.5 py-0.5 rounded-full border ${
                                  freeTaken ? 'bg-red-500/10 text-red-400 border-red-500/20' : 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'
                                }`}>
                                  {freeTaken ? 'Taken' : 'Available'}
                                </span>
                              </div>
                              <p className={`font-mono text-[13px] mt-1.5 ${freeSelected ? 'text-white/80' : 'text-white/40'}`}>
                                open.bloby.bot/{botName}
                              </p>
                            </button>
                          );
                        })()}

                        {/* ── Premium tier block ── */}
                        {(() => {
                          const premAvail = tierAvailability['premium'];
                          const premTaken = premAvail === false;
                          const premReserved = premTaken && tierReserved['premium'];
                          return (
                            <div className={`rounded-xl border transition-all duration-200 text-left px-4 py-3 ${
                              premReserved
                                ? 'border-[#0069FE]/20'
                                : 'border-white/[0.06]'
                            }`}>
                              <div className="flex items-center justify-between">
                                <div className="flex items-center gap-2">
                                  <span className="text-[12px] font-semibold text-white/60 uppercase tracking-wider">Premium</span>
                                  <span className="text-[11px] font-medium px-2.5 py-0.5 rounded-full border bg-[#0069FE]/15 text-[#0069FE] border-[#0069FE]/20">$5</span>
                                </div>
                                <span className={`text-[11px] font-medium px-2.5 py-0.5 rounded-full border ${
                                  premReserved
                                    ? 'bg-[#0069FE]/10 text-[#0069FE] border-[#0069FE]/20'
                                    : premTaken
                                      ? 'bg-red-500/10 text-red-400 border-red-500/20'
                                      : 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'
                                }`}>
                                  {premReserved ? 'Reserved' : premTaken ? 'Taken' : 'Available'}
                                </span>
                              </div>
                              <p className="font-mono text-[13px] mt-1.5 text-white/40">
                                bloby.bot/{botName}
                              </p>

                              {/* Premium available — purchase on website */}
                              {premAvail && (
                                <div className="mt-3 pt-3 border-t border-white/[0.06] flex items-center justify-between">
                                  <p className="text-[12px] text-white/40">Purchase on bloby.bot</p>
                                  <a
                                    href="https://www.bloby.bot/#reserve"
                                    target="_blank"
                                    rel="noopener noreferrer"
                                    className="text-[12px] font-medium text-[#0069FE] hover:text-[#3391FF] transition-colors"
                                  >
                                    Purchase
                                  </a>
                                </div>
                              )}

                              {/* Premium taken but reserved by user — activation */}
                              {premReserved && (
                                <div className="mt-3 pt-3 border-t border-[#0069FE]/10">
                                  {!showClaimInput ? (
                                    <div className="flex items-center justify-between">
                                      <p className="text-[12px] text-white/40">Is that yours?</p>
                                      <button
                                        onClick={() => { setShowClaimInput(true); setClaimCode(''); setClaimError(''); }}
                                        className="text-[12px] font-medium text-[#0069FE] hover:text-[#3391FF] transition-colors"
                                      >
                                        Activate
                                      </button>
                                    </div>
                                  ) : (
                                    <div className="space-y-2.5">
                                      <p className="text-[12px] text-white/40">
                                        Enter the 5-character code from your <span className="text-white/60 font-medium">bloby.bot</span> account
                                      </p>
                                      <div className="flex gap-2">
                                        <input
                                          type="text"
                                          value={claimCode}
                                          onChange={(e) => setClaimCode(e.target.value.trim())}
                                          maxLength={5}
                                          placeholder="e.g. a3Kx9"
                                          spellCheck={false}
                                          autoComplete="off"
                                          autoFocus
                                          className="flex-1 bg-white/[0.05] border border-white/[0.08] text-white rounded-lg px-3 py-2 text-[13px] font-mono outline-none focus:border-[#0069FE]/30 transition-colors placeholder:text-white/20 tracking-widest text-center"
                                        />
                                        <button
                                          onClick={onClaimReserved}
                                          disabled={claiming || claimCode.length < 5}
                                          className="px-4 py-2 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-semibold rounded-lg transition-colors flex items-center gap-1.5 disabled:opacity-40"
                                        >
                                          {claiming ? <LoaderCircle className="h-3.5 w-3.5 animate-spin" /> : 'Activate'}
                                        </button>
                                      </div>
                                      {claimError && (
                                        <p className="text-red-400 text-[12px]">{claimError}</p>
                                      )}
                                      <button
                                        onClick={() => { setShowClaimInput(false); setClaimCode(''); setClaimError(''); }}
                                        className="text-[11px] text-white/25 hover:text-white/40 transition-colors"
                                      >
                                        Cancel
                                      </button>
                                    </div>
                                  )}
                                </div>
                              )}
                            </div>
                          );
                        })()}

                        {/* No handle — use random tunnel URL */}
                        <button
                          onClick={() => { setHandleChoice('tunnel'); setSelectedTier('skip'); }}
                          className={`w-full rounded-xl border transition-all duration-200 text-left px-4 py-3 ${
                            selectedTier === 'skip'
                              ? 'border-[#0069FE]/30 bg-white/[0.04]'
                              : 'border-white/[0.06] hover:border-white/10 hover:bg-white/[0.02]'
                          }`}
                        >
                          <span className="text-[12px] font-semibold text-white/60 uppercase tracking-wider">No handle</span>
                          <p className={`text-[13px] mt-1.5 ${selectedTier === 'skip' ? 'text-white/60' : 'text-white/30'}`}>
                            I'll use the random tunnel URL
                          </p>
                        </button>
                      </div>
                    )}

                    {/* Registered success (after claiming) */}
                    {registered && (
                      <div className="mt-4 bg-emerald-500/8 border border-emerald-500/15 rounded-xl px-4 py-3">
                        <div className="flex items-center gap-2">
                          <Check className="h-4 w-4 text-emerald-400" />
                          <p className="text-emerald-400/90 text-[13px] font-medium">Handle claimed!</p>
                        </div>
                        <p className="text-emerald-400/60 text-[12px] mt-1 font-mono">{registeredUrl}</p>
                      </div>
                    )}

                    {/* Action button — grayed out until user picks an option */}
                    {handleStatus === 'ready' && botName.length > 0 && !registered && (
                      <button
                        onClick={
                          selectedTier === 'skip' ? next
                          : handleChoice === 'relay' && selectedTier === 'at' ? (showChangeConfirm ? onChangeHandle : onClaimHandle)
                          : undefined
                        }
                        disabled={!selectedTier || registering || changingHandle || (selectedTier === 'at' && !tierAvailability['at'])}
                        className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40 disabled:cursor-not-allowed"
                      >
                        {(registering || changingHandle) ? (
                          <><LoaderCircle className="h-4 w-4 animate-spin" />{showChangeConfirm ? 'Changing...' : 'Claiming...'}</>
                        ) : !selectedTier ? (
                          <>Select an option to continue</>
                        ) : selectedTier === 'skip' ? (
                          <>Continue<ArrowRight className="h-4 w-4" /></>
                        ) : (
                          <>{showChangeConfirm ? 'Change Handle' : 'Claim & Continue'}<ArrowRight className="h-4 w-4" /></>
                        )}
                      </button>
                    )}

                    {/* Continue after claim (onboarding only — settings is hub-and-spoke) */}
                    {isInitialSetup && registered && (
                      <button
                        onClick={next}
                        className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
                      >
                        Continue
                        <ArrowRight className="h-4 w-4" />
                      </button>
                    )}

                    {/* Cancel change */}
                    {showChangeConfirm && !registered && (
                      <button
                        onClick={() => {
                          setShowChangeConfirm(false);
                          setBotName(existingHandle!.username);
                          setRegistered(true);
                          setRegisteredUrl(existingHandle!.url);
                          setSelectedTier(existingHandle!.tier);
                          setHandleChoice('relay');
                          setHandleStatus(null);
                        }}
                        className="w-full mt-2 py-2 text-white/25 hover:text-white/40 text-[12px] transition-colors"
                      >
                        Cancel — keep current handle
                      </button>
                    )}
                  </>
                )}

                {/* ── Tailscale-only switch (only when re-running wizard) ── */}
                {!isInitialSetup && (
                  <>
                    {/* Access badge */}
                    <div className="mt-5 flex items-center gap-2">
                      <span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-medium border ${
                        isPrivateAccess(accessMethod)
                          ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'
                          : 'bg-blue-500/10 text-blue-400 border-blue-500/20'
                      }`}>
                        {isPrivateAccess(accessMethod) ? <Wifi className="h-3 w-3" /> : <Globe className="h-3 w-3" />}
                        Accessing via {ACCESS_LABELS[accessMethod]}
                      </span>
                    </div>

                    {/* Switch to private network only */}
                    {!showTailscaleSwitch && (
                      <button
                        onClick={() => {
                          if (isPrivateAccess(accessMethod)) {
                            setShowTailscaleSwitch(true);
                            setTailscaleConfirmText('');
                            setTailscaleSwitchError('');
                          }
                        }}
                        disabled={!isPrivateAccess(accessMethod)}
                        className={`w-full mt-3 flex items-center justify-between px-4 py-3 rounded-xl border transition-all text-left ${
                          isPrivateAccess(accessMethod)
                            ? 'border-white/[0.06] bg-white/[0.02] hover:border-white/10 hover:bg-white/[0.04] cursor-pointer'
                            : 'border-white/[0.04] bg-transparent opacity-50 cursor-not-allowed'
                        }`}
                      >
                        <div>
                          <p className="text-[13px] text-white/70 font-medium">Switch to private network only</p>
                          <p className="text-[11px] text-white/30 mt-0.5">
                            {isPrivateAccess(accessMethod)
                              ? 'Stop the tunnel and relay — access only via Tailscale or LAN'
                              : 'Connect via Tailscale or private network to unlock'}
                          </p>
                        </div>
                        <WifiOff className="h-4 w-4 text-white/30 shrink-0 ml-3" />
                      </button>
                    )}

                    {/* Confirmation flow */}
                    {showTailscaleSwitch && (
                      <div className="mt-3 space-y-3">
                        <div className="bg-amber-500/8 border border-amber-500/20 rounded-xl px-4 py-3">
                          <div className="flex items-start gap-2">
                            <TriangleAlert className="h-4 w-4 text-amber-400 shrink-0 mt-0.5" />
                            <div>
                              <p className="text-amber-400/90 text-[13px] font-medium">Switch to private network only?</p>
                              <p className="text-amber-400/60 text-[12px] mt-1 leading-relaxed">
                                This will stop the Cloudflare tunnel and relay connection. Your bot will only be accessible via your private network.
                              </p>
                              {existingHandle && (
                                <p className="text-amber-400/50 text-[12px] mt-1.5">
                                  Your handle will be preserved and can be re-activated later.
                                </p>
                              )}
                            </div>
                          </div>
                        </div>
                        <div>
                          <label className="text-[12px] text-white/40 font-medium mb-1.5 block">
                            Type <span className="font-mono text-white/60">I confirm</span> to proceed
                          </label>
                          <input
                            type="text"
                            value={tailscaleConfirmText}
                            onChange={(e) => setTailscaleConfirmText(e.target.value)}
                            placeholder="I confirm"
                            spellCheck={false}
                            autoFocus
                            className={inputSmCls}
                          />
                        </div>
                        {tailscaleSwitchError && (
                          <p className="text-red-400/70 text-[12px]">{tailscaleSwitchError}</p>
                        )}
                        <div className="flex gap-2">
                          <button
                            onClick={async () => {
                              if (!onTunnelSwitch || tailscaleConfirmText.trim().toLowerCase() !== 'i confirm') return;
                              setTailscaleSwitching(true);
                              setTailscaleSwitchError('');
                              try {
                                await onTunnelSwitch('off');
                                setTunnelMode('off');
                                setShowTailscaleSwitch(false);
                              } catch (err: any) {
                                setTailscaleSwitchError(err.message || 'Failed to switch');
                              } finally {
                                setTailscaleSwitching(false);
                              }
                            }}
                            disabled={tailscaleSwitching || tailscaleConfirmText.trim().toLowerCase() !== 'i confirm' || !onTunnelSwitch}
                            className="flex-1 py-2.5 bg-amber-600 hover:bg-amber-500 text-white text-[13px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
                          >
                            {tailscaleSwitching ? <><LoaderCircle className="h-4 w-4 animate-spin" />Switching...</> : 'Confirm Switch'}
                          </button>
                          <button
                            onClick={() => { setShowTailscaleSwitch(false); setTailscaleConfirmText(''); setTailscaleSwitchError(''); }}
                            disabled={tailscaleSwitching}
                            className="px-5 py-2.5 bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.08] text-white/60 text-[13px] font-medium rounded-full transition-colors"
                          >
                            Cancel
                          </button>
                        </div>
                      </div>
                    )}
                  </>
                )}
              </div>
            )}

            {/* ── Step 3: Password + 2FA (sub-phase flow) ── */}
            {step === 3 && (
              <div>
                <AnimatePresence mode="wait">
                  {/* ── Phase: Password ── */}
                  {step3Phase === 'password' && (
                    <motion.div
                      key="password"
                      initial={{ opacity: 0, x: -20 }}
                      animate={{ opacity: 1, x: 0 }}
                      exit={{ opacity: 0, x: -20 }}
                      transition={{ duration: 0.15 }}
                    >
                      <h1 className="text-xl font-bold text-white tracking-tight">
                        {portalExists ? 'Password & 2FA' : 'Set a password'}
                      </h1>
                      <p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
                        {portalExists
                          ? "Your Bloby Chat password is already set — you can keep it as-is or change it below."
                          : "You'll need this password to access your Bloby Chat. Keep it safe — anyone with your URL will need it to log in."}
                      </p>

                      {/* ── Existing-password collapsed state: just a "Change password" pill ── */}
                      {portalExists && !portalChangeMode && (
                        <div className="mt-5 flex items-center justify-between bg-white/[0.02] border border-white/[0.06] rounded-xl px-4 py-3">
                          <div>
                            <p className="text-white/70 text-[13px] font-medium">Bloby Chat Password</p>
                            <p className="text-white/30 text-[11px] mt-0.5">Already configured.</p>
                          </div>
                          <button
                            type="button"
                            onClick={() => {
                              setPortalChangeMode(true);
                              setPortalOldPass('');
                              setPortalOldPassError('');
                              setPortalOldPassVerified(false);
                              setPortalPass('');
                              setPortalPassConfirm('');
                            }}
                            className="shrink-0 px-3.5 py-2 bg-white/[0.04] hover:bg-white/[0.08] text-white/70 hover:text-white text-[12px] font-medium rounded-lg transition-colors"
                          >
                            Change password
                          </button>
                        </div>
                      )}

                      {/* ── Existing-password expanded: current password verify step ── */}
                      {portalExists && portalChangeMode && (
                        <div className="mt-5">
                          <div className="flex items-center justify-between mb-1.5">
                            <label className="text-[12px] text-white/40 font-medium">Current password</label>
                            <button
                              type="button"
                              onClick={() => {
                                setPortalChangeMode(false);
                                setPortalOldPass('');
                                setPortalOldPassError('');
                                setPortalOldPassVerified(false);
                                setPortalPass('');
                                setPortalPassConfirm('');
                              }}
                              className="text-white/30 hover:text-white/60 text-[11px] transition-colors"
                            >
                              Cancel
                            </button>
                          </div>
                          <div className="flex items-center gap-2">
                            <input
                              type="password"
                              value={portalOldPass}
                              onChange={(e) => { setPortalOldPass(e.target.value); setPortalOldPassError(''); setPortalOldPassVerified(false); }}
                              placeholder="Enter your current password"
                              autoComplete="current-password"
                              className={inputCls + ' flex-1'}
                            />
                            {portalOldPass.length > 0 && !portalOldPassVerified && (
                              <button
                                onClick={async () => {
                                  setPortalVerifying(true);
                                  setPortalOldPassError('');
                                  try {
                                    const res = await fetch('/api/portal/verify-password', {
                                      method: 'POST',
                                      headers: { 'Content-Type': 'application/json' },
                                      body: JSON.stringify({ password: portalOldPass }),
                                    });
                                    const data = await res.json();
                                    if (data.valid) {
                                      setPortalOldPassVerified(true);
                                    } else {
                                      setPortalOldPassError('Incorrect password');
                                    }
                                  } catch {
                                    setPortalOldPassError('Could not verify');
                                  } finally {
                                    setPortalVerifying(false);
                                  }
                                }}
                                disabled={portalVerifying}
                                className="shrink-0 px-4 py-3 bg-white/[0.06] hover:bg-white/[0.1] text-white/60 text-[13px] font-medium rounded-xl transition-colors disabled:opacity-40"
                              >
                                {portalVerifying ? <LoaderCircle className="h-4 w-4 animate-spin" /> : 'Verify'}
                              </button>
                            )}
                            {portalOldPassVerified && (
                              <div className="shrink-0 w-10 h-10 flex items-center justify-center">
                                <Check className="h-4 w-4 text-emerald-400" />
                              </div>
                            )}
                          </div>
                          {portalOldPassError && (
                            <p className="text-red-400/70 text-[11px] mt-1">{portalOldPassError}</p>
                          )}
                        </div>
                      )}

                      {/* ── New-password fields: first-time onboard OR verified change-mode ── */}
                      {(!portalExists || (portalChangeMode && portalOldPassVerified)) && (
                        <>
                          <div className={portalExists ? 'mt-3' : 'mt-5'}>
                            <label className="text-[12px] text-white/40 font-medium mb-1.5 block">
                              {portalExists ? 'New password' : 'Password'}
                            </label>
                            <input
                              type="password"
                              value={portalPass}
                              onChange={(e) => setPortalPass(e.target.value)}
                              placeholder="••••••••"
                              autoComplete="new-password"
                              autoFocus={!portalExists}
                              onKeyDown={handleKeyDown}
                              className={inputCls}
                            />
                            {portalPass.length > 0 && portalPass.length < 6 && (
                              <p className="text-amber-400/70 text-[11px] mt-1">At least 6 characters</p>
                            )}
                          </div>

                          <div className="mt-3">
                            <label className="text-[12px] text-white/40 font-medium mb-1.5 block">
                              {portalExists ? 'Confirm new password' : 'Confirm password'}
                            </label>
                            <input
                              type="password"
                              value={portalPassConfirm}
                              onChange={(e) => setPortalPassConfirm(e.target.value)}
                              placeholder="••••••••"
                              autoComplete="new-password"
                              onKeyDown={handleKeyDown}
                              className={inputCls}
                            />
                            {portalPassConfirm.length > 0 && !portalPassMatch && (
                              <p className="text-red-400/70 text-[11px] mt-1">Passwords don't match</p>
                            )}
                          </div>
                        </>
                      )}

                      {/* ── 2FA toggle card ── */}
                      <div className="mt-5 border border-white/[0.06] rounded-xl overflow-hidden">
                        <button
                          onClick={async () => {
                            if (totpEnabled && totpVerified) {
                              setTotpDisabling(true);
                              return;
                            }
                            if (!totpEnabled) {
                              // Enable → go to setup phase
                              setTotpEnabled(true);
                              setTotpError('');
                              setTotpCode('');
                              // Enabling 2FA on an existing portal requires the current
                              // password to authenticate the setup call. If the user hasn't
                              // entered it yet, open change-mode so the current-password
                              // field becomes visible and surface a clear error.
                              if (portalExists && !portalOldPassVerified) {
                                setPortalChangeMode(true);
                                setTotpError('Enter your current portal password above first, then toggle 2FA again.');
                                setTotpEnabled(false);
                                return;
                              }
                              // Fetch QR if not already loaded
                              if (!totpQrUri) {
                                try {
                                  const res = await fetch('/api/portal/totp/setup', {
                                    method: 'POST',
                                    headers: { 'Content-Type': 'application/json' },
                                    body: JSON.stringify({ password: portalExists ? portalOldPass : portalPass }),
                                  });
                                  const data = await res.json();
                                  if (res.ok) {
                                    setTotpSecret(data.secret);
                                    setTotpQrUri(data.qrDataUri);
                                    setTotpOtpauthUri(data.otpauthUri);
                                    setStep3Phase('totp-setup');
                                  } else {
                                    setTotpError(data.error || 'Setup failed');
                                    setTotpEnabled(false);
                                  }
                                } catch {
                                  setTotpError('Could not reach server');
                                  setTotpEnabled(false);
                                }
                              } else {
                                setStep3Phase('totp-setup');
                              }
                            } else {
                              // Disable (setup incomplete)
                              setTotpEnabled(false);
                              setTotpQrUri('');
                              setTotpSecret('');
                              setTotpOtpauthUri('');
                            }
                          }}
                          type="button"
                          className="w-full flex items-center gap-3 px-4 py-3 text-left"
                        >
                          <div className={`w-9 h-9 rounded-lg flex items-center justify-center shrink-0 ${totpEnabled && totpVerified ? 'bg-emerald-500/10' : 'bg-white/[0.04]'}`}>
                            {totpEnabled && totpVerified ? (
                              <ShieldCheck className="h-[18px] w-[18px] text-emerald-400" />
                            ) : (
                              <Shield className="h-[18px] w-[18px] text-white/30" />
                            )}
                          </div>
                          <div className="flex-1 min-w-0">
                            <span className="text-[13px] font-medium text-white block">Two-Factor Auth</span>
                            <p className="text-[11px] text-white/30 mt-0.5">
                              {totpEnabled && totpVerified ? (
                                <span className="text-emerald-400/70">Active — authenticator app required</span>
                              ) : (tunnelMode === 'quick' || tunnelMode === 'named') ? (
                                <span>Recommended for public bots</span>
                              ) : (
                                <span>Extra security with an authenticator app</span>
                              )}
                            </p>
                          </div>
                          <div className={`w-10 h-6 rounded-full transition-colors relative shrink-0 ${totpEnabled ? 'bg-emerald-500' : 'bg-white/10'}`}>
                            <div className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${totpEnabled ? 'translate-x-5' : 'translate-x-1'}`} />
                          </div>
                        </button>

                        {/* Inline disable flow */}
                        {totpDisabling && (
                          <div className="px-4 pb-3 border-t border-white/[0.06]">
                            <p className="text-[12px] text-white/40 mt-3 mb-2">Enter your current TOTP code to disable 2FA:</p>
                            <div className="flex items-center gap-2">
                              <input
                                type="text"
                                inputMode="numeric"
                                autoComplete="one-time-code"
                                maxLength={6}
                                value={totpDisableCode}
                                onChange={(e) => { setTotpDisableCode(e.target.value.replace(/\D/g, '')); setTotpDisableError(''); }}
                                placeholder="000000"
                                className={inputSmCls + ' flex-1 tracking-[0.3em] text-center font-mono'}
                              />
                              <button
                                onClick={async () => {
                                  if (totpDisableCode.length !== 6) return;
                                  try {
                                    const res = await fetch('/api/portal/totp/disable', {
                                      method: 'POST',
                                      headers: { 'Content-Type': 'application/json' },
                                      body: JSON.stringify({ password: portalExists ? portalOldPass : portalPass, code: totpDisableCode }),
                                    });
                                    const data = await res.json();
                                    if (res.ok) {
                                      setTotpEnabled(false);
                                      setTotpVerified(false);
                                      setTotpSecret('');
                                      setTotpQrUri('');
                                      setTotpOtpauthUri('');
                                      setTotpDisabling(false);
                                      setTotpDisableCode('');
                                      setRecoveryCodes([]);
                                      setRecoveryCodesCopied(false);
                                    } else {
                                      setTotpDisableError(data.error || 'Failed to disable');
                                    }
                                  } catch {
                                    setTotpDisableError('Could not reach server');
                                  }
                                }}
                                disabled={totpDisableCode.length !== 6}
                                className="shrink-0 px-4 py-2.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 text-[13px] font-medium rounded-xl transition-colors disabled:opacity-40"
                              >
                                Disable
                              </button>
                            </div>
                            {totpDisableError && <p className="text-red-400/70 text-[11px] mt-1">{totpDisableError}</p>}
                            <button onClick={() => { setTotpDisabling(false); setTotpDisableCode(''); setTotpDisableError(''); }} className="text-[11px] text-white/30 hover:text-white/50 mt-2">Cancel</button>
                          </div>
                        )}

                        {totpError && step3Phase === 'password' && (
                          <div className="mx-4 mb-3 bg-red-500/8 border border-red-500/15 rounded-xl px-3 py-2">
                            <p className="text-red-400/90 text-[11px]">{totpError}</p>
                          </div>
                        )}
                      </div>

                      {isInitialSetup && (
                      <button
                        onClick={next}
                        disabled={!canNext}
                        className="w-full mt-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
                      >
                        Continue
                        <ArrowRight className="h-4 w-4" />
                      </button>
                      )}
                    </motion.div>
                  )}

                  {/* ── Phase: TOTP Setup (QR + verify) ── */}
                  {step3Phase === 'totp-setup' && (
                    <motion.div
                      key="totp-setup"
                      initial={{ opacity: 0, x: 20 }}
                      animate={{ opacity: 1, x: 0 }}
                      exit={{ opacity: 0, x: 20 }}
                      transition={{ duration: 0.15 }}
                    >
                      <div className="flex items-center gap-3 mb-1">
                        <button
                          onClick={() => {
                            setTotpEnabled(false);
                            setTotpQrUri('');
                            setTotpSecret('');
                            setTotpOtpauthUri('');
                            setTotpCode('');
                            setTotpError('');
                            setStep3Phase('password');
                            clearTotpStorage();
                          }}
                          className="w-7 h-7 rounded-full bg-white/[0.04] flex items-center justify-center text-white/40 hover:text-white/70 transition-colors shrink-0"
                        >
                          <ArrowLeft className="h-4 w-4" />
                        </button>
                        <h1 className="text-xl font-bold text-white tracking-tight">Set up 2FA</h1>
                      </div>

                      {totpError && (
                        <div className="mt-3 bg-red-500/8 border border-red-500/15 rounded-xl px-3 py-2">
                          <p className="text-red-400/90 text-[11px]">{totpError}</p>
                        </div>
                      )}

                      {totpQrUri && (
                        <>
                          {/* Desktop: horizontal QR + instructions | Mobile: authenticator link */}
                          {isMobileDevice ? (
                            <div className="mt-4">
                              <p className="text-white/40 text-[13px] leading-relaxed mb-4">
                                Add Bloby to your authenticator app, then enter the 6-digit code below to confirm.
                              </p>

                              {/* Primary: copy secret key (doesn't leave the app) */}
                              <button
                                onClick={() => {
                                  navigator.clipboard.writeText(totpSecret);
                                  setTotpSecretCopied(true);
                                  setTimeout(() => setTotpSecretCopied(false), 3000);
                                }}
                                className={`w-full py-3 text-[14px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 ${
                                  totpSecretCopied
                                    ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
                                    : 'bg-white/[0.05] text-white/70 hover:bg-white/[0.08] border border-white/[0.08]'
                                }`}
                              >
                                {totpSecretCopied ? (
                                  <><Check className="h-4 w-4" />Secret key copied</>
                                ) : (
                                  <><Copy className="h-4 w-4" />Copy secret key</>
                                )}
                              </button>
                              <p className="text-[11px] text-white/20 text-center mt-2">
                                Paste it in your authenticator app → Add account → Enter key
                              </p>

                              {/* Secondary: deep link (saves state before leaving) */}
                              <a
                                href={totpOtpauthUri}
                                onClick={() => saveTotpState()}
                                className="w-full mt-3 py-2.5 text-[13px] text-white/30 hover:text-white/50 flex items-center justify-center gap-1.5 transition-colors"
                              >
                                <Smartphone className="h-3.5 w-3.5" />
                                Or open directly in authenticator
                              </a>
                            </div>
                          ) : (
                            <div className="mt-4 flex gap-4 items-start">
                              {/* QR Code — compact */}
                              <div className="shrink-0">
                                <div className="bg-white rounded-xl p-1.5">
                                  <img src={totpQrUri} alt="TOTP QR Code" className="w-[140px] h-[140px]" />
                                </div>
                              </div>
                              {/* Instructions */}
                              <div className="flex-1 min-w-0 pt-1">
                                <p className="text-white/40 text-[13px] leading-relaxed">
                                  Scan this QR code with your authenticator app.
                                </p>
                                <p className="text-white/25 text-[11px] mt-2 leading-relaxed">
                                  Google Authenticator, Authy, 1Password, or any TOTP app.
                                </p>
                                <button
                                  onClick={() => { navigator.clipboard.writeText(totpSecret); }}
                                  className="mt-3 text-[11px] text-white/25 hover:text-white/40 flex items-center gap-1 transition-colors"
                                >
                                  <Copy className="h-3 w-3" />
                                  Copy secret key
                                </button>
                              </div>
                            </div>
                          )}

                          {/* Verification input */}
                          <div className="mt-5">
                            <label className="text-[12px] text-white/40 font-medium mb-1.5 block">Enter the 6-digit code from your app</label>
                            <div className="flex items-center gap-2">
                              <input
                                type="text"
                                inputMode="numeric"
                                autoComplete="one-time-code"
                                maxLength={6}
                                value={totpCode}
                                onChange={(e) => { setTotpCode(e.target.value.replace(/\D/g, '')); setTotpError(''); }}
                                placeholder="000000"
                                autoFocus
                                className={inputCls + ' tracking-[0.3em] text-center font-mono flex-1'}
                              />
                              <button
                                onClick={async () => {
                                  if (totpCode.length !== 6) return;
                                  setTotpVerifying(true);
                                  setTotpError('');
                                  try {
                                    const res = await fetch('/api/portal/totp/verify-setup', {
                                      method: 'POST',
                                      headers: { 'Content-Type': 'application/json' },
                                      body: JSON.stringify({ code: totpCode, password: portalExists ? portalOldPass : portalPass }),
                                    });
                                    const data = await res.json();
                                    if (res.ok && data.success) {
                                      setTotpVerified(true);
                                      setRecoveryCodes(data.recoveryCodes || []);
                                      setStep3Phase('recovery');
                                      clearTotpStorage();
                                    } else {
                                      setTotpError(data.error || 'Verification failed');
                                    }
                                  } catch {
                                    setTotpError('Could not reach server');
                                  } finally {
                                    setTotpVerifying(false);
                                  }
                                }}
                                disabled={totpCode.length !== 6 || totpVerifying}
                                className="shrink-0 px-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-xl transition-colors flex items-center gap-2 disabled:opacity-40"
                              >
                                {totpVerifying ? <LoaderCircle className="h-4 w-4 animate-spin" /> : 'Verify'}
                              </button>
                            </div>
                          </div>
                        </>
                      )}
                    </motion.div>
                  )}

                  {/* ── Phase: Recovery codes ── */}
                  {step3Phase === 'recovery' && (
                    <motion.div
                      key="recovery"
                      initial={{ opacity: 0, x: 20 }}
                      animate={{ opacity: 1, x: 0 }}
                      exit={{ opacity: 0, x: 20 }}
                      transition={{ duration: 0.15 }}
                    >
                      <div className="flex items-center gap-3 mb-1">
                        <div className="w-9 h-9 rounded-xl bg-emerald-500/10 flex items-center justify-center shrink-0">
                          <ShieldCheck className="h-[18px] w-[18px] text-emerald-400" />
                        </div>
                        <div>
                          <h1 className="text-xl font-bold text-white tracking-tight">2FA enabled</h1>
                          <p className="text-emerald-400/70 text-[12px]">Save your recovery codes</p>
                        </div>
                      </div>

                      <p className="text-white/35 text-[13px] mt-3 leading-relaxed">
                        If you lose your authenticator app, you can use one of these codes to sign in. Each code works once. Store them somewhere safe.
                      </p>

                      <div className="mt-4 grid grid-cols-2 gap-1.5">
                        {recoveryCodes.map((code, i) => (
                          <div key={i} className="bg-white/[0.03] border border-white/[0.06] rounded-lg px-3 py-2 text-center">
                            <code className="text-[13px] text-white/60 font-mono tracking-wider">{code}</code>
                          </div>
                        ))}
                      </div>

                      <button
                        onClick={() => {
                          navigator.clipboard.writeText(recoveryCodes.join('\n'));
                          setRecoveryCodesCopied(true);
                        }}
                        className={`w-full mt-4 py-3 text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 ${
                          recoveryCodesCopied
                            ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
                            : 'bg-white/[0.05] text-white/60 hover:bg-white/[0.08] border border-white/[0.08]'
                        }`}
                      >
                        {recoveryCodesCopied ? (
                          <><Check className="h-4 w-4" />Copied</>
                        ) : (
                          <><Copy className="h-4 w-4" />Copy recovery codes</>
                        )}
                      </button>

                      <button
                        onClick={() => {
                          setRecoveryCodes([]);
                          setStep3Phase('password');
                          clearTotpStorage();
                        }}
                        disabled={!recoveryCodesCopied}
                        className="w-full mt-3 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
                      >
                        Done
                        <ArrowRight className="h-4 w-4" />
                      </button>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>
            )}

            {/* ── Step 4: Provider + Auth + Model ── */}
            {step === 4 && (
              <div>
                <h1 className="text-xl font-bold text-white tracking-tight">
                  Choose your AI provider
                </h1>
                <p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
                  Pick one provider to power your bot, authenticate, and select a model.
                </p>

                {/* Provider cards */}
                <div className="flex gap-2.5 mt-4">
                  {PROVIDERS.map((p) => (
                    <button
                      key={p.id}
                      onClick={() => handleProviderChange(p.id)}
                      className={`flex-1 relative rounded-xl border transition-all duration-200 p-3 text-left ${
                        provider === p.id
                          ? 'bg-white/[0.04] border-[#0069FE]/40'
                          : 'bg-transparent border-white/[0.06] hover:border-white/10 hover:bg-white/[0.02]'
                      }`}
                    >
                      <div className="flex flex-col items-center gap-1.5 py-0.5">
                        {(() => {
                          const px = (p as any).iconHeight ?? BASE_ICON_HEIGHT;
                          // Reserve the tallest possible row so cards align even when
                          // one logo is bumped up a few px for optical balance.
                          const rowHeight = Math.max(BASE_ICON_HEIGHT, px);
                          return (
                            <div
                              className="flex items-center justify-center"
                              style={{ height: `${rowHeight}px` }}
                            >
                              {p.icon ? (
                                <img
                                  src={p.icon}
                                  alt={p.name}
                                  className="w-auto object-contain"
                                  style={{ height: `${px}px` }}
                                />
                              ) : (
                                <div
                                  className="rounded-lg bg-white/[0.06] flex items-center justify-center text-white/50 text-sm font-bold"
                                  style={{ height: `${px}px`, width: `${px}px` }}
                                />
                              )}
                            </div>
                          );
                        })()}
                        <div className="text-center">
                          <div className="text-[13px] font-medium text-white">{p.name}</div>
                          <div className="text-[10px] text-white/30 whitespace-pre-line leading-tight">{p.subtitle}</div>
                        </div>
                      </div>
                      {authState[p.id] === 'connected' ? (
                        <div className="absolute top-2 right-2 w-4 h-4 rounded-full bg-emerald-500/15 flex items-center justify-center">
                          <Check className="h-2.5 w-2.5 text-emerald-400" />
                        </div>
                      ) : provider === p.id ? (
                        <div className="absolute top-2 right-2 w-2 h-2 rounded-full bg-gradient-brand" />
                      ) : null}
                    </button>
                  ))}
                </div>

                <div className="border-t border-white/[0.06] mt-4 mb-3" />

                {/* ── Auth flow: Pi (bring your own model) ── */}
                {provider === 'pi' && (
                  <div className="space-y-2">
                    {authState.pi === 'connected' && piSavedStatus ? (
                      <div className="flex items-center justify-between bg-emerald-500/8 border border-emerald-500/15 rounded-lg px-3 py-2">
                        <p className="text-emerald-400/90 text-[12px] truncate">
                          Connected — {piSubProviders.find((p) => p.id === piSavedStatus.subProvider)?.name || piSavedStatus.subProvider}
                          {piSavedStatus.modelId ? <> · <span className="font-mono">{piSavedStatus.modelId}</span></> : null}
                        </p>
                        <button
                          onClick={handlePiDisconnect}
                          className="text-white/30 hover:text-white/60 text-[11px] flex items-center gap-1 shrink-0 ml-2"
                        >
                          <RefreshCw className="h-3 w-3" />
                          Change
                        </button>
                      </div>
                    ) : (
                      <>
                        {/* Two-column compact row: provider dropdown + model picker */}
                        <div className="grid grid-cols-2 gap-2">
                          <div>
                            <label className="text-[11px] text-white/40 font-medium mb-1 block">Provider</label>
                            <ModelDropdown
                              models={piSubProviders.map((sp) => ({ id: sp.id, label: sp.name }))}
                              value={piSubProvider}
                              onChange={choosePiSubProvider}
                            />
                          </div>
                          <div>
                            <label className="text-[11px] text-white/40 font-medium mb-1 block">Model</label>
                            {selectedPiSub && Array.isArray(selectedPiSub.models) ? (
                              <ModelDropdown
                                models={selectedPiSub.models}
                                value={piModelId}
                                onChange={setPiModelId}
                              />
                            ) : (
                              <input
                                type="text"
                                value={piModelId}
                                onChange={(e) => setPiModelId(e.target.value)}
                                placeholder={selectedPiSub?.defaultModel || 'model-id'}
                                className={inputSmCls + ' font-mono'}
                              />
                            )}
                          </div>
                        </div>

                        {selectedPiSub?.needsBaseUrl && (
                          <input
                            type="text"
                            value={piBaseUrl}
                            onChange={(e) => setPiBaseUrl(e.target.value)}
                            placeholder={selectedPiSub.baseUrl || 'https://example.com/v1'}
                            className={inputSmCls + ' font-mono'}
                          />
                        )}

                        {selectedPiSub?.needsApiKey && (
                          <div className="relative">
                            <input
                              type={piShowKey ? 'text' : 'password'}
                              value={piApiKey}
                              onChange={(e) => setPiApiKey(e.target.value)}
                              onKeyDown={(e) => e.key === 'Enter' && handlePiConnect()}
                              placeholder="API key…"
                              className={inputSmCls + ' pr-16 font-mono'}
                            />
                            <div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1.5">
                              {selectedPiSub.apiKeyUrl && (
                                <button
                                  type="button"
                                  onClick={() => openExternal(selectedPiSub.apiKeyUrl!)}
                                  className="text-white/25 hover:text-white/55 transition-colors"
                                  title="Get a key"
                                >
                                  <ExternalLink className="h-3.5 w-3.5" />
                                </button>
                              )}
                              <button
                                type="button"
                                onClick={() => setPiShowKey((v) => !v)}
                                className="text-white/25 hover:text-white/55 transition-colors"
                              >
                                {piShowKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
                              </button>
                            </div>
                          </div>
                        )}

                        {piError && (
                          <div className="bg-red-500/8 border border-red-500/15 rounded-lg px-3 py-2">
                            <p className="text-[12px] text-red-400/90 break-words">{piError}</p>
                          </div>
                        )}

                        <button
                          onClick={handlePiConnect}
                          disabled={
                            piConnecting ||
                            !piSubProvider ||
                            (!!selectedPiSub?.needsApiKey && !piApiKey.trim()) ||
                            (!!selectedPiSub?.needsBaseUrl && !piBaseUrl.trim()) ||
                            !piModelId.trim()
                          }
                          className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
                        >
                          {piConnecting ? (
                            <><LoaderCircle className="h-3.5 w-3.5 animate-spin" />Connecting…</>
                          ) : (
                            <>Test &amp; connect<ArrowRight className="h-3.5 w-3.5 opacity-60" /></>
                          )}
                        </button>
                      </>
                    )}
                  </div>
                )}

                {/* ── Auth flow: Bloby (coming soon placeholder) ── */}
                {provider === 'bloby' && (
                  <div className="space-y-3">
                    <p className="text-[12px] text-white/40 leading-relaxed">
                      Bloby (managed) is on the way. Sign in with Google once it ships and your bot will be hosted, updated, and billed by us — no API keys to manage.
                    </p>
                    <button
                      type="button"
                      disabled
                      title="Coming soon"
                      className="w-full py-2.5 px-4 bg-white/[0.04] border border-white/[0.06] text-white/35 text-[13px] font-medium rounded-xl flex items-center justify-center gap-2 cursor-not-allowed"
                    >
                      <svg viewBox="0 0 24 24" className="h-4 w-4 opacity-50" fill="currentColor">
                        <path d="M21.35 11.1h-9.17v2.96h5.27c-.23 1.39-1.62 4.08-5.27 4.08-3.17 0-5.76-2.62-5.76-5.85s2.59-5.85 5.76-5.85c1.81 0 3.02.77 3.71 1.43l2.53-2.43C16.85 3.91 14.74 3 12.18 3 7.03 3 2.86 7.17 2.86 12.29s4.17 9.29 9.32 9.29c5.38 0 8.94-3.79 8.94-9.12 0-.61-.07-1.08-.17-1.55Z"/>
                      </svg>
                      Login with Google
                    </button>
                    <p className="text-[10.5px] text-white/25 text-center">Not available yet.</p>
                  </div>
                )}

                {/* ── Auth flow: Anthropic ── */}
                {provider === 'anthropic' && (
                  <div className="space-y-2.5">
                    {isConnected && (
                      <div className="space-y-2.5">
                        <div className="bg-emerald-500/8 border border-emerald-500/15 rounded-lg px-3.5 py-2.5">
                          <p className="text-emerald-400/90 text-[12px]">Connected — Anthropic subscription is active.</p>
                        </div>
                        <button
                          onClick={() => { setAuthState((s) => ({ ...s, anthropic: 'idle' })); setOauthStarted(false); setAnthropicCode(''); setAnthropicError(''); }}
                          className="w-full py-1.5 text-white/25 text-[11px] hover:text-white/40 transition-colors flex items-center justify-center gap-1.5"
                        >
                          <RefreshCw className="h-3 w-3" />
                          Re-authenticate
                        </button>
                      </div>
                    )}

                    {!isConnected && (
                      <>
                        {anthropicError && (
                          <div className="bg-red-500/8 border border-red-500/15 rounded-lg px-3.5 py-2.5">
                            <p className="text-red-400/90 text-[12px]">{anthropicError}</p>
                          </div>
                        )}

                        <div className="space-y-1.5">
                          {[
                            'Click the button below to open Anthropic\'s login page',
                            'Sign in with your Anthropic account — a code will be generated',
                            'Copy the code and paste it in the field below',
                          ].map((text, i) => (
                            <div key={i} className="flex items-start gap-2">
                              <span className="flex-shrink-0 w-[18px] h-[18px] rounded-full bg-white/[0.06] text-white/30 text-[10px] font-medium flex items-center justify-center mt-px">{i + 1}</span>
                              <p className="text-white/40 text-[12px] leading-relaxed">{text}</p>
                            </div>
                          ))}
                        </div>

                        <button
                          onClick={handleAnthropicAuth}
                          className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2"
                        >
                          {oauthStarted ? (
                            <><ExternalLink className="h-3.5 w-3.5 opacity-60" />Open authentication page again</>
                          ) : (
                            <>Authenticate with Anthropic<ArrowRight className="h-3.5 w-3.5 opacity-60" /></>
                          )}
                        </button>

                        <div className="relative">
                          <input
                            type="text"
                            value={anthropicCode}
                            onChange={(e) => setAnthropicCode(e.target.value)}
                            onKeyDown={(e) => e.key === 'Enter' && handleAnthropicConnect()}
                            placeholder="Paste your code here..."
                            className={inputSmCls + ' pr-10 font-mono'}
                          />
                          <button
                            onClick={handleAnthropicPaste}
                            className="absolute right-3 top-1/2 -translate-y-1/2 text-white/20 hover:text-white/50 transition-colors"
                          >
                            <ClipboardPaste className="h-3.5 w-3.5" />
                          </button>
                        </div>

                        <button
                          onClick={handleAnthropicConnect}
                          disabled={!anthropicCode.trim() || isExchanging}
                          className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
                        >
                          {isExchanging ? (<><LoaderCircle className="h-3.5 w-3.5 animate-spin" />Verifying...</>) : 'Connect'}
                        </button>

                        <button
                          onClick={handleAnthropicCheckAuth}
                          disabled={anthropicChecking}
                          className="w-full py-1.5 text-white/25 text-[11px] hover:text-white/40 transition-colors flex items-center justify-center gap-1.5 disabled:opacity-50"
                        >
                          {anthropicChecking ? (
                            <LoaderCircle className="h-3 w-3 animate-spin" />
                          ) : (
                            <RefreshCw className="h-3 w-3" />
                          )}
                          {anthropicChecking ? 'Checking...' : 'I\'m already authenticated'}
                        </button>
                      </>
                    )}
                  </div>
                )}

                {/* ── Auth flow: OpenAI ── */}
                {provider === 'openai' && (
                  <div className="space-y-2.5">
                    {isConnected && (
                      <div className="space-y-2.5">
                        <div className="bg-emerald-500/8 border border-emerald-500/15 rounded-lg px-3.5 py-2.5">
                          <p className="text-emerald-400/90 text-[12px]">Connected — ChatGPT subscription is active.</p>
                        </div>
                        <button
                          onClick={() => { setAuthState((s) => ({ ...s, openai: 'idle' })); setOpenaiError(''); }}
                          className="w-full py-1.5 text-white/25 text-[11px] hover:text-white/40 transition-colors flex items-center justify-center gap-1.5"
                        >
                          <RefreshCw className="h-3 w-3" />
                          Re-authenticate
                        </button>
                      </div>
                    )}

                    {!isConnected && (
                      <>
                        {openaiError && (
                          <div className="bg-red-500/8 border border-red-500/15 rounded-lg px-3.5 py-2.5">
                            <p className="text-red-400/90 text-[12px]">{openaiError}</p>
                          </div>
                        )}

                        {/* ── Device-code flow (default) ── */}
                        {codexFlow === 'device' && codexDeviceState !== 'pending' && (
                          <>
                            <div className="space-y-1.5">
                              {[
                                'Click below — we\'ll open auth.openai.com/codex/device for you',
                                'Sign in with your ChatGPT Plus or Pro account',
                                'Type the one-time code shown here on that page',
                              ].map((text, i) => (
                                <div key={i} className="flex items-start gap-2">
                                  <span className="flex-shrink-0 w-[18px] h-[18px] rounded-full bg-white/[0.06] text-white/30 text-[10px] font-medium flex items-center justify-center mt-px">{i + 1}</span>
                                  <p className="text-white/40 text-[12px] leading-relaxed">{text}</p>
                                </div>
                              ))}
                            </div>

                            <button
                              onClick={handleOpenAIDeviceStart}
                              disabled={codexDeviceStarting}
                              className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-60"
                            >
                              {codexDeviceStarting ? (
                                <><LoaderCircle className="h-3.5 w-3.5 animate-spin" />Requesting code...</>
                              ) : (
                                <>Sign in with ChatGPT<ArrowRight className="h-3.5 w-3.5 opacity-60" /></>
                              )}
                            </button>
                          </>
                        )}

                        {codexFlow === 'device' && codexDeviceState === 'pending' && codexUserCode && (
                          <div className="space-y-3">
                            <div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-4">
                              <p className="text-[11px] text-white/40 mb-1">1. Open this URL in any browser</p>
                              <button
                                onClick={() => openExternal(codexVerificationUrl)}
                                className="w-full text-left py-2 px-3 bg-white/[0.04] hover:bg-white/[0.07] rounded-lg text-[12px] font-mono text-white/80 flex items-center justify-between transition-colors"
                              >
                                <span className="truncate">{codexVerificationUrl}</span>
                                <ExternalLink className="h-3.5 w-3.5 ml-2 opacity-60 shrink-0" />
                              </button>

                              <p className="text-[11px] text-white/40 mt-3 mb-1">2. Enter this code</p>
                              <button
                                onClick={handleCopyUserCode}
                                className="w-full py-3 px-3 bg-white/[0.04] hover:bg-white/[0.07] rounded-lg text-center transition-colors flex items-center justify-center gap-3 group"
                              >
                                <span className="text-[20px] font-mono font-semibold text-white tracking-widest">{codexUserCode}</span>
                                {codexCodeCopied ? (
                                  <Check className="h-4 w-4 text-emerald-400" />
                                ) : (
                                  <Copy className="h-4 w-4 text-white/30 group-hover:text-white/60" />
                                )}
                              </button>

                              <div className="flex items-center justify-center gap-2 mt-3 text-[11px] text-white/40">
                                <LoaderCircle className="h-3 w-3 animate-spin" />
                                Waiting for you to approve...
                              </div>
                            </div>

                            <button
                              onClick={handleOpenAIDeviceCancel}
                              className="w-full py-1.5 text-white/25 text-[11px] hover:text-white/40 transition-colors"
                            >
                              Cancel
                            </button>
                          </div>
                        )}

                        {/* ── Paste-back flow (fallback) ── */}
                        {codexFlow === 'paste' && (
                          <>
                            <div className="space-y-1.5">
                              {[
                                'Click below to open ChatGPT sign-in',
                                'Sign in with your ChatGPT Plus or Pro account',
                                'Your browser will say "site can\'t be reached" — that\'s expected. Copy the FULL URL from the address bar and paste it below.',
                              ].map((text, i) => (
                                <div key={i} className="flex items-start gap-2">
                                  <span className="flex-shrink-0 w-[18px] h-[18px] rounded-full bg-white/[0.06] text-white/30 text-[10px] font-medium flex items-center justify-center mt-px">{i + 1}</span>
                                  <p className="text-white/40 text-[12px] leading-relaxed">{text}</p>
                                </div>
                              ))}
                            </div>

                            <button
                              onClick={handleOpenAIAuth}
                              className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2"
                            >
                              {codexOauthStarted ? (
                                <><ExternalLink className="h-3.5 w-3.5 opacity-60" />Open authentication page again</>
                              ) : (
                                <>Authenticate with ChatGPT<ArrowRight className="h-3.5 w-3.5 opacity-60" /></>
                              )}
                            </button>

                            <div className="relative">
                              <input
                                type="text"
                                value={codexCode}
                                onChange={(e) => setCodexCode(e.target.value)}
                                onKeyDown={(e) => e.key === 'Enter' && handleOpenAIConnect()}
                                placeholder="Paste callback URL or code here..."
                                className={inputSmCls + ' pr-10 font-mono'}
                              />
                              <button
                                onClick={handleOpenAIPaste}
                                className="absolute right-3 top-1/2 -translate-y-1/2 text-white/20 hover:text-white/50 transition-colors"
                              >
                                <ClipboardPaste className="h-3.5 w-3.5" />
                              </button>
                            </div>

                            <button
                              onClick={handleOpenAIConnect}
                              disabled={!codexCode.trim() || codexExchanging}
                              className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
                            >
                              {codexExchanging ? (<><LoaderCircle className="h-3.5 w-3.5 animate-spin" />Verifying...</>) : 'Connect'}
                            </button>
                          </>
                        )}

                        {/* ── Flow toggle + already-authenticated ── */}
                        {codexDeviceState !== 'pending' && (
                          <div className="flex items-center justify-between pt-1">
                            <button
                              onClick={() => {
                                setOpenaiError(undefined);
                                setCodexFlow(codexFlow === 'device' ? 'paste' : 'device');
                              }}
                              className="text-white/25 text-[11px] hover:text-white/40 transition-colors"
                            >
                              {codexFlow === 'device' ? 'Use callback URL instead' : 'Use device code instead'}
                            </button>
                            <button
                              onClick={handleOpenAICheckAuth}
                              disabled={codexChecking}
                              className="text-white/25 text-[11px] hover:text-white/40 transition-colors flex items-center gap-1.5 disabled:opacity-50"
                            >
                              {codexChecking ? (
                                <LoaderCircle className="h-3 w-3 animate-spin" />
                              ) : (
                                <RefreshCw className="h-3 w-3" />
                              )}
                              {codexChecking ? 'Checking...' : 'I\'m already authenticated'}
                            </button>
                          </div>
                        )}
                      </>
                    )}
                  </div>
                )}

                {/* ── Model dropdown (after auth) — pi flow renders its own model picker inside its block ── */}
                {isConnected && provider !== 'pi' && (
                  <>
                    <div className="border-t border-white/[0.06] mt-4 mb-3" />
                    <label className="text-[12px] text-white/40 font-medium mb-1.5 block">
                      Select a model
                    </label>
                    <ModelDropdown
                      models={MODELS[provider] || []}
                      value={model}
                      onChange={setModel}
                    />
                  </>
                )}

                {isInitialSetup && isConnected && (
                  <button
                    onClick={next}
                    disabled={!canNext}
                    className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
                  >
                    Continue
                    <ArrowRight className="h-4 w-4" />
                  </button>
                )}
              </div>
            )}

            {/* ── Step 5: Voice Messages + Complete ── */}
            {step === 5 && (
              <div>
                <h1 className="text-xl font-bold text-white tracking-tight mb-1">
                  Voice Messages
                </h1>
                <p className="text-white/40 text-[13px] mt-1 leading-relaxed">
                  Voice input works out of the box using your browser's built-in speech recognition. For better accuracy, you can optionally enable OpenAI Whisper.
                </p>

                {/* Browser Speech Recognition card */}
                <div className="w-full mt-5 rounded-xl border border-emerald-500/20 bg-emerald-500/[0.04] p-4">
                  <div className="flex items-center gap-3.5">
                    <div className="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center shrink-0">
                      <Mic className="h-5 w-5 text-emerald-400" />
                    </div>
                    <div className="flex-1">
                      <div className="text-[14px] font-medium text-white">Browser Speech Recognition</div>
                      <div className="text-[12px] text-white/35 mt-0.5 leading-relaxed">
                        Built-in voice input — no setup needed. Works in Chrome, Edge, and Safari.
                      </div>
                    </div>
                    <div className="flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500/20 shrink-0">
                      <Check className="h-3.5 w-3.5 text-emerald-400" />
                    </div>
                  </div>
                </div>

                {/* Whisper upgrade toggle */}
                <button
                  onClick={() => setWhisperEnabled((v) => !v)}
                  className={`w-full mt-3 rounded-xl border transition-all duration-200 p-4 text-left ${
                    whisperEnabled
                      ? 'bg-white/[0.04] border-[#0069FE]/40'
                      : 'bg-transparent border-white/[0.06] hover:border-white/10 hover:bg-white/[0.02]'
                  }`}
                >
                  <div className="flex items-center gap-3.5">
                    <img src="/icons/openai.svg" alt="OpenAI" className="w-10 h-10 rounded-xl bg-white/[0.04] p-1.5" />
                    <div className="flex-1">
                      <div className="text-[14px] font-medium text-white">OpenAI Whisper</div>
                      <div className="text-[12px] text-white/35 mt-0.5 leading-relaxed">
                        Upgrade to more accurate transcription that works in all browsers including Firefox.
                      </div>
                    </div>
                    <div className={`w-10 h-[22px] rounded-full transition-colors duration-200 flex items-center px-0.5 shrink-0 ${
                      whisperEnabled ? 'bg-gradient-brand' : 'bg-white/[0.08]'
                    }`}>
                      <div className={`w-[18px] h-[18px] rounded-full bg-white shadow-sm transition-transform duration-200 ${
                        whisperEnabled ? 'translate-x-[18px]' : 'translate-x-0'
                      }`} />
                    </div>
                  </div>
                </button>

                {whisperEnabled && (
                  <div className="mt-3">
                    <label className="text-[12px] text-white/40 font-medium mb-1.5 block">OpenAI API Key</label>
                    <input
                      type="password"
                      value={whisperKey}
                      onChange={(e) => setWhisperKey(e.target.value.trim())}
                      placeholder="sk-..."
                      autoComplete="off"
                      className={inputCls + ' font-mono text-[13px]'}
                    />
                    {whisperKey.length > 0 && !whisperKey.startsWith('sk-') && (
                      <p className="text-amber-400/70 text-[11px] mt-1">Key should start with sk-</p>
                    )}
                    {whisperKey.length > 0 && whisperKey.startsWith('sk-') && whisperKey.length < 20 && (
                      <p className="text-amber-400/70 text-[11px] mt-1">Key looks too short</p>
                    )}
                    <div className="flex items-start gap-2.5 mt-3 bg-white/[0.02] border border-white/[0.06] rounded-xl px-4 py-3">
                      <Mic className="h-4 w-4 text-[#0069FE]/60 mt-0.5 shrink-0" />
                      <p className="text-white/35 text-[12px] leading-relaxed">
                        Whisper provides more accurate transcription and works in all browsers including Firefox.
                      </p>
                    </div>
                  </div>
                )}

                {isInitialSetup && (
                  <button
                    onClick={handleComplete}
                    disabled={saving || (whisperEnabled && (!whisperKey.startsWith('sk-') || whisperKey.length < 20))}
                    className="w-full mt-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
                  >
                    {saving ? (
                      <><LoaderCircle className="h-4 w-4 animate-spin" />Setting up...</>
                    ) : (
                      <>Complete Setup<ArrowRight className="h-4 w-4" /></>
                    )}
                  </button>
                )}

                {!whisperEnabled && (
                  <p className="text-center text-white/20 text-[11px] mt-2.5">
                    Voice input is active using your browser's built-in speech recognition.
                  </p>
                )}
              </div>
            )}

            {/* ── Step 6: All Set (initial onboard only) ── */}
            {step === 6 && isInitialSetup && (() => {
              const isPrivate = tunnelMode === 'off';
              const finalUrl = (() => {
                if (isPrivate) return window.location.origin;
                if (tunnelMode === 'named') return `https://${tunnelDomain}`;
                if (handleChoice === 'relay' && registeredUrl) return registeredUrl;
                return tunnelUrl || `http://localhost:${3000}`;
              })();
              const finalUrlFull = finalUrl.startsWith('http') ? finalUrl : `https://${finalUrl}`;
              const descriptionText = isPrivate
                ? 'Your agent is running on your private network. Access it from any device on your local network or VPN.'
                : tunnelMode === 'named'
                  ? 'Access your agent at your custom domain.'
                  : handleChoice === 'relay' && registeredUrl
                    ? 'Your agent is live and ready. From now on, access it using your custom URL below.'
                    : 'Your agent is live and ready. Your tunnel URL is shown below. Note: this URL changes on restart.';
              return (
              <div className="flex flex-col items-center text-center">
                <div className="w-16 h-16 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center mb-5">
                  <Check className="h-8 w-8 text-emerald-400" />
                </div>
                <h1 className="text-2xl font-bold text-white tracking-tight">
                  All Set!
                </h1>
                <p className="text-white/40 text-[13px] mt-2 leading-relaxed max-w-[340px]">
                  {descriptionText}
                </p>

                {/* URL */}
                <div className="w-full mt-6 flex items-center gap-2 bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-3">
                  <span className="font-mono text-[13px] text-white/70 truncate flex-1 text-left">{finalUrl}</span>
                  <button
                    onClick={() => {
                      navigator.clipboard.writeText(finalUrlFull);
                      setPortalCopied(true);
                      setTimeout(() => setPortalCopied(false), 2000);
                    }}
                    className="shrink-0 text-white/30 hover:text-white/60 transition-colors"
                  >
                    {portalCopied ? (
                      <Check className="h-4 w-4 text-emerald-400" />
                    ) : (
                      <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
                        <rect x="9" y="9" width="13" height="13" rx="2" />
                        <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
                      </svg>
                    )}
                  </button>
                </div>

                {/* Password display with eye toggle */}
                <div className="w-full mt-3">
                  <label className="text-[12px] text-white/40 font-medium mb-1.5 block text-left">Your password</label>
                  <div className="flex items-center gap-2 bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-3">
                    <span className="font-mono text-[13px] text-white/70 truncate flex-1 text-left">
                      {showPassAllSet ? portalPass : '\u2022'.repeat(Math.max(portalPass.length, 8))}
                    </span>
                    <button
                      onClick={() => setShowPassAllSet((v) => !v)}
                      className="shrink-0 text-white/30 hover:text-white/60 transition-colors"
                    >
                      {showPassAllSet ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
                    </button>
                  </div>
                </div>

                {/* Terms & Privacy checkbox */}
                <label className="flex items-start gap-2.5 mt-6 cursor-pointer text-left select-none">
                  <input
                    type="checkbox"
                    checked={acceptedTerms}
                    onChange={(e) => setAcceptedTerms(e.target.checked)}
                    className="mt-0.5 h-4 w-4 rounded border-white/20 bg-white/5 accent-emerald-500 shrink-0"
                  />
                  <span className="text-[12px] text-white/50 leading-relaxed">
                    I accept the{' '}
                    <a href="https://www.bloby.bot/terms" target="_blank" rel="noopener noreferrer" className="text-white/70 underline hover:text-white/90">terms</a>
                    {' '}and{' '}
                    <a href="https://www.bloby.bot/privacy" target="_blank" rel="noopener noreferrer" className="text-white/70 underline hover:text-white/90">privacy policy</a>
                  </span>
                </label>

                {/* Redirect / done button */}
                <button
                  disabled={!acceptedTerms}
                  onClick={() => {
                    if (isPrivate) {
                      (window.top || window).location.href = '/';
                    } else {
                      (window.top || window).location.href = finalUrlFull;
                    }
                  }}
                  className={`w-full mt-4 py-3 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 ${acceptedTerms ? 'bg-gradient-brand hover:opacity-90' : 'bg-white/10 cursor-not-allowed opacity-50'}`}
                >
                  {isPrivate ? 'Go to dashboard' : 'Go to your agent'}
                  <ExternalLink className="h-4 w-4" />
                </button>

                <p className="text-white/20 text-[11px] mt-3 leading-relaxed">
                  {isPrivate
                    ? 'Access from any device on your network using the URL above.'
                    : `You'll be redirected to your ${tunnelMode === 'named' ? 'custom domain' : handleChoice === 'relay' ? 'custom URL' : 'tunnel URL'}.`
                  }
                </p>
              </div>
              );
            })()}

            {/* ── Step 6: Environment Variables (settings re-run only) ── */}
            {step === 6 && !isInitialSetup && <EnvSettings cacheRef={envCacheRef} />}

            {/* ── Step 7: Pulse & Crons (settings re-run only) ── */}
            {step === 7 && !isInitialSetup && <PulseCronSettings cacheRef={pulseCacheRef} />}
          </motion.div>
        </AnimatePresence>
        </div>

        {/* Back link — onboarding only; settings mode navigates via the header. */}
        {isInitialSetup && step > 0 && step < TOTAL_STEPS - 1 && (
          <div className="px-8 pb-5 pt-2 shrink-0">
            <button
              onClick={back}
              className="text-white/25 hover:text-white/50 text-[12px] transition-colors"
            >
              &larr; Back
            </button>
          </div>
        )}

        {/* Settings mode: per-screen Save — sticky at the bottom, shown only when the current
            screen has changes. (Env step 6 / Pulse & Crons step 7 save themselves inside.) */}
        {!isInitialSetup && step >= 1 && step <= 5 && (settingsScreenDirty || settingsSaving || settingsSaved || settingsSaveError) && (
          <div className="px-8 pb-6 pt-3 shrink-0 border-t border-white/[0.06] bg-[#181818]">
            {settingsSaveError && (
              <p className="text-red-400 text-[12px] mb-2 flex items-center gap-1.5">
                <TriangleAlert className="h-3.5 w-3.5 shrink-0" />{settingsSaveError}
              </p>
            )}
            {settingsSaved && !settingsScreenDirty ? (
              <div className="flex items-center justify-center gap-2 py-3 text-emerald-400 text-[13px] font-medium">
                <Check className="h-4 w-4" /> Saved
              </div>
            ) : (
              <button
                onClick={saveSettings}
                disabled={settingsSaving || !settingsScreenDirty}
                className="w-full py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40 disabled:cursor-not-allowed"
              >
                {settingsSaving ? (
                  <><LoaderCircle className="h-4 w-4 animate-spin" />Saving...</>
                ) : (
                  <><Check className="h-4 w-4" />Save changes</>
                )}
              </button>
            )}
          </div>
        )}
      </motion.div>
    </div>
  );
}
