// DAEV Arbiter — app root
// Wires together the 4 screens with navigation + tweaks panel

const { useState: SA, useEffect: EA, useRef: RA, useMemo: MA, useCallback: CA } = React;

// Analytics hook — replaceable at deploy time. Default is a no-op so the demo
// runs anywhere. Wire to PostHog/Plausible/etc. via window.DAEV_TRACK = (e,p)=>...
if (typeof window !== 'undefined' && typeof window.DAEV_TRACK !== 'function') {
  window.DAEV_TRACK = function (event, props) {
    try { console.debug('[daev:track]', event, props || {}); } catch (_) {}
  };
}
const track = (event, props) => {
  try { window.DAEV_TRACK && window.DAEV_TRACK(event, props || {}); } catch (_) {}
};

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "mode": "light",
  "accent": "teal",
  "nodeStyle": "dots",
  "direction": "LR"
}/*EDITMODE-END*/;

// Slice 6.4.8 Phase 2 (F-213): renders a thin banner at the top of the demo
// when the session is in anon mode (no X-DAEV-API-Key set). Quietly upsells
// the API key for the 10/hour cap. Hidden in embed mode and when the
// browser holds a master key. The auth-mode check is purely client-side
// (window.DAEV_API.getAuthMode) so this never blocks the first paint.
function DemoModeBanner({ t }) {
  const [authMode, setAuthMode] = SA(null);
  EA(() => {
    const tick = () => {
      try {
        if (window.DAEV_API && typeof window.DAEV_API.getAuthMode === 'function') {
          setAuthMode(window.DAEV_API.getAuthMode());
        }
      } catch (_) { /* noop */ }
    };
    tick();
    // Re-check when storage changes (another tab sets the key, or this tab
    // sets it via setApiKey from a tweak panel).
    const onStorage = (e) => {
      if (!e || e.key === 'daev.api_key') tick();
    };
    window.addEventListener('storage', onStorage);
    return () => window.removeEventListener('storage', onStorage);
  }, []);
  if (authMode !== 'anon') return null;
  return (
    <div
      data-testid="demo-mode-banner"
      style={{
        padding: '6px 16px',
        background: 'rgba(99, 102, 241, 0.08)',
        borderBottom: `1px solid ${(t && t.line2) || 'rgba(99, 102, 241, 0.2)'}`,
        color: (t && t.fg2) || '#444',
        fontSize: '0.85em',
        textAlign: 'center',
        lineHeight: 1.4,
      }}>
      🎮 Demo mode · 10 audits/hour · Want unlimited access?{' '}
      <a
        href="https://aelethion.com/contact"
        target="_blank"
        rel="noopener noreferrer"
        style={{
          marginLeft: 6,
          color: (t && t.accent) || '#6366f1',
          textDecoration: 'underline',
        }}>
        Get an API key
      </a>
    </div>
  );
}

function App() {
  // URL params take precedence over localStorage on first paint
  // Supported: ?screen=audit|live|result|dashboard, ?fixture=sqli|cmdi|...,
  //            ?mode=fast|full, ?demo=fresh (clear all persistence),
  //            ?embed=1 (hide top bar for iframe embedding)
  const initialUrl = (() => {
    if (typeof window === 'undefined') return {};
    const p = new URLSearchParams(window.location.search);
    if (p.get('demo') === 'fresh') {
      try {
        localStorage.removeItem('daev.screen');
        localStorage.removeItem('daev.fixture');
        localStorage.removeItem('daev.mode');
        localStorage.removeItem('daev.seen');
        localStorage.removeItem('daev.tour.v2');
        localStorage.removeItem('daev.tour.v3');
        // also drop the (orphaned) v1 result tour key so a fresh share is truly fresh
        localStorage.removeItem('daev.tour.result');
      } catch(_) {}
    }
    return {
      screen:  p.get('screen'),
      fixture: p.get('fixture'),
      mode:    p.get('mode'),
      theme:   p.get('theme'),         // 'light' | 'dark'
      autorun: p.get('autorun') === '1',
      embed:   p.get('embed') === '1',
      tour:    p.get('tour') === '1',  // force tour to run regardless of localStorage
    };
  })();

  // Slice 6.4.5.3 Phase 1 (Item 1): validate localStorage state before trusting it.
  // Prior sessions can leave zombie state (e.g. daev.fixture='_github_live' +
  // daev.screen='result') that crashes ResultDashboard on cold load because
  // _github_live is lazy-created only after a GitHub audit completes.
  const safeBoot = (() => {
    const VALID_SCREENS = ['audit', 'live', 'result', 'dashboard'];
    const fixtures = (typeof window !== 'undefined' && window.DAEV_FIXTURES) || {};
    let desiredFixture = initialUrl.fixture || localStorage.getItem('daev.fixture');
    let desiredScreen  = initialUrl.screen  || localStorage.getItem('daev.screen');

    // 1. Reject fixtures that are not currently loaded (handles _github_live
    //    left from a prior session — it is lazy and only exists post-audit).
    if (desiredFixture && !Object.prototype.hasOwnProperty.call(fixtures, desiredFixture)) {
      console.warn(`[daev-app] fixture '${desiredFixture}' is not in DAEV_FIXTURES; clearing zombie state`);
      try {
        localStorage.removeItem('daev.fixture');
        localStorage.removeItem('daev.screen');
      } catch (_) {}
      desiredFixture = null;
      desiredScreen = null;
    }

    // 2. Reject unknown screens.
    if (desiredScreen && !VALID_SCREENS.includes(desiredScreen)) {
      console.warn(`[daev-app] invalid daev.screen='${desiredScreen}'; clearing`);
      try { localStorage.removeItem('daev.screen'); } catch (_) {}
      desiredScreen = null;
    }

    // 3. result screen requires a finding (or a known fixture that intentionally
    //    has none, like 'safe' or '_user'). Fall back to audit otherwise to
    //    prevent ResultDashboard's `fx.finding` access from throwing.
    if (desiredScreen === 'result' && desiredFixture) {
      const fx = fixtures[desiredFixture];
      const noFindingExpected = desiredFixture === 'safe' || desiredFixture === '_user';
      if (!fx || (fx.finding === undefined && !noFindingExpected)) {
        console.warn(`[daev-app] screen='result' but '${desiredFixture}' has no finding loaded; falling back to audit`);
        desiredScreen = 'audit';
      }
    }

    return { fixture: desiredFixture, screen: desiredScreen };
  })();

  const [screen, setScreen] = SA(() =>
    safeBoot.screen ||
    // First-time visitor with no localStorage and no URL: drop into the wow flow
    (localStorage.getItem('daev.seen') ? 'dashboard' : 'audit')
  );
  const [fixtureId, setFixtureId] = SA(() => {
    if (safeBoot.fixture) return safeBoot.fixture;
    // First-time default: racecond — the causal-only finding SAST can't catch.
    return 'racecond';
  });
  const [mode, setMode] = SA(() => {
    const raw = initialUrl.mode || localStorage.getItem('daev.mode') || 'fast';
    return raw === 'full' ? 'council' : raw;
  });
  const embedMode = initialUrl.embed;

  // Slice 6.4.5 wiring brief — re-render trigger when window.DAEV_FIXTURES is mutated
  // by the hydration layer (window.DAEV_WIRING.hydrateAll).
  const [hydrationTick, setHydrationTick] = SA(0);
  EA(() => {
    const handler = () => {
      setHydrationTick(t => t + 1);
      console.log('[daev-wiring] App re-rendering for hydration');
    };
    window.addEventListener('daev-fixtures-hydrated', handler);
    return () => window.removeEventListener('daev-fixtures-hydrated', handler);
  }, []);

  // Mark as seen on first interaction
  EA(() => { try { localStorage.setItem('daev.seen', '1'); } catch(_) {} }, []);

  // Auto-run live build if requested via ?autorun=1
  EA(() => {
    if (initialUrl.autorun && initialUrl.fixture) {
      const fx = initialUrl.fixture;
      try { localStorage.setItem('daev.fixture', fx); } catch(_) {}
      setFixtureId(fx);
      setScreen('live');
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Tweaks state — `mode` (light/dark) persists per visitor; the rest are defaults.
  const [tweaks, setTweaks] = SA(() => {
    const next = { ...TWEAK_DEFAULTS };
    // URL param wins (?theme=dark | ?theme=light), then saved choice, then default.
    const urlTheme = initialUrl.theme;
    if (urlTheme === 'dark' || urlTheme === 'light') {
      next.mode = urlTheme;
    } else {
      try {
        const stored = localStorage.getItem('daev.theme');
        if (stored === 'dark' || stored === 'light') next.mode = stored;
      } catch (_) {}
    }
    return next;
  });
  const [tweaksOpen, setTweaksOpen] = SA(false);
  const [tweaksAvailable, setTweaksAvailable] = SA(false);

  const t = MA(() => window.DAEV_buildTokens({ mode: tweaks.mode, accentKey: tweaks.accent }), [tweaks.mode, tweaks.accent]);

  // Persist nav state
  EA(() => { localStorage.setItem('daev.screen', screen); }, [screen]);
  EA(() => { localStorage.setItem('daev.fixture', fixtureId); }, [fixtureId]);
  EA(() => { localStorage.setItem('daev.mode', mode); }, [mode]);

  // Tweaks wiring — register listener BEFORE announcing
  EA(() => {
    const onMsg = (e) => {
      if (!e.data || typeof e.data !== 'object') return;
      if (e.data.type === '__activate_edit_mode') setTweaksOpen(true);
      if (e.data.type === '__deactivate_edit_mode') setTweaksOpen(false);
    };
    window.addEventListener('message', onMsg);
    // Now announce
    try { window.parent.postMessage({ type: '__edit_mode_available' }, '*'); } catch(_) {}
    setTweaksAvailable(true);
    return () => window.removeEventListener('message', onMsg);
  }, []);

  const setTweak = (key, value) => {
    setTweaks(prev => {
      const next = { ...prev, [key]: value };
      try { window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [key]: value } }, '*'); } catch(_) {}
      return next;
    });
  };

  // Theme toggle (light <-> dark) + persist to localStorage so the visitor's
  // choice survives reload. Also paint <html> background immediately to avoid
  // a brief flash when the next mount happens.
  const toggleTheme = () => {
    const nextMode = tweaks.mode === 'dark' ? 'light' : 'dark';
    setTweak('mode', nextMode);
    try { localStorage.setItem('daev.theme', nextMode); } catch (_) {}
    try {
      document.documentElement.style.background = nextMode === 'dark' ? '#0d1117' : '#F7F6F2';
      document.body.style.background = nextMode === 'dark' ? '#0d1117' : '#F7F6F2';
    } catch (_) {}
    track('theme_toggled', { to: nextMode });
  };

  // Phase A — Council Mode pinned-replay via debate-stream SSE.
  // Streams a pre-run council audit's persisted debate_rounds instead of
  // firing a fresh (cold-start-prone) live council audit. Backend contract
  // verified Phase 4a (/tmp/debate-stream-contract-2026-05-13.md):
  //   event: round  — one frame per persisted debate_rounds row
  //   event: done   — terminal; closes the stream
  //   event: error  — backend producer failure; closes the stream
  //   data: ping     — 15s keepalive; an untyped frame, so it reaches no
  //                    addEventListener handler below and is silently ignored
  // Auth: ?token=<jwt> query string (EventSource cannot set headers).
  // The prestaged audit's /audits/{id} detail is the authoritative final
  // render state — the SSE stream is the round-by-round drama; on done OR
  // on error we finalize from that detail so the beat always completes
  // (graceful degradation beats a cold-start live council audit on camera).
  // MD-133-PhaseA: the displayed audit_id stays the prestaged one so demo
  // narration ("paste this audit_id") matches what the audience sees.
  const replayCouncilViaDebateStream = async (auditId) => {
    let token;
    try {
      // FLAG 2 ruling: mint the JWT at CLICK time, not page load — the
      // 120 s TTL means a load-time mint would be stale by click.
      token = await window.DAEV_API.mintDemoToken();
    } catch (e) {
      console.warn('[daev-replay] JWT mint failed; council replay aborted:', e.message);
      return;
    }

    // Authoritative final render state — fetched in parallel with the stream.
    const detailPromise = window.DAEV_API.fetchAuditDetail(auditId).catch((e) => {
      console.warn('[daev-replay] prestaged detail fetch failed:', e.message);
      return null;
    });

    const url = `${window.DAEV_API.API_BASE}/council/debate-stream/${auditId}` +
      `?token=${encodeURIComponent(token)}`;
    const es = new EventSource(url);
    const rounds = [];
    let settled = false;

    // Render the final council state via the SAME hydration path a live
    // council run uses (window.DAEV_FIXTURES + daev-fixtures-hydrated), so
    // no council-panel changes are needed. Idempotent via the settled guard
    // (done is normally followed by a transport-close 'error' event).
    const finalize = async (reason) => {
      if (settled) return;
      settled = true;
      try { es.close(); } catch (_) {}
      const detail = await detailPromise;
      if (detail && window.DAEV_TRANSFORM) {
        const fx = window.DAEV_FIXTURES[fixtureId] || {};
        const wired = window.DAEV_TRANSFORM.apiAuditToFixtureShape(detail, fx.lines, fixtureId);
        if (wired) {
          window.DAEV_FIXTURES[fixtureId] = wired;
          window.dispatchEvent(new CustomEvent('daev-fixtures-hydrated', {
            detail: { source: 'council-replay', fixture: fixtureId, audit_id: auditId },
          }));
        }
      }
      console.log(`[daev-replay] council replay finalized (${reason}); ` +
        `${rounds.length} rounds streamed for ${auditId}`);
    };

    es.addEventListener('round', (ev) => {
      try {
        const data = JSON.parse(ev.data);
        rounds.push(data);
        window.dispatchEvent(new CustomEvent('daev-council-replay-round', {
          detail: { audit_id: auditId, round: data, rounds_so_far: rounds.length },
        }));
      } catch (err) {
        console.warn('[daev-replay] unparseable round frame:', err.message);
      }
    });

    es.addEventListener('done', (ev) => {
      let total = rounds.length;
      try { total = JSON.parse(ev.data || '{}').total_rounds || rounds.length; } catch (_) {}
      console.log(`[daev-replay] debate-stream done: ${total} rounds`);
      finalize('done');
    });

    // EventSource routes BOTH the backend's named `event: error` frame and
    // transport-level failures through the 'error' listener. Distinguish by
    // ev.data (present → backend producer error; absent → transport error).
    // Either way finalize from the prestaged detail so the beat completes.
    es.addEventListener('error', (ev) => {
      if (ev && ev.data) {
        let errClass = 'unknown';
        try { errClass = JSON.parse(ev.data).error || 'unknown'; } catch (_) {}
        console.warn(`[daev-replay] debate-stream producer error: ${errClass}`);
      } else {
        console.warn('[daev-replay] debate-stream transport error');
      }
      finalize('error');
    });
  };

  // Slice 6.4.5 wiring brief — handleRunAudit upgraded to fire live /switzerland/gate
  const handleRunAudit = async () => {
    track('audit_started', { fixture: fixtureId, mode });
    setScreen('live');

    // Phase A — Council Mode pinned-replay (recon §11.G option 2). When the
    // operator has pre-run a council audit and set PRESTAGED_COUNCIL_AUDIT_ID,
    // the sqli council beat replays that audit's persisted debate rounds via
    // debate-stream SSE instead of a fresh /switzerland/gate?mode=council POST
    // — eliminating council cold-start latency on camera. Flag defaults to
    // null (path inert); operator sets it pre-film, reverts post-film.
    if (
      mode === 'council' &&
      window.PRESTAGED_COUNCIL_AUDIT_ID &&
      fixtureId === 'sqli'
    ) {
      console.log('[daev-replay] Council Mode pinned-replay active for sqli');
      await replayCouncilViaDebateStream(window.PRESTAGED_COUNCIL_AUDIT_ID);
      return;
    }

    // Only fire live audit if:
    // 1. API client is loaded
    // 2. Fixture is one we want to wire (not the static-only ones like racecond)
    // 3. User has an API key set
    const STATIC_ONLY = ['safe', 'xss-js', 'cmdi-js', 'racecond'];
    if (
      !window.DAEV_API ||
      !window.DAEV_TRANSFORM ||
      STATIC_ONLY.includes(fixtureId) ||
      !window.DAEV_API.getApiKey()
    ) {
      // Static animation continues with whatever fixture data is currently loaded
      return;
    }

    try {
      const fx = window.DAEV_FIXTURES[fixtureId];
      const sourceCode = (fx && fx.lines) ? fx.lines.join('\n') : '';
      if (!sourceCode || sourceCode.length < 10) return;

      console.log(`[daev-wiring] firing live /switzerland/gate for ${fixtureId} (mode=${mode})`);
      const gateResult = await window.DAEV_API.submitInlineAudit(sourceCode, fx.lang || 'python', mode);
      console.log(`[daev-wiring] /gate returned audit_id=${gateResult.audit_id} verdict=${gateResult.verdict}`);

      // Fetch full detail (gate response is summary; detail has full graph)
      const detail = await window.DAEV_API.fetchAuditDetail(gateResult.audit_id);
      const wired = window.DAEV_TRANSFORM.apiAuditToFixtureShape(detail, fx.lines, fixtureId);
      if (wired) {
        // Slice 6.4.5.2 Phase 13 (BUG-85): if the live re-audit produced an empty
        // graph (F-187 floor: minimal snippets / module-level constants don't
        // emit nodes), preserve the prestaged fixture's graph + finding for
        // visual continuity. Update only audit_id + live_audit_metadata so the
        // demo beat completes (audience sees a graph + finding) and DevTools
        // still shows the live call fired.
        const liveNodes = wired.graph && wired.graph.nodes ? wired.graph.nodes.length : 0;
        const liveHasFinding = !!wired.finding;
        if (liveNodes < 2 || !liveHasFinding) {
          console.warn(
            `[daev-wiring] live audit returned ${liveNodes}-node graph for ${fixtureId} ` +
            `(F-187 floor); preserving prestaged fixture for visual continuity`
          );
          window.DAEV_FIXTURES[fixtureId] = {
            ...fx,
            audit_id: gateResult.audit_id,
            live_audit_metadata: {
              live_audit_id: gateResult.audit_id,
              live_verdict: gateResult.verdict,
              live_nodes: liveNodes,
              degraded: true,
            },
          };
        } else {
          window.DAEV_FIXTURES[fixtureId] = wired;
        }
        window.dispatchEvent(new CustomEvent('daev-fixtures-hydrated', {
          detail: { source: 'live-audit', fixture: fixtureId, audit_id: gateResult.audit_id }
        }));
        console.log(`[daev-wiring] live audit complete; fixture ${fixtureId} replaced`);
      }
    } catch (e) {
      console.warn('[daev-wiring] live audit failed; static fixture remains:', e.message);
      // No regression — static animation continues; user sees the demo flow either way
    }
  };

  // Slice 6.4.5 wiring brief — handleRunGithubAudit submits GitHub URL → polls → renders result
  // Slice 6.4.5.3 Phase 6 (Item 8) — hardened polling per Q1 amendment:
  //   * 5-min timeout (was 3 min) for bandit-sized repos
  //   * Live progress feed via window.DAEV_GITHUB_PROGRESS + event;
  //     LiveBuild renders an overlay reading from this.
  //   * On timeout, surface "still processing" placeholder fixture instead
  //     of throwing into a silent UI freeze.
  //   * 401 mid-poll handled transparently by authedFetchJWT (re-mints).
  const setGithubProgress = (state) => {
    window.DAEV_GITHUB_PROGRESS = state;
    try {
      window.dispatchEvent(new CustomEvent('daev-github-progress', { detail: state }));
    } catch (_) {}
  };
  const handleRunGithubAudit = async (url) => {
    if (!url || !url.trim()) return;
    track('github_audit_started', { url: url.slice(0, 60) });
    if (!window.DAEV_API || !window.DAEV_API.getApiKey()) {
      console.warn('[daev-wiring] no API key; cannot submit GitHub audit');
      return;
    }
    setScreen('live');

    // Slice 6.4.7 Phase 5 (HC-53): pre-staged github lookup. If the
    // repo URL matches a known prestaged audit_id, short-circuit the
    // 30-90 s submit + poll cycle by fetching the persisted record
    // directly. Real backend data — no fabricated finding (HC-53).
    if (window.DAEV_WIRING && window.DAEV_WIRING.getPrestagedGithubAudit) {
      try {
        const prestaged = await window.DAEV_WIRING.getPrestagedGithubAudit(url.trim());
        if (prestaged) {
          console.log(`[daev-wiring] using prestaged audit for ${url.trim()}`);
          window.DAEV_FIXTURES['_github_live'] = prestaged;
          setFixtureId('_github_live');
          setGithubProgress({ active: false, phase: 'complete',
            audit_id: prestaged.audit_id, repo_url: url.trim(), message: '' });
          window.dispatchEvent(new CustomEvent('daev-fixtures-hydrated', {
            detail: {
              source: 'github-prestaged',
              fixture: '_github_live',
              audit_id: prestaged.audit_id,
            }
          }));
          return;
        }
      } catch (e) {
        console.warn('[daev-wiring] prestaged github lookup error:', e.message);
        // Fall through to live audit submission.
      }
    }

    setGithubProgress({
      active: true, phase: 'queueing', repo_url: url.trim(),
      message: `Queueing audit for ${url.trim()}...`,
    });
    try {
      console.log(`[daev-wiring] submitting GitHub audit: ${url}`);
      const result = await window.DAEV_API.submitGithubAudit(url.trim());
      console.log(`[daev-wiring] github submit → audit_id=${result.audit_id} status=${result.status} files=${result.file_count}`);
      setGithubProgress({
        active: true, phase: 'polling',
        audit_id: result.audit_id, repo_url: url.trim(),
        file_count: result.file_count, estimated_tokens: result.estimated_tokens,
        elapsed_s: 0, total_nodes: 0,
        message: `Queued: ${result.file_count} files (~${result.estimated_tokens} tokens). Analyzing…`,
      });

      const detail = await window.DAEV_API.pollAuditUntilReady(result.audit_id, {
        intervalMs: 5000,
        timeoutMs: 300000,
        onProgress: ({ ready, elapsed, total_nodes, finding_count }) => {
          const elapsed_s = Math.round((elapsed || 0) / 1000);
          console.log(`[daev-wiring] poll ${result.audit_id}: ready=${ready} elapsed=${elapsed_s}s nodes=${total_nodes || 0}`);
          setGithubProgress({
            active: true, phase: 'polling',
            audit_id: result.audit_id, repo_url: url.trim(),
            file_count: result.file_count, estimated_tokens: result.estimated_tokens,
            elapsed_s, total_nodes: total_nodes || 0, finding_count: finding_count || 0,
            message: `Analyzing… ${total_nodes || 0} nodes processed (${elapsed_s}s)`,
          });
        },
      });

      // Slice 6.4.5.3 Q1: pollAuditUntilReady now returns {timedOut: true} on
      // timeout instead of throwing. Build a queue-receipt placeholder fixture
      // so the user gets a meaningful fallback instead of a stuck LiveBuild.
      if (detail && detail.timedOut) {
        console.warn(`[daev-wiring] GitHub audit ${result.audit_id} did not complete within 5 min; rendering placeholder`);
        window.DAEV_FIXTURES['_github_live'] = {
          id: '_github_live',
          lang: 'python',
          label: `GitHub: ${url.trim()}`,
          audit_id: result.audit_id,
          audit_metadata: { repo_url: url.trim(), files_audited: result.file_count, source: 'github' },
          lines: [
            `# GitHub audit queued — still processing`,
            `# audit_id: ${result.audit_id}`,
            `# files queued: ${result.file_count}, estimated tokens: ${result.estimated_tokens}`,
            `# This audit takes longer than 5 minutes; check back in 1-2 minutes via the audits list.`,
          ],
          finding: undefined,
          graph: { nodes: [], edges: [], positions: {} },
        };
        setFixtureId('_github_live');
        setGithubProgress({
          active: true, phase: 'still_processing',
          audit_id: result.audit_id, repo_url: url.trim(),
          file_count: result.file_count, estimated_tokens: result.estimated_tokens,
          message: `Audit still processing — check back in 1-2 minutes.`,
        });
        window.dispatchEvent(new CustomEvent('daev-fixtures-hydrated', {
          detail: { source: 'github-audit-timeout', audit_id: result.audit_id }
        }));
        return;
      }

      const sourceLines = ['# (GitHub repo: ' + url + ')', '# Source not retained for from-github audits'];
      const wired = window.DAEV_TRANSFORM.apiAuditToFixtureShape(detail, sourceLines, '_github_live');
      if (wired) {
        // Make sure repo_url is preserved on the wired fixture even if the
        // backend metadata.repo_url ever falls back to null.
        wired.audit_metadata = wired.audit_metadata || {};
        if (!wired.audit_metadata.repo_url) wired.audit_metadata.repo_url = url.trim();
        window.DAEV_FIXTURES['_github_live'] = wired;
        setFixtureId('_github_live');
        setGithubProgress({
          active: false, phase: 'complete',
          audit_id: result.audit_id, repo_url: url.trim(),
          message: '',
        });
        window.dispatchEvent(new CustomEvent('daev-fixtures-hydrated', {
          detail: { source: 'github-audit', audit_id: result.audit_id }
        }));
        console.log(`[daev-wiring] GitHub audit complete; switched to _github_live fixture`);
      }
    } catch (e) {
      console.warn('[daev-wiring] GitHub audit failed:', e.message);
      setGithubProgress({
        active: true, phase: 'error', repo_url: url.trim(),
        message: `Audit failed: ${e.message || 'unknown error'}. Try again or use a smaller repo.`,
      });
    }
  };
  const handleOpenAudit = (a) => {
    track('audit_opened', { fixture: a.fix, mode: a.mode, audit_id: a.id });
    setFixtureId(a.fix);
    setMode(a.mode);
    setScreen('result');
  };
  const handleNewAudit = () => {
    track('new_audit_clicked');
    setScreen('audit');
  };

  // ⌘K / Ctrl-K — fixture quick-switch palette
  const [paletteOpen, setPaletteOpen] = SA(false);
  // ?  — keyboard-shortcut legend
  const [shortcutsOpen, setShortcutsOpen] = SA(false);

  // Guided walkthrough — 17-step tour (see tour.jsx)
  // Auto-fires for first-time visitors. Forced on with ?tour=1.
  const [tour, setTour] = SA(() => {
    if (initialUrl.tour) return { active: true, step: 0, completed: false, skipped: false };
    let seen = false;
    try { seen = !!localStorage.getItem('daev.tour.v3'); } catch (_) {}
    return { active: !seen && !embedMode, step: 0, completed: false, skipped: false };
  });

  // Fire tour_started for the auto-launch / URL-forced path. window.DAEV_TRACK
  // is patched at module load by the analytics block in app.jsx, but we wrap
  // in try/catch in case the host page hasn't injected its own override yet.
  EA(() => {
    if (!tour.active || tour.step !== 0) return;
    try {
      const trigger = initialUrl.tour ? 'url_param' : 'auto_first_visit';
      window.DAEV_TRACK && window.DAEV_TRACK('tour_started', { trigger });
    } catch (_) {}
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Forced tab override for tour steps 10-14 (Causal/Findings/Council/SAST/Export).
  // ResultDashboard reads this and uses it as a primary; null means user-controlled.
  const [forcedTab, setForcedTab] = SA(null);

  const launchTour = (trigger) => {
    setTour({ active: true, step: 0, completed: false, skipped: false });
    track('tour_started', { trigger: trigger || 'manual' });
  };
  EA(() => {
    const onKey = (e) => {
      // Skip when user is typing in an input or contentEditable
      const tag = (e.target && e.target.tagName) || '';
      const editable = tag === 'INPUT' || tag === 'TEXTAREA' || (e.target && e.target.isContentEditable);
      const isMeta = e.metaKey || e.ctrlKey;

      if (isMeta && (e.key === 'k' || e.key === 'K')) {
        e.preventDefault();
        setPaletteOpen(o => !o);
        track('palette_toggled');
      } else if (e.key === 'Escape') {
        setPaletteOpen(false);
        setShortcutsOpen(false);
      } else if (!isMeta && !editable && (e.key === '?' || (e.shiftKey && e.key === '/'))) {
        e.preventDefault();
        setShortcutsOpen(o => !o);
        track('shortcuts_toggled');
      } else if (!isMeta && !editable && (e.key === 't' || e.key === 'T')) {
        // T toggles theme
        toggleTheme();
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [tweaks.mode]);

  const switchFixtureFromPalette = (id) => {
    setFixtureId(id);
    try { localStorage.setItem('daev.fixture', id); } catch (_) {}
    setPaletteOpen(false);
    track('fixture_switched_via_palette', { fixture: id });
    // If the visitor is on the dashboard, jump them to the result for the new fixture
    if (screen !== 'audit' && screen !== 'live') setScreen('result');
  };

  // Render the active screen
  let body;
  if (screen === 'dashboard') {
    body = <DashboardHome t={t} onOpenAudit={handleOpenAudit} onNewAudit={handleNewAudit}
      onLaunchTour={() => launchTour('dashboard_strip')}/>;
  } else if (screen === 'audit') {
    body = <AuditInput t={t}
      fixtureId={fixtureId === 'user' ? 'sqli' : fixtureId}
      setFixtureId={setFixtureId}
      mode={mode} setMode={setMode}
      onRunAudit={handleRunAudit}
      onRunGithubAudit={handleRunGithubAudit}/>;
  } else if (screen === 'live') {
    body = <LiveBuild t={t} key={fixtureId+mode}
      fixtureId={fixtureId} mode={mode}
      direction={tweaks.direction}
      nodeStyle={tweaks.nodeStyle}
      onDone={(advance) => {
        track('live_build_completed', { fixture: fixtureId, mode });
        if (advance === true) setScreen('result');
      }}/>;
  } else {
    body = <ResultDashboard t={t}
      fixtureId={fixtureId} mode={mode}
      direction={tweaks.direction}
      nodeStyle={tweaks.nodeStyle}
      forcedTab={forcedTab}
      clearForcedTab={() => setForcedTab(null)}
      onNewAudit={() => setScreen('audit')}
      onSwitchToFull={() => {
        setMode('council');
        try { localStorage.setItem('daev.mode', 'council'); } catch (_) {}
        track('council_switch_to_council', { fixture: fixtureId });
        setScreen('live');
      }}/>;
  }

  // Tour actions — single object so the tour stays decoupled from internals
  const tourActions = MA(() => ({
    currentScreen: screen,
    setScreen,
    setFixtureId,
    setMode,
    setForcedTab,
    runAudit:           () => { handleRunAudit(); },
    skipToResult:       () => { setScreen('result'); },
    navigateToDashboard:() => { setScreen('dashboard'); },
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }), [screen]);

  return (
    <div style={{
      position:'fixed', inset: 0,
      display:'flex', flexDirection:'column',
      background: t.bg0, color: t.fg0,
      fontFamily: t.font.ui,
    }}
      data-screen-label={screenLabel(screen)}>
      {!embedMode && <TopBar t={t} screen={screen}
        themeMode={tweaks.mode}
        onToggleTheme={toggleTheme}
        onOpenShortcuts={() => setShortcutsOpen(true)}
        onNavigate={(s) => setScreen(s === 'audit' ? 'audit' : 'dashboard')}/>}
      {!embedMode && <DemoModeBanner t={t}/>}
      {/* Sub-step indicator inside audit flow */}
      {(screen === 'audit' || screen === 'live' || screen === 'result') && (
        <AuditFlowSteps t={t} screen={screen}/>
      )}
      {body}
      {tweaksOpen && <TweaksPanel t={t} tweaks={tweaks} setTweak={setTweak} onClose={()=>setTweaksOpen(false)}/>}
      {paletteOpen && <FixturePalette t={t} current={fixtureId}
        onPick={switchFixtureFromPalette}
        onClose={() => setPaletteOpen(false)}/>}
      {shortcutsOpen && <ShortcutsModal t={t}
        onClose={() => setShortcutsOpen(false)}
        onTakeTour={() => { setShortcutsOpen(false); launchTour('shortcuts_modal'); }}/>}
      <TourEngine t={t} tour={tour} setTour={setTour} tourActions={tourActions}/>
      <ToastHost t={t}/>
    </div>
  );
}

// Keyboard shortcut legend — opens with `?` or via the TopBar `?` button.
function ShortcutsModal({ t, onClose, onTakeTour }) {
  const groups = [
    { title: 'Navigation', items: [
      { keys: ['⌘K'],    desc: 'Quick fixture switcher' },
      { keys: ['T'],     desc: 'Toggle light / dark theme' },
      { keys: ['?'],     desc: 'Open this shortcuts panel' },
      { keys: ['esc'],   desc: 'Close any open panel' },
    ]},
    { title: 'Result Dashboard', items: [
      { keys: ['↑','↓'], desc: 'Navigate fixture list (in palette)' },
      { keys: ['↵'],     desc: 'Select highlighted item' },
    ]},
    { title: 'URL routing', items: [
      { keys: ['?fixture=racecond'],         desc: 'Deep-link to a fixture' },
      { keys: ['?screen=result'],            desc: 'Jump to a specific screen' },
      { keys: ['?theme=dark'],               desc: 'Force dark mode on load' },
      { keys: ['?demo=fresh'],               desc: 'Clear all saved state' },
      { keys: ['?embed=1'],                  desc: 'Hide top bar (iframe)' },
      { keys: ['?autorun=1'],                desc: 'Auto-run the live build' },
    ]},
  ];
  return (
    <div onClick={onClose} style={{
      position:'fixed', inset:0, zIndex: 2100,
      background: t.mode === 'dark' ? 'rgba(0,0,0,0.55)' : 'rgba(15,14,12,0.32)',
      backdropFilter: 'blur(2px)',
      display:'flex', alignItems:'flex-start', justifyContent:'center',
      paddingTop: '12vh',
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        width: 540, maxWidth: '92vw',
        background: t.bg1,
        border: `1px solid ${t.line2}`,
        borderRadius: t.radius.lg,
        boxShadow: `0 24px 60px rgba(0,0,0,0.32)`,
        overflow: 'hidden',
        fontFamily: t.font.ui,
      }}>
        <div style={{
          padding:'14px 18px',
          borderBottom: `1px solid ${t.line}`,
          display:'flex', alignItems:'center', justifyContent:'space-between',
        }}>
          <span style={{
            fontFamily: t.font.mono, fontSize: 11, fontWeight: 700,
            color: t.accent.base, letterSpacing: 1.4, textTransform:'uppercase',
          }}>// keyboard shortcuts</span>
          <button onClick={onClose} aria-label="Close" title="Close (esc)"
            style={{
              width: 26, height: 26, padding: 0,
              background: 'transparent', border: `1px solid ${t.line}`,
              borderRadius: t.radius.sm,
              color: t.fg2, cursor:'pointer',
              display:'inline-flex', alignItems:'center', justifyContent:'center',
            }}>
            <Icon name="x" size={14}/>
          </button>
        </div>
        <div style={{ maxHeight: '64vh', overflow:'auto', padding: '12px 18px 16px' }}>
          {/* Take the tour — re-launch entry */}
          {onTakeTour && (
            <button onClick={onTakeTour} style={{
              width: '100%', textAlign: 'left',
              display: 'flex', alignItems: 'center', gap: 10,
              padding: '12px 14px', marginBottom: 16,
              background: `color-mix(in oklch, ${t.accent.base} 10%, ${t.bg1})`,
              border: `1px solid ${t.accent.base}`,
              borderRadius: t.radius.md,
              color: t.fg0, cursor: 'pointer',
              fontFamily: t.font.ui,
            }}>
              <Icon name="play" size={14} color={t.accent.base}/>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 13, fontWeight: 700 }}>Take the tour</div>
                <div style={{ fontFamily: t.font.mono, fontSize: 10.5, color: t.fg2, marginTop: 2 }}>
                  17-step guided walkthrough · 2 min
                </div>
              </div>
              <Icon name="arrow_r" size={14} color={t.accent.base}/>
            </button>
          )}
          {groups.map((g, gi) => (
            <div key={gi} style={{ marginBottom: 14 }}>
              <div style={{
                fontFamily: t.font.mono, fontSize: 9.5, fontWeight: 700,
                color: t.fg2, letterSpacing: 1.6, textTransform: 'uppercase',
                marginBottom: 8,
              }}>{g.title}</div>
              <div style={{ display:'flex', flexDirection:'column', gap: 6 }}>
                {g.items.map((it, ii) => (
                  <div key={ii} style={{
                    display:'flex', alignItems:'center', gap: 12,
                    padding: '6px 0',
                  }}>
                    <div style={{ display:'inline-flex', gap: 4, flexShrink: 0 }}>
                      {it.keys.map((k, ki) => (
                        <kbd key={ki} style={{
                          fontFamily: t.font.mono, fontSize: 10.5, fontWeight: 600,
                          padding: '2px 7px',
                          background: t.bg2,
                          border: `1px solid ${t.line2}`,
                          borderRadius: t.radius.sm,
                          color: t.fg0,
                          minWidth: 22,
                          textAlign:'center',
                        }}>{k}</kbd>
                      ))}
                    </div>
                    <span style={{ fontFamily: t.font.ui, fontSize: 12.5, color: t.fg1, lineHeight: 1.4 }}>
                      {it.desc}
                    </span>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </div>
        <div style={{
          padding: '10px 18px',
          borderTop: `1px solid ${t.line}`,
          fontFamily: t.font.mono, fontSize: 10.5, color: t.fg3,
          letterSpacing: 0.4,
        }}>
          Press <kbd style={{
            fontFamily: t.font.mono, fontSize: 10, padding: '1px 5px',
            border: `1px solid ${t.line2}`, borderRadius: 3,
            color: t.fg1,
          }}>esc</kbd> to close.
        </div>
      </div>
    </div>
  );
}

// ⌘K command palette — fast fixture switcher with arrow-key navigation.
function FixturePalette({ t, current, onPick, onClose }) {
  const all = [
    { id: 'racecond',  label: 'TOCTOU race',          file: 'billing/redeem.py',         tag: 'causal-only · differentiator' },
    { id: 'sqli',      label: 'SQL injection',        file: 'payments/checkout.py',      tag: 'CWE-89 · critical' },
    { id: 'cmdi',      label: 'Command injection',    file: 'infra/ssh_executor.py',     tag: 'CWE-78 · critical' },
    { id: 'pathtrav',  label: 'Path traversal',       file: 'storage/s3_proxy.py',       tag: 'CWE-22 · high' },
    { id: 'xss-js',    label: 'React XSS',            file: 'frontend/Profile.tsx',      tag: 'CWE-79 · high' },
    { id: 'cmdi-js',   label: 'Node child_process',   file: 'ops/diag.js',               tag: 'CWE-78 · critical' },
    { id: 'hardcoded', label: 'Hardcoded credentials',file: 'config/secrets.py',         tag: 'CWE-798 · medium' },
    { id: 'safe',      label: 'Clean (parameterized)',file: 'payments/checkout.py (v2)', tag: 'baseline · no finding' },
  ];
  const [q, setQ] = SA('');
  const [idx, setIdx] = SA(0);
  const filtered = MA(() => {
    const term = q.trim().toLowerCase();
    if (!term) return all;
    return all.filter(o =>
      o.label.toLowerCase().includes(term) ||
      o.file.toLowerCase().includes(term) ||
      o.tag.toLowerCase().includes(term) ||
      o.id.toLowerCase().includes(term)
    );
  }, [q]);
  EA(() => { setIdx(0); }, [q]);
  EA(() => {
    const onKey = (e) => {
      if (e.key === 'ArrowDown') { e.preventDefault(); setIdx(i => Math.min(filtered.length - 1, i + 1)); }
      else if (e.key === 'ArrowUp') { e.preventDefault(); setIdx(i => Math.max(0, i - 1)); }
      else if (e.key === 'Enter') {
        e.preventDefault();
        if (filtered[idx]) onPick(filtered[idx].id);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [filtered, idx, onPick]);

  return (
    <div onClick={onClose} style={{
      position:'fixed', inset: 0, zIndex: 2000,
      background: t.mode === 'dark' ? 'rgba(0,0,0,0.55)' : 'rgba(15,14,12,0.32)',
      backdropFilter: 'blur(2px)',
      display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
      paddingTop: '14vh',
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        width: 540, maxWidth: '92vw',
        background: t.bg1,
        border: `1px solid ${t.line2}`,
        borderRadius: t.radius.lg,
        boxShadow: `0 24px 60px rgba(0,0,0,0.32)`,
        overflow: 'hidden',
        fontFamily: t.font.ui,
      }}>
        <div style={{
          padding: '14px 18px',
          borderBottom: `1px solid ${t.line}`,
          display: 'flex', alignItems: 'center', gap: 10,
        }}>
          <Icon name="search" size={14} color={t.fg2}/>
          <input autoFocus value={q} onChange={e => setQ(e.target.value)}
            placeholder="Search fixtures · file paths · CWEs"
            style={{
              flex: 1, background: 'transparent', border: 'none', outline: 'none',
              fontFamily: t.font.mono, fontSize: 13, color: t.fg0,
            }}/>
          <span style={{
            fontFamily: t.font.mono, fontSize: 10, fontWeight: 600,
            color: t.fg2, letterSpacing: 0.6,
            padding: '3px 7px', border: `1px solid ${t.line2}`,
            borderRadius: t.radius.sm,
          }}>esc</span>
        </div>
        <div style={{ maxHeight: '52vh', overflow: 'auto', padding: '6px 0' }}>
          {filtered.length === 0 && (
            <div style={{
              padding: '20px 18px',
              fontFamily: t.font.mono, fontSize: 12, color: t.fg2,
            }}>No fixtures match "{q}"</div>
          )}
          {filtered.map((o, i) => {
            const active = i === idx;
            const isCurrent = o.id === current;
            const isCausal = o.id === 'racecond';
            return (
              <button key={o.id}
                onMouseEnter={() => setIdx(i)}
                onClick={() => onPick(o.id)}
                style={{
                  display: 'grid',
                  gridTemplateColumns: '1fr auto',
                  alignItems: 'center',
                  gap: 12,
                  width: '100%', textAlign: 'left',
                  padding: '10px 18px',
                  background: active ? t.bg2 : 'transparent',
                  border: 'none', borderLeft: `3px solid ${active ? t.accent.base : 'transparent'}`,
                  cursor: 'pointer',
                  color: t.fg0,
                }}>
                <div>
                  <div style={{
                    fontFamily: t.font.ui, fontSize: 13, fontWeight: 600,
                    color: t.fg0, letterSpacing: -0.01,
                    display: 'inline-flex', alignItems: 'center', gap: 8,
                  }}>
                    {isCausal && (
                      <span style={{
                        width: 6, height: 6, borderRadius:'50%',
                        background: t.accent.base,
                        boxShadow: `0 0 6px ${t.accent.base}`,
                      }}/>
                    )}
                    {o.label}
                    {isCurrent && (
                      <span style={{
                        fontFamily: t.font.mono, fontSize: 9, fontWeight: 700,
                        color: t.accent.base, letterSpacing: 1, textTransform: 'uppercase',
                        padding: '1px 6px',
                        border: `1px solid ${t.accent.base}`,
                        borderRadius: t.radius.sm,
                      }}>current</span>
                    )}
                  </div>
                  <div style={{
                    fontFamily: t.font.mono, fontSize: 11, color: t.fg2,
                    marginTop: 2, letterSpacing: 0.2,
                  }}>{o.file} · <span style={{ color: t.fg3 }}>{o.tag}</span></div>
                </div>
                {active && <Icon name="arrow_r" size={14} color={t.accent.base}/>}
              </button>
            );
          })}
        </div>
        <div style={{
          padding: '8px 18px',
          borderTop: `1px solid ${t.line}`,
          display: 'flex', alignItems: 'center', gap: 14,
          fontFamily: t.font.mono, fontSize: 10, color: t.fg2,
          letterSpacing: 0.4,
        }}>
          <span><b style={{ color: t.fg1 }}>↑↓</b> navigate</span>
          <span><b style={{ color: t.fg1 }}>↵</b> select</span>
          <span><b style={{ color: t.fg1 }}>esc</b> close</span>
          <span style={{ flex: 1 }}/>
          <span style={{ color: t.fg3 }}>{filtered.length} of {all.length}</span>
        </div>
      </div>
    </div>
  );
}

function screenLabel(s) {
  switch(s) {
    case 'dashboard': return '01 Dashboard · History';
    case 'audit':     return '02 Audit Input · Home';
    case 'live':      return '03 Live Build · Causal graph construction';
    case 'result':    return '04 Result Dashboard · Finding review';
  }
}

function AuditFlowSteps({ t, screen }) {
  // Labels shift to active-tense on the live screen so visitors know the
  // engine is working ("Building causal model") rather than passively staged.
  const steps = [
    { id:'audit',  label:'Input',  activeLabel:'Input' },
    { id:'live',   label:'Build',  activeLabel:'Building causal model' },
    { id:'result', label:'Result', activeLabel:'Result' },
  ];
  const activeIdx = steps.findIndex(s => s.id === screen);
  return (
    <div style={{
      padding: '0 20px',
      borderBottom: `1px solid ${t.line}`,
      display:'flex', alignItems:'center', gap: 10,
      background: t.bg0,
      height: 34,
      flexShrink: 0,
    }}>
      <div style={{
        fontFamily: t.font.mono, fontSize: 10,
        color: t.fg3, letterSpacing: 1, textTransform:'uppercase',
      }}>flow</div>
      {steps.map((s, i) => {
        const done = i < activeIdx;
        const active = i === activeIdx;
        return (
          <div key={s.id} style={{ display:'flex', alignItems:'center', gap: 8 }}>
            <div style={{
              width: 14, height: 14, borderRadius:'50%',
              border: `1px solid ${active ? t.accent.base : done ? t.accent.base : t.line2}`,
              background: done ? t.accent.base : 'transparent',
              display:'flex', alignItems:'center', justifyContent:'center',
              flexShrink: 0,
            }}>
              {done && <Icon name="check" size={9} color={t.mode==='dark'?'oklch(0.14 0.01 240)':'white'}/>}
              {active && <div style={{
                width: 5, height: 5, borderRadius:'50%',
                background: t.accent.base,
                boxShadow: `0 0 8px ${t.accent.base}`,
              }}/>}
            </div>
            <span style={{
              fontFamily: t.font.mono, fontSize: 11,
              color: active ? t.fg0 : done ? t.fg1 : t.fg3,
              fontWeight: active ? 700 : 500,
              letterSpacing: 0.3,
              textTransform:'uppercase',
            }}>{active ? s.activeLabel : s.label}</span>
            {i < steps.length - 1 && (
              <div style={{
                width: 28, height: 1,
                background: i < activeIdx ? t.accent.base : t.line2,
              }}/>
            )}
          </div>
        );
      })}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Tweaks Panel
// ─────────────────────────────────────────────────────────────
function TweaksPanel({ t, tweaks, setTweak, onClose }) {
  return (
    <div style={{
      position:'fixed', top: 70, right: 20,
      width: 280,
      background: t.bg1, border:`1px solid ${t.line2}`,
      borderRadius: t.radius.md,
      boxShadow:`0 12px 32px rgba(0,0,0,0.3)`,
      zIndex: 1000,
      fontFamily: t.font.ui,
    }}>
      <div style={{
        padding:'10px 14px',
        borderBottom:`1px solid ${t.line}`,
        display:'flex', alignItems:'center', justifyContent:'space-between',
      }}>
        <div style={{
          fontFamily: t.font.mono, fontSize: 10, color: t.fg2,
          letterSpacing: 1, textTransform:'uppercase', fontWeight:600,
        }}>Tweaks</div>
        <button onClick={onClose} style={{
          background:'transparent', border:'none', color: t.fg2, cursor:'pointer',
          padding: 2, display:'flex',
        }}>
          <Icon name="x" size={14}/>
        </button>
      </div>
      <div style={{ padding: 14, display:'flex', flexDirection:'column', gap: 14 }}>

        {/* theme */}
        <TweakRow label="Theme">
          <Segmented t={t} value={tweaks.mode} onChange={v=>setTweak('mode', v)} options={[
            { value:'dark', label:'Dark' },
            { value:'light', label:'Light' },
          ]}/>
        </TweakRow>

        {/* accent */}
        <TweakRow label="Accent">
          <div style={{ display:'flex', gap: 6 }}>
            {Object.entries(window.DAEV_ACCENTS).map(([k, v]) => {
              const c = `oklch(0.70 ${v.c} ${v.h})`;
              const active = tweaks.accent === k;
              return (
                <button key={k} onClick={()=>setTweak('accent', k)} style={{
                  flex:1,
                  height: 28,
                  background: c,
                  border: `1px solid ${active ? t.fg0 : t.line}`,
                  borderRadius: t.radius.sm,
                  cursor:'pointer',
                  padding: 0,
                  boxShadow: active ? `0 0 0 2px ${t.bg1}, 0 0 0 3px ${c}` : 'none',
                }} title={v.name}/>
              );
            })}
          </div>
        </TweakRow>

        {/* node style */}
        <TweakRow label="Node style">
          <Segmented t={t} value={tweaks.nodeStyle} onChange={v=>setTweak('nodeStyle', v)} options={[
            { value:'dots', label:'Dots' },
            { value:'shapes', label:'Shapes' },
            { value:'cards', label:'Cards' },
          ]}/>
        </TweakRow>

        {/* direction */}
        <TweakRow label="Layout">
          <Segmented t={t} value={tweaks.direction} onChange={v=>setTweak('direction', v)} options={[
            { value:'LR', label:'Left → Right' },
            { value:'TB', label:'Top → Down' },
          ]}/>
        </TweakRow>

        <div style={{
          borderTop:`1px dashed ${t.line}`, paddingTop: 10,
          fontFamily: t.font.mono, fontSize: 10, color: t.fg3,
          lineHeight: 1.5,
        }}>
          Tweaks persist across navigations. Scope: visual system, node rendering, graph orientation.
        </div>
      </div>
    </div>
  );
}

function TweakRow({ label, children }) {
  return (
    <div>
      <div style={{
        fontSize: 10, fontWeight: 600, letterSpacing: 1, textTransform:'uppercase',
        color: 'var(--fg2)', marginBottom: 6,
        fontFamily: 'ui-monospace, Menlo',
        opacity: 0.7,
      }}>{label}</div>
      {children}
    </div>
  );
}

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