// ============================================================================
// Sandbox app shell — chrome (top nav, sidebar, view switching), guided tour,
// and the MAC chat dock. MAC posts to ATAAH's /chat/sandbox endpoint with the
// active trade (HVAC | GC) so the operator sees a persona-consistent reply.
// ============================================================================

const { useState, useRef, useEffect } = React;

// ----------------------------- LAUNCH OVERLAY --------------------------------
// Trade picker — drives the active demo company + MAC's system prompt.
// HVAC ('Aire Technology') and GC ('Riggs General Contracting') are
// the two C-Phase-1 personas; both reach the same /chat/sandbox endpoint
// on the ATAAH backend with the chosen trade.
const TRADE_CARDS = [
  {
    id: 'hvac',
    name: 'HVAC Contractor',
    company: 'Aire Technology, Inc.',
    blurb: 'Florida HVAC contractor. Service + retrofit jobs, light AIA billing, equipment-heavy.',
    job: 'JOB#41209 Lakeside HVAC Retrofit · $246,902 contract',
  },
  {
    id: 'gc',
    name: 'General Contractor',
    company: 'Riggs General Contracting, LLC',
    blurb: 'Florida GC. AIA G702/G703, retention, subs + lien waivers, larger crews.',
    job: 'JOB#41310 Magnolia Park Townhomes Ph2 · $1.84M contract',
  },
];

const SXLaunch = ({ onLaunch }) => {
  const [trade, setTrade] = useState('hvac');
  const [withTour, setWithTour] = useState(true);

  return (
  <div style={{
    minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center',
    background: 'var(--ink-900)', padding: 24, position: 'relative', overflow: 'hidden',
  }}>
    <div style={{ position: 'absolute', inset: 0,
      backgroundImage: 'linear-gradient(rgba(59,130,246,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(59,130,246,0.04) 1px, transparent 1px)',
      backgroundSize: '48px 48px' }}/>
    <div style={{
      position: 'relative', maxWidth: 720, padding: '56px 48px',
      background: 'var(--ink-800)', border: '1px solid var(--line-strong)',
    }}>
      <div style={{ position: 'absolute', top: 0, left: 0, width: 18, height: 18, borderTop: '2px solid var(--signal)', borderLeft: '2px solid var(--signal)' }}/>
      <div style={{ position: 'absolute', top: 0, right: 0, width: 18, height: 18, borderTop: '2px solid var(--signal)', borderRight: '2px solid var(--signal)' }}/>
      <div style={{ position: 'absolute', bottom: 0, left: 0, width: 18, height: 18, borderBottom: '2px solid var(--signal)', borderLeft: '2px solid var(--signal)' }}/>
      <div style={{ position: 'absolute', bottom: 0, right: 0, width: 18, height: 18, borderBottom: '2px solid var(--signal)', borderRight: '2px solid var(--signal)' }}/>

      <div style={{ display: 'flex', alignItems: 'center', gap: 12, color: 'var(--paper)', marginBottom: 22 }}>
        <HarnessMark size={40} pulse />
        <div>
          <div className="disp" style={{ fontSize: 18, lineHeight: 0.9 }}>THE BUILDERS<br/>HARNESS</div>
          <Mono style={{ fontSize: 9, color: 'var(--steel-500)', letterSpacing: '0.18em', marginTop: 4, display: 'block' }}>SANDBOX · LIVE MAC · DEMO DATA</Mono>
        </div>
      </div>

      <Tick style={{ display: 'block', marginBottom: 10 }}>PICK YOUR TRADE</Tick>
      <h1 className="disp" style={{ fontSize: 'clamp(30px, 5vw, 52px)', color: 'var(--paper)', margin: 0, lineHeight: 0.95, letterSpacing: '-0.005em' }}>
        Which contractor<br/>are you <span style={{ color: 'var(--signal)' }}>today</span>?
      </h1>
      <p style={{ color: 'var(--paper-dim)', fontSize: 14, lineHeight: 1.55, marginTop: 14, maxWidth: '60ch' }}>
        Same harness, two trades. The sandbox flips between an HVAC specialist and a general contractor so you can see how the agents and queue adapt. <span style={{ color: 'var(--paper)' }}>You can ask MAC anything in the chat dock — live, not canned.</span>
      </p>

      <div style={{ marginTop: 24, display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
        {TRADE_CARDS.map(c => (
          <button key={c.id} onClick={() => setTrade(c.id)} style={{
            background: trade === c.id ? 'rgba(59,130,246,0.08)' : 'var(--ink-900)',
            border: trade === c.id ? '1px solid var(--signal)' : '1px solid var(--line-strong)',
            padding: '16px 16px 14px',
            cursor: 'pointer',
            textAlign: 'left',
            color: 'var(--paper)',
            position: 'relative',
            fontFamily: 'inherit',
          }}>
            {trade === c.id && <span style={{ position: 'absolute', top: 8, right: 10, fontFamily: 'var(--font-mono)', fontSize: 9, color: 'var(--signal)', letterSpacing: '0.14em' }}>✓ SELECTED</span>}
            <Mono style={{ fontSize: 9, color: 'var(--steel-500)', letterSpacing: '0.16em' }}>{c.name.toUpperCase()}</Mono>
            <div className="disp" style={{ fontSize: 16, marginTop: 6, lineHeight: 1.1 }}>{c.company}</div>
            <div style={{ fontSize: 12, color: 'var(--paper-dim)', marginTop: 10, lineHeight: 1.45 }}>{c.blurb}</div>
            <Mono style={{ fontSize: 9, color: 'var(--signal)', letterSpacing: '0.10em', marginTop: 10, display: 'block' }}>{c.job}</Mono>
          </button>
        ))}
      </div>

      <div style={{ marginTop: 22, display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
        <Btn kind="primary" onClick={() => onLaunch({ trade, withTour })} style={{ flex: '1 1 auto', padding: '14px 22px', fontFamily: 'var(--font-mono)', fontSize: 12, letterSpacing: '0.16em', textTransform: 'uppercase' }}>
          <span style={{ width: 8, height: 8, background: 'var(--paper)', display: 'inline-block', marginRight: 10 }}/>
          LAUNCH SANDBOX
        </Btn>
        <label style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'var(--paper-dim)', fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.10em', cursor: 'pointer', userSelect: 'none' }}>
          <input type="checkbox" checked={withTour} onChange={e => setWithTour(e.target.checked)} />
          GUIDED TOUR
        </label>
      </div>
      <Mono style={{ fontSize: 9, color: 'var(--steel-500)', letterSpacing: '0.18em', display: 'block', marginTop: 18 }}>
        ← <a href="index.html" style={{ color: 'var(--paper-dim)', textDecoration: 'none' }}>BACK TO THE SITE</a>
      </Mono>
    </div>
  </div>
  );
};

// ----------------------------- TOP NAV ---------------------------------------
const SXTopNav = ({ view, setView, openChat, restartTour }) => (
  <nav style={{
    position: 'sticky', top: 0, zIndex: 20,
    background: 'rgba(14,22,32,0.94)', backdropFilter: 'blur(8px)',
    borderBottom: '1px solid var(--line)',
  }}>
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 24px', gap: 18, flexWrap: 'wrap' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
        <span style={{ color: 'var(--paper)' }}><HarnessMark size={22} pulse /></span>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
          <span className="disp" style={{ fontSize: 14, color: 'var(--paper)' }}>BUILDERS HARNESS</span>
          <Mono style={{ fontSize: 9, color: 'var(--steel-500)' }}>/SANDBOX · AIRE TECH · NODE-DEMO</Mono>
        </div>
      </div>
      <div style={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
        {[
          ['briefing', 'Briefing'],
          ['queue',    'Approvals'],
          ['estimate', 'Estimate'],
          ['co',       'CO'],
          ['wo',       'WO'],
          ['job',      'Job'],
          ['field',    'Field'],
          ['audit',    'Audit'],
        ].map(([id, label]) => (
          <button key={id} onClick={() => setView(id)} data-tour-id={`nav-${id}`} style={{
            background: view === id ? 'rgba(59,130,246,0.12)' : 'transparent',
            color: view === id ? 'var(--paper)' : 'var(--paper-dim)',
            border: 0, borderBottom: `2px solid ${view === id ? 'var(--signal)' : 'transparent'}`,
            padding: '8px 14px', fontFamily: 'var(--font-narrow)', fontSize: 13, fontWeight: 500,
            textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer',
          }}>{label}</button>
        ))}
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <Btn onClick={restartTour}>Replay tour</Btn>
        <Mono style={{ fontSize: 9, color: 'var(--steel-500)', letterSpacing: '0.14em', textAlign: 'right' }}>
          tom@aire-tech.com<br/>NODE-DEMO · LIVE
        </Mono>
        <Dot color="var(--green)" pulse />
      </div>
    </div>
  </nav>
);

// ----------------------------- TOUR ------------------------------------------
const TOUR_STEPS = [
  { id: 'briefing-kpis',  view: 'briefing', target: null,
    title: '06:00 — your morning briefing.',
    body: 'Cash position, AR aging, WIP, gross margin — all drafted by the harness against your live QBO and bank feeds. Status dots tell you what to look at first.' },
  { id: 'briefing-cta',   view: 'briefing', target: null,
    title: 'The 13-week cash forecast.',
    body: 'agent.cfo runs this overnight. Weeks below your minimum target are flagged with ⨯. wk03 and wk04 need attention — see why.' },
  { id: 'queue-row',      view: 'queue',    target: 'first-row',
    title: 'Five drafted actions.',
    body: 'Every overnight agent run lands here. Tier 1 is routine, tier 4 is owner-only. Approve, reject, or click Edit to see the JSON payload that will post to QuickBooks.' },
  { id: 'job-detail',     view: 'job',      target: null,
    title: 'A job under the hood.',
    body: 'Budget vs. actual by phase. Recent activity (every approve, propose, sync). Retention math. Subs are amber — variance routes to the PM.' },
  { id: 'audit',          view: 'audit',    target: null,
    title: 'The audit log is the receipt.',
    body: 'Every approve, reject, edit, agent run, QBO sync writes one append-only row. UPDATE and DELETE are blocked at the DB layer. Bonded? Bond audit ready out of the box.' },
];

const Tour = ({ step, setView, onNext, onSkip }) => {
  if (step == null) return null;
  const s = TOUR_STEPS[step];
  React.useEffect(() => { if (s) setView(s.view); }, [step]);
  if (!s) return null;
  return (
    <div style={{
      position: 'fixed', bottom: 24, right: 24, zIndex: 50,
      width: 420, maxWidth: 'calc(100vw - 32px)',
      background: 'var(--ink-800)', border: '1px solid var(--signal)',
      padding: '20px 22px',
      boxShadow: '0 30px 80px rgba(0,0,0,0.5)',
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
        <Mono style={{ fontSize: 10, color: 'var(--signal)', letterSpacing: '0.16em', textTransform: 'uppercase' }}>
          STEP {step + 1} / {TOUR_STEPS.length}
        </Mono>
        <button onClick={onSkip} style={{ background: 'transparent', border: 0, color: 'var(--steel-500)', fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.14em', cursor: 'pointer' }}>SKIP TOUR ×</button>
      </div>
      <h3 className="disp" style={{ fontSize: 22, color: 'var(--paper)', margin: '0 0 8px', lineHeight: 1.05, letterSpacing: '-0.005em' }}>{s.title}</h3>
      <p style={{ fontSize: 13, color: 'var(--paper-dim)', lineHeight: 1.5, margin: 0 }}>{s.body}</p>
      <div style={{ display: 'flex', gap: 8, marginTop: 16, alignItems: 'center', justifyContent: 'space-between' }}>
        <div style={{ display: 'flex', gap: 4 }}>
          {TOUR_STEPS.map((_, i) => (
            <span key={i} style={{ width: 22, height: 3, background: i <= step ? 'var(--signal)' : 'var(--line-strong)' }}/>
          ))}
        </div>
        <Btn kind="primary" onClick={onNext}>
          {step === TOUR_STEPS.length - 1 ? 'Done · explore on your own' : 'Next →'}
        </Btn>
      </div>
    </div>
  );
};

// ----------------------------- MAC CHAT --------------------------------------
// The MAC persona + trade-specific system prompts live on the ATAAH backend
// at apps/api/chat/sandbox_routes.py — the sandbox just passes {trade,messages}
// and renders the reply.

const SANDBOX_API_BASE = 'https://app.thebuildersharness.com/chat/sandbox';
const SANDBOX_API_URL = SANDBOX_API_BASE;

const TRADE_GREETING = {
  hvac: "Morning Tom. 5 in your queue — 2 awaiting you (#1039 variance + #1041 cfo digest). What do you want to look at?",
  gc:   "Morning Tom. 6 in your queue — 3 awaiting you (#1042 retention release · #1044 CO-008 · #1045 sub COI lapse). What do you want to look at?",
};

const TRADE_SUGGESTIONS = {
  hvac: ['What is JOB#41209 looking like?', 'Draft a CO for 220V drop, $2400 mat + 6h', 'Why is wk04 below target?'],
  gc:   ['What is JOB#41310 looking like?', 'Draft a CO for window upgrade, $14,200', 'Retention status across active jobs'],
};

// Adapt /chat/sandbox/queue rows to the canned-approval shape so they
// can be merged into the existing `approvals` state and rendered by
// SXQueue / SXBriefing without code changes.
function adaptSessionRow(r) {
  return {
    id: r.id,                   // big number (90000+) — distinguishes from canned
    kind: r.kind,
    tier: 2,
    department: r.kind.startsWith('co.')  ? 'pm'
              : r.kind.startsWith('ar.')  ? 'ar'
              : r.kind.startsWith('ap.')  ? 'ap'
              : 'other',
    status: r.status,
    summary: r.decision_notes || `(${r.kind})`,
    amount: r.amount_cents ? `$${(r.amount_cents / 100).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}` : '—',
    amount_cents: r.amount_cents || 0,
    created_by: 'sandbox.mac',
    at: 'this session',
    payload: {},
    dri: 'Tom Riggs',
    _sandbox: true,            // marker used by the approve handler + UI badge
  };
}

const SXMacChat = ({ open, setOpen, data, trade, sessionId, setSessionId, onQueued }) => {
  const [messages, setMessages] = useState([
    { role: 'assistant', content: TRADE_GREETING[trade] || TRADE_GREETING.hvac },
  ]);
  const [input, setInput] = useState('');
  const [busy, setBusy]   = useState(false);
  const scroll = useRef(null);

  useEffect(() => { scroll.current?.scrollTo({ top: 1e9, behavior: 'smooth' }); }, [messages, open]);

  async function send() {
    const text = input.trim();
    if (!text || busy) return;
    const next = [...messages, { role: 'user', content: text }];
    setMessages(next);
    setInput('');
    setBusy(true);
    try {
      const resp = await fetch(SANDBOX_API_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          trade,
          session_id: sessionId || null,
          // Last 12 turns is plenty for context; keeps payload small + within
          // /chat/sandbox's 40-message cap.
          messages: next.slice(-12).map(m => ({ role: m.role, content: m.content })),
        }),
      });
      if (!resp.ok) {
        const detail = resp.status === 429
          ? "Too many messages — give MAC a few minutes."
          : `MAC backend returned ${resp.status}. Try again in a moment.`;
        setMessages([...next, { role: 'assistant', content: detail }]);
        return;
      }
      const body = await resp.json();
      // Latch the server-minted session_id on first call.
      if (body.session_id && body.session_id !== sessionId) {
        setSessionId(body.session_id);
        try { sessionStorage.setItem('tbh-sandbox-session', body.session_id); } catch (e) {}
      }
      setMessages([...next, { role: 'assistant', content: (body.reply || '').trim() || '[empty reply]' }]);
      // If MAC drafted something this turn, pull the live queue rows
      // and merge them into the operator's approval list.
      if (Array.isArray(body.queued_rows) && body.queued_rows.length > 0 && onQueued) {
        onQueued(body.session_id || sessionId);
      }
    } catch (e) {
      setMessages([...next, { role: 'assistant', content: 'Lost the node for a sec — try again.' }]);
    } finally { setBusy(false); }
  }

  if (!open) {
    return (
      <button onClick={() => setOpen(true)} style={{
        position: 'fixed', bottom: 24, right: 24, zIndex: 30,
        background: 'var(--signal)', color: 'var(--paper)',
        border: 0, padding: '14px 22px',
        fontFamily: 'var(--font-mono)', fontSize: 12, letterSpacing: '0.16em', textTransform: 'uppercase',
        cursor: 'pointer', boxShadow: '0 16px 40px rgba(59,130,246,0.35)',
        display: 'flex', alignItems: 'center', gap: 10,
      }}>
        <Dot color="var(--paper)" pulse />
        ASK MAC
      </button>
    );
  }

  return (
    <div style={{
      position: 'fixed', bottom: 24, right: 24, zIndex: 30,
      width: 420, maxWidth: 'calc(100vw - 32px)', height: 540, maxHeight: 'calc(100vh - 48px)',
      background: 'var(--ink-800)', border: '1px solid var(--line-strong)',
      display: 'flex', flexDirection: 'column',
      boxShadow: '0 30px 80px rgba(0,0,0,0.6)',
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 14px', borderBottom: '1px solid var(--line)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <span style={{ color: 'var(--paper)' }}><HarnessMark size={20} pulse /></span>
          <div>
            <div className="disp" style={{ fontSize: 14, color: 'var(--paper)' }}>MAC</div>
            <Mono style={{ fontSize: 8, color: 'var(--steel-500)', letterSpacing: '0.18em' }}>MULTI-AGENT COMPANION · LIVE</Mono>
          </div>
        </div>
        <button onClick={() => setOpen(false)} style={{ background: 'transparent', border: 0, color: 'var(--steel-500)', fontFamily: 'var(--font-mono)', fontSize: 12, cursor: 'pointer' }}>×</button>
      </div>

      <div ref={scroll} style={{ flex: 1, overflow: 'auto', padding: '14px 14px 4px', display: 'flex', flexDirection: 'column', gap: 10 }}>
        {messages.map((m, i) => (
          <div key={i} style={{
            alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start',
            maxWidth: '86%',
            background: m.role === 'user' ? 'var(--signal)' : 'var(--ink-900)',
            color: m.role === 'user' ? 'var(--paper)' : 'var(--paper)',
            border: m.role === 'user' ? 0 : '1px solid var(--line)',
            padding: '10px 12px', fontSize: 13, lineHeight: 1.5, whiteSpace: 'pre-wrap',
          }}>{m.content}</div>
        ))}
        {busy && (
          <div style={{ alignSelf: 'flex-start', display: 'flex', gap: 6, padding: '10px 12px' }}>
            <Dot color="var(--signal)" pulse />
            <Mono style={{ fontSize: 10, color: 'var(--steel-500)', letterSpacing: '0.14em' }}>MAC IS DRAFTING…</Mono>
          </div>
        )}
      </div>

      {messages.length <= 2 && (
        <div style={{ padding: '4px 14px 8px', display: 'flex', flexWrap: 'wrap', gap: 6 }}>
          {(TRADE_SUGGESTIONS[trade] || TRADE_SUGGESTIONS.hvac).map(q => (
            <button key={q} onClick={() => { setInput(q); setTimeout(send, 50); }} style={{
              background: 'transparent', border: '1px solid var(--line-strong)', color: 'var(--paper-dim)',
              padding: '5px 10px', fontFamily: 'var(--font-mono)', fontSize: 9, letterSpacing: '0.10em', textTransform: 'uppercase', cursor: 'pointer',
            }}>{q}</button>
          ))}
        </div>
      )}

      <div style={{ padding: '10px 14px', borderTop: '1px solid var(--line)', display: 'flex', gap: 8 }}>
        <input
          value={input} onChange={e => setInput(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && send()}
          placeholder="Ask MAC anything…"
          style={{
            flex: 1, background: 'var(--ink-900)', border: '1px solid var(--line-strong)', color: 'var(--paper)',
            padding: '9px 12px', fontFamily: 'var(--font-body)', fontSize: 13, outline: 'none', borderRadius: 0,
          }}
        />
        <Btn kind="primary" onClick={send} disabled={busy}>Send</Btn>
      </div>
    </div>
  );
};

// ----------------------------- APP -------------------------------------------
const SXApp = () => {
  const data0 = window.SANDBOX_DATA;
  const [launched,  setLaunched]  = useState(false);
  const [trade,     setTrade]     = useState('hvac');
  // Phase 3 sandbox-session UUID; persisted across page reloads so a
  // visitor's queued items survive a tab refresh inside the 60-min TTL.
  const [sessionId, setSessionId] = useState(() => {
    try { return sessionStorage.getItem('tbh-sandbox-session') || null; } catch (e) { return null; }
  });
  const [tourStep,  setTourStep]  = useState(null);
  const [view,      setView]      = useState('briefing');
  const [approvals, setApprovals] = useState(data0.approvals);
  const [audit,     setAudit]     = useState(data0.audit_log);
  const [expanded,  setExpanded]  = useState(null);
  const [chatOpen,  setChatOpen]  = useState(false);

  // Hydrate any pre-existing session rows on launch (e.g. tab refresh
  // inside the 60-min TTL).
  useEffect(() => {
    if (launched && sessionId) refreshSessionQueue(sessionId);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [launched]);

  // Pulls fresh session-scoped approvals and merges them into the
  // operator's queue. Called after MAC drafts something via tool-call.
  async function refreshSessionQueue(sid) {
    if (!sid) return;
    try {
      const resp = await fetch(`${SANDBOX_API_BASE}/queue?session_id=${sid}`);
      if (!resp.ok) return;
      const body = await resp.json();
      const adapted = (body.rows || []).map(adaptSessionRow);
      setApprovals(prev => {
        // Drop any prior sandbox rows; replace with the fresh server snapshot.
        const canned = prev.filter(a => !a._sandbox);
        return [...adapted, ...canned];
      });
    } catch (e) { /* benign — keep the canned queue visible */ }
  }

  function decide(id, status) {
    // Sandbox-tagged rows (big id) get the server-side decide call;
    // canned rows stay client-side.
    const row = approvals.find(a => a.id === id);
    if (row && row._sandbox && sessionId) {
      fetch(`${SANDBOX_API_BASE}/decide`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ session_id: sessionId, approval_id: id, decision: status }),
      }).then(() => refreshSessionQueue(sessionId)).catch(() => {});
      return;
    }
    setApprovals(prev => prev.map(a => a.id === id ? { ...a, status } : a));
    const a = approvals.find(x => x.id === id);
    if (a) {
      const now = new Date();
      const at = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')} PT`;
      setAudit(prev => [
        { at, kind: `${status}.user`, actor: 'tom@aire-tech.com', msg: `#${id} · ${a.kind} · ${a.amount}` },
        ...prev,
      ]);
      if (status === 'approved') {
        setTimeout(() => {
          const now2 = new Date();
          const at2 = `${String(now2.getHours()).padStart(2,'0')}:${String(now2.getMinutes()).padStart(2,'0')}:${String(now2.getSeconds()).padStart(2,'0')} PT`;
          setAudit(prev => [
            { at: at2, kind: 'qbo.write', actor: 'node-demo', msg: `${a.kind} #${id} · idem=${a.kind}-${id}-20260511 · ok` },
            ...prev,
          ]);
        }, 600);
      }
    }
  }

  function submitDraft(row) {
    setApprovals(prev => [{ ...row }, ...prev]);
    const now = new Date();
    const at = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')} PT`;
    setAudit(prev => [
      { at, kind: row.created_by, actor: row.created_by, msg: `drafted ${row.kind} · ${row.summary.slice(0, 80)} · queued as #${row.id} tier ${row.tier}` },
      ...prev,
    ]);
  }

  if (!launched) {
    return <SXLaunch onLaunch={({ trade: t, withTour }) => { setTrade(t); setLaunched(true); if (withTour) setTourStep(0); }} />;
  }

  const screens = {
    briefing: <SXBriefing data={data0} approvals={approvals} onJump={setView} />,
    queue:    <SXQueue    data={data0} approvals={approvals} onDecide={decide} expanded={expanded} setExpanded={setExpanded} />,
    estimate: <SXEstimate submitDraft={submitDraft} onJump={setView} />,
    co:       <SXChangeOrder data={data0} submitDraft={submitDraft} onJump={setView} />,
    wo:       <SXWorkOrder   data={data0} submitDraft={submitDraft} onJump={setView} />,
    job:      <SXJobDetail data={data0} />,
    field:    <SXField    approvals={approvals} onDecide={decide} />,
    audit:    <SXAudit    audit={audit} />,
  };

  return (
    <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }} data-screen-label={`Sandbox · ${view}`}>
      <SXTopNav view={view} setView={setView}
                openChat={() => setChatOpen(true)}
                restartTour={() => setTourStep(0)} />
      {screens[view]}
      <Tour step={tourStep}
            setView={setView}
            onNext={() => setTourStep(s => (s + 1 < TOUR_STEPS.length ? s + 1 : null))}
            onSkip={() => setTourStep(null)} />
      <SXMacChat
        open={chatOpen} setOpen={setChatOpen} data={data0} trade={trade}
        sessionId={sessionId} setSessionId={setSessionId}
        onQueued={refreshSessionQueue}
      />
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(<SXApp />);
