// Screens B — Live Build (hero) and Result Dashboard (hero)

const { useState: Sb, useEffect: Eb, useRef: Rb, useMemo: Mb } = React;

// Slice 6.4.5.3 Phase 6 (Item 8): GitHub-audit progress overlay.
// Renders a translucent banner at the top of LiveBuild when a GitHub audit
// is in flight. Reads `window.DAEV_GITHUB_PROGRESS` (mirrored into local
// state via `daev-github-progress` events). Hidden when progress.active is
// falsy or progress is null.
function GithubProgressOverlay({ t, progress }) {
  if (!progress || !progress.active) return null;
  const amber = (t && t.semantic && t.semantic.amber) || '#f59e0b';
  const accent = (t && t.accent && t.accent.base) || amber;
  const phaseColor = progress.phase === 'error' ? (t.semantic.red || '#ef4444')
    : progress.phase === 'still_processing' ? amber
    : accent;

  // Slice 6.4.7 Phase 6: client-side ETA. Calibration heuristic:
  //   * 30 s base (queue + zip fetch + per-file build_module_dag setup)
  //   * 1.0 s per file (build_module_dag + DefUseAnalyzer per .py)
  //   * 0.04 s per 1k tokens (SecurityAgent inference at ~25k tok/s
  //     mocked-LLM ceiling; real Nemotron is slower but warm runs hit
  //     this roughly with prompt caching)
  // For DVPWA (25 files, ~160k tokens) this predicts ≈ 30 + 25 + 6 = 61 s.
  // Empirical Probe Pack F warm-state was ~40 s — heuristic is an upper
  // bound which feels honest in the UI (better than under-promising).
  let etaText = null;
  if (progress.phase === 'polling' || progress.phase === 'queueing') {
    const fileCount = Number(progress.file_count) || 0;
    const tokens = Number(progress.estimated_tokens) || 0;
    const elapsed = Number(progress.elapsed_s) || 0;
    const totalEst = Math.round(30 + fileCount * 1.0 + tokens * 0.00004);
    const remaining = Math.max(0, totalEst - elapsed);
    if (totalEst > 0) {
      // Render as either "ETA ~Ns" or "ETA ~Nm Ms" — single line.
      if (remaining >= 60) {
        const m = Math.floor(remaining / 60);
        const s = remaining % 60;
        etaText = `ETA ~${m}m ${s}s`;
      } else {
        etaText = `ETA ~${remaining}s`;
      }
    }
  }

  return (
    <div style={{
      position: 'absolute',
      top: 0, left: 0, right: 0, zIndex: 50,
      padding: '10px 16px',
      background: `color-mix(in oklch, ${phaseColor} 12%, ${t.bg0})`,
      borderBottom: `1px solid ${phaseColor}`,
      display: 'flex', alignItems: 'center', gap: 14,
      fontFamily: t.font.mono, fontSize: 12,
      color: t.fg0,
    }}>
      <span style={{
        width: 8, height: 8, borderRadius: '50%',
        background: phaseColor,
        boxShadow: progress.phase !== 'error' ? `0 0 8px ${phaseColor}` : 'none',
        animation: progress.phase === 'polling' ? 'daev-blip 1.2s ease-in-out infinite' : 'none',
        flexShrink: 0,
      }}/>
      <span style={{
        textTransform: 'uppercase',
        letterSpacing: 0.6, fontSize: 10, fontWeight: 700,
        color: phaseColor, whiteSpace: 'nowrap',
      }}>
        github audit · {progress.phase || 'pending'}
      </span>
      <span style={{ color: t.fg1, flex: 1, minWidth: 0, overflow: 'hidden',
                     textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
        {progress.message}
      </span>
      {(progress.elapsed_s != null) && (
        <span style={{ color: t.fg2, whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' }}>
          {progress.elapsed_s}s
        </span>
      )}
      {etaText && (
        <span style={{ color: t.fg2, whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' }}>
          {etaText}
        </span>
      )}
      {(progress.file_count != null) && (
        <span style={{ color: t.fg2, whiteSpace: 'nowrap' }}>
          {progress.file_count} files
        </span>
      )}
      {(progress.estimated_tokens != null && progress.estimated_tokens > 0) && (
        <span style={{ color: t.fg3, whiteSpace: 'nowrap' }}>
          ~{Math.round(progress.estimated_tokens / 1000)}k tokens
        </span>
      )}
    </div>
  );
}

// Slice 6.4.5.3 Phase 4 (Item 6, completes BUG-76): friendly label for the
// LiveBuild meta strip "fixture" row. The lazy `_github_live` fixture is
// built from /audits/from-github responses; its `fx.label` is the finding
// title (e.g. "Hardcoded Secret in Password Hashing"), which is misleading
// for an audit-of-a-repo. Prefer the repo slug from audit_metadata.repo_url.
function getFixtureDisplayLabel(fx, fixtureId) {
  if (!fx) return '—';
  if (fixtureId === '_github_live') {
    const url = (fx.audit_metadata && fx.audit_metadata.repo_url) || null;
    if (url) {
      const m = url.match(/github\.com\/([^/]+\/[^/?#]+)/i);
      if (m && m[1]) return `GitHub: ${m[1].replace(/\.git$/, '')}`;
    }
    return 'GitHub: <unknown repo>';
  }
  if (fixtureId === '_user') return 'Custom paste';
  return fx.label || fixtureId;
}

// ─────────────────────────────────────────────────────────────
// Screen 2 — Live Build (SSE simulation)
// ─────────────────────────────────────────────────────────────
function LiveBuild({ t, fixtureId, mode, direction, nodeStyle, onDone, autoplay=true, onBack }) {
  // Slice 6.4.7 Phase 3 (HC-55): subscribe to daev-fixtures-hydrated so
  // when handleRunAudit replaces the prestaged fixture with the live
  // wired result the LiveBuild animation can react. Specifically: in
  // council mode, advancing past the local timeline while the backend
  // is still mid-debate would otherwise show the static fixture's
  // verdict before the real council_metadata arrives.
  const [hydrationTick, setHydrationTick] = Sb(0);
  Eb(() => {
    if (typeof window === 'undefined') return undefined;
    const handler = (ev) => {
      const detail = (ev && ev.detail) || {};
      if (!detail.fixture || detail.fixture === fixtureId) {
        setHydrationTick(k => k + 1);
      }
    };
    window.addEventListener('daev-fixtures-hydrated', handler);
    return () => window.removeEventListener('daev-fixtures-hydrated', handler);
  }, [fixtureId]);
  void hydrationTick;
  const fx = window.DAEV_FIXTURES[fixtureId];
  const [events, setEvents] = Sb([]);
  const [revealedN, setRevN] = Sb([]);
  const [revealedE, setRevE] = Sb([]);
  const [done, setDone] = Sb(false);
  const [pulseKey, setPulse] = Sb(0);
  const [hovered, setHovered] = Sb(null);
  const [startedAt] = Sb(Date.now());
  const [elapsed, setElapsed] = Sb(0);
  const [skipVisible, setSkipVisible] = Sb(false);

  // Slice 6.4.5.3 Phase 6 (Item 8): mirror window.DAEV_GITHUB_PROGRESS into
  // local state so the GithubProgressOverlay re-renders when the progress
  // bus dispatches an update. handleRunGithubAudit in app.jsx fires
  // 'daev-github-progress' events whenever it transitions phase or polls.
  const [ghProgress, setGhProgress] = Sb(() =>
    (typeof window !== 'undefined' && window.DAEV_GITHUB_PROGRESS) || null);
  Eb(() => {
    const handler = (ev) => setGhProgress((ev && ev.detail) || window.DAEV_GITHUB_PROGRESS || null);
    if (typeof window !== 'undefined') {
      window.addEventListener('daev-github-progress', handler);
      return () => window.removeEventListener('daev-github-progress', handler);
    }
    return undefined;
  }, []);

  Eb(() => {
    // Slice 6.4.5.2 Phase 8 (BUG-84): freeze the elapsed timer when the audit
    // reaches the "done" state. Re-running the effect on `done` flip clears
    // the interval, so the displayed elapsed value pins at the completion
    // moment instead of incrementing forever after View result appears.
    if (done) return undefined;
    const iv = setInterval(() => setElapsed((Date.now()-startedAt)/1000), 80);
    const skipTimer = setTimeout(() => setSkipVisible(true), 3000);
    return () => { clearInterval(iv); clearTimeout(skipTimer); };
  }, [startedAt, done]);

  // Build sequence
  const timeline = Mb(() => {
    const t0 = 120;
    const steps = [];
    steps.push({ at: 0, ev: { kind:'meta', msg:`▸ audit started · ${fixtureId} · mode=${mode}` }});
    steps.push({ at: 200, ev: { kind:'meta', msg:`▸ parsing ${fx.lines.length} lines · ${fx.lang}` }});
    steps.push({ at: 500, ev: { kind:'meta', msg:`▸ def-use graph · construct` }});

    let cursor = 700;
    fx.graph.nodes.forEach((n, i) => {
      steps.push({ at: cursor, node: n.id, ev: { kind:'node', node: n, msg: `• node · ${n.kind} · ${n.label}` }});
      // reveal incoming edges after node
      const incoming = fx.graph.edges.filter(e => e.to === n.id && fx.graph.nodes.findIndex(x=>x.id===e.from) < i);
      incoming.forEach((e, ei) => {
        steps.push({ at: cursor + 80 + ei*40, edge: e.id, ev: { kind:'edge', edge:e, msg: `  └ edge · ${e.kind} · ${e.from}→${e.to}` }});
      });
      cursor += 340 + Math.random()*80;
    });

    // remaining edges
    const seen = new Set();
    steps.forEach(s => s.edge && seen.add(s.edge));
    fx.graph.edges.forEach(e => {
      if (!seen.has(e.id)) {
        steps.push({ at: cursor, edge: e.id, ev: { kind:'edge', edge:e, msg:`  └ edge · ${e.kind} · ${e.from}→${e.to}`}});
        cursor += 90;
      }
    });

    cursor += 200;
    steps.push({ at: cursor, ev: { kind:'meta', msg:`▸ symbolic reasoning · ${mode === 'council' ? '5-agent council' : 'fast-path'}` }});
    cursor += 400;

    if (fx.finding) {
      steps.push({ at: cursor, pulse: true, ev: { kind:'finding', finding: fx.finding,
        msg:`⚠ finding · ${fx.finding.title} · line ${fx.finding.line}` }});
      cursor += 500;
      steps.push({ at: cursor, ev: { kind:'meta', msg:`▸ counterfactual verify · chain confirmed (${fx.finding.hops} hops)` }});
    } else {
      steps.push({ at: cursor, ev: { kind:'safe', msg:`✓ no unsafe taint flow reached a sink` }});
      cursor += 400;
      steps.push({ at: cursor, ev: { kind:'meta', msg:`▸ counterfactual verify · sanitizer dominates sink` }});
    }
    cursor += 500;
    steps.push({ at: cursor, end: true, ev: { kind:'meta', msg:`✓ audit complete · ${fixtureId} · ${(cursor/1000).toFixed(1)}s` }});
    return steps;
  }, [fx, fixtureId, mode]);

  Eb(() => {
    if (!autoplay) return;
    const timers = [];
    timeline.forEach(step => {
      const T = setTimeout(() => {
        setEvents(prev => [...prev, step.ev]);
        if (step.node) setRevN(prev => [...prev, step.node]);
        if (step.edge) setRevE(prev => [...prev, step.edge]);
        if (step.pulse) setPulse(k => k+1);
        if (step.end) {
          // Slice 6.4.7 Phase 3 (HC-55): for council mode, defer the
          // "done" transition until the backend audit completes so the
          // ResultDashboard never renders the static-fixture verdict
          // ahead of the real council_metadata. The static-fixture
          // animation continues looping at the final node until then;
          // a 120 s hard cap surfaces the dashboard regardless so a
          // backend hiccup doesn't stall the demo indefinitely.
          const liveFx = window.DAEV_FIXTURES[fixtureId];
          const liveCouncilReady = liveFx && liveFx.audit_mode === 'council' && liveFx.council_metadata;
          if (mode === 'council' && !liveCouncilReady) {
            // Insert a "waiting on council debate" event line if not
            // already shown; the actual setDone fires from the hydrate
            // listener effect below or the 120 s safety timeout.
            setEvents(prev => prev.some(e => e.kind === 'council-wait')
              ? prev
              : [...prev, {
                kind: 'council-wait',
                msg: '▸ council debate in progress · 5 specialists · ~30-90s typical'
              }]);
            return;
          }
          setDone(true);
          onDone && onDone();
        }
      }, step.at);
      timers.push(T);
    });
    return () => timers.forEach(clearTimeout);
  }, [timeline, autoplay, mode, fixtureId]);

  // Slice 6.4.7 Phase 3 (HC-55): once live audit hydration arrives
  // (handleRunAudit dispatches daev-fixtures-hydrated; LiveBuild
  // listens via the hydrationTick state above), promote the LiveBuild
  // to "done" so ResultDashboard can render the live council data.
  Eb(() => {
    if (mode !== 'council') return;
    if (done) return;
    const liveFx = window.DAEV_FIXTURES[fixtureId];
    if (liveFx && liveFx.audit_mode === 'council' && liveFx.council_metadata) {
      setDone(true);
      onDone && onDone();
    }
  }, [hydrationTick, mode, fixtureId, done]);

  // Safety timeout — surface the dashboard after 120 s even if the
  // backend never returns. The verdict will be the static fixture's
  // (non-council fast-mode prestage), but the demo doesn't stall.
  Eb(() => {
    if (mode !== 'council') return;
    if (done) return;
    const safety = setTimeout(() => {
      setEvents(prev => [...prev, {
        kind: 'council-timeout',
        msg: '▸ council audit timeout · falling through to prestaged fixture'
      }]);
      setDone(true);
      onDone && onDone();
    }, 120_000);
    return () => clearTimeout(safety);
  }, [mode, done]);

  const totalNodes = fx.graph.nodes.length;
  const totalEdges = fx.graph.edges.length;
  const progress = Math.min(1, (revealedN.length + revealedE.length) / (totalNodes + totalEdges));

  return (
    <div style={{ flex:1, display:'flex', minHeight:0, position: 'relative' }}>
      {/* Slice 6.4.5.3 Phase 6 (Item 8): GitHub-audit progress overlay.
          Visible only while window.DAEV_GITHUB_PROGRESS.active is true. */}
      <GithubProgressOverlay t={t} progress={ghProgress}/>
      {/* LEFT: event log + status */}
      <div style={{
        width: 380, borderRight:`1px solid ${t.line}`,
        display:'flex', flexDirection:'column', background: t.bg0,
        flexShrink:0,
      }}>
        <div style={{
          padding:'12px 14px', borderBottom:`1px solid ${t.line}`,
          display:'flex', flexDirection:'column', gap: 10,
        }}>
          <div style={{
            display:'flex', alignItems:'center', justifyContent:'space-between',
            gap: 10,
          }}>
            <div style={{
              fontFamily: t.font.mono, fontSize: 10,
              color: t.fg2, letterSpacing: 1, textTransform:'uppercase',
              display:'inline-flex', alignItems:'center', gap: 6,
            }} title="The DAEV engine narrates each reasoning step as it traces the causal path. Watch a finding emerge from the source code in real time.">
              {!done && (
                <span style={{
                  width: 6, height: 6, borderRadius:'50%',
                  background: t.accent.base,
                  boxShadow: `0 0 8px ${t.accent.base}`,
                  animation: 'daev-blip 1.2s ease-in-out infinite',
                  flexShrink: 0,
                }}/>
              )}
              live · streaming reasoning
            </div>
            {/* ETA pill — calms first-time-visitor "is this hung?" anxiety.
                Footnote reconciles the hero "600ms verdict" claim with the
                ~6s/~42s full-stream timings shown here. */}
            <span title="600ms is the verdict-only timing for the Fast graph. ~6s is the Fast mode with the full reasoning stream you're watching now. ~42s is Full mode with the 5-agent council."
              style={{
                fontFamily: t.font.mono, fontSize: 10, fontWeight: 600,
                color: done ? t.semantic.green : t.accent.base,
                background: done
                  ? `color-mix(in oklch, ${t.semantic.green} 14%, transparent)`
                  : `color-mix(in oklch, ${t.accent.base} 14%, transparent)`,
                border: `1px solid ${done ? t.semantic.green : t.accent.base}`,
                borderRadius: 999,
                padding: '3px 10px',
                letterSpacing: 0.6, textTransform: 'uppercase',
                whiteSpace: 'nowrap',
                fontVariantNumeric: 'tabular-nums',
                cursor: 'help',
              }}>
              {done ? 'complete' : (mode === 'fast' ? 'eta ~6s · 600ms verdict' : 'eta ~42s · council')}
            </span>
          </div>

          {/* progress + stats */}
          <div>
            <div style={{
              height: 2, background: t.bg2, borderRadius: 1, overflow:'hidden',
              position:'relative',
            }}>
              <div style={{
                width: `${progress*100}%`, height: '100%',
                background: done ? t.semantic.green : t.accent.base,
                transition: 'width 260ms ease-out',
                boxShadow: `0 0 8px ${done ? t.semantic.green : t.accent.base}`,
              }}/>
            </div>
            <div style={{
              marginTop: 8,
              display:'flex', justifyContent:'space-between',
              fontFamily: t.font.mono, fontSize: 10, color: t.fg2,
              letterSpacing: 0.5,
            }}>
              <span>{revealedN.length}/{totalNodes} nodes · {revealedE.length}/{totalEdges} edges</span>
              <span style={{ fontVariantNumeric:'tabular-nums' }}>{elapsed.toFixed(1)}s</span>
            </div>
          </div>
        </div>

        {/* event list */}
        <div data-tour="live-stream" style={{ flex:1, overflow:'auto', padding:'8px 0' }}>
          {events.map((e, i) => (
            <div key={i} style={{
              padding: '3px 14px',
              fontFamily: t.font.mono, fontSize: 11.5, lineHeight: 1.5,
              color: e.kind==='finding' ? t.semantic.red :
                     e.kind==='safe' ? t.semantic.green :
                     e.kind==='node' ? t.fg0 :
                     e.kind==='edge' ? t.fg2 :
                     t.fg1,
              animation: 'daev-log-in 160ms ease-out both',
              whiteSpace: 'pre-wrap',
              wordBreak: 'break-word',
            }}>{e.msg}</div>
          ))}
        </div>

        {/* bottom meta */}
        <div style={{
          padding: '10px 14px', borderTop:`1px solid ${t.line}`,
          fontFamily: t.font.mono, fontSize: 10, color: t.fg2,
          letterSpacing: 0.5,
          display:'grid', gridTemplateColumns:'1fr 1fr', gap: 4,
        }}>
          <span>audit_id</span>
          <span style={{ color: t.fg1, textAlign:'right' }}>{fx.audit_id || `a_0x9f2e.${fixtureId}`}</span>
          <span>mode</span>
          <span style={{ color: t.fg1, textAlign:'right' }}>{mode}</span>
          <span>fixture</span>
          <span style={{ color: t.fg1, textAlign:'right' }}>{getFixtureDisplayLabel(fx, fixtureId)}</span>
        </div>
      </div>

      {/* RIGHT: graph canvas */}
      <div style={{ flex:1, display:'flex', flexDirection:'column', background: t.bg0 }}>
        <div style={{
          padding:'10px 14px', borderBottom:`1px solid ${t.line}`,
          display:'flex', alignItems:'center', gap: 14,
        }}>
          <div style={{
            fontFamily: t.font.mono, fontSize: 10,
            color: t.fg2, letterSpacing: 1, textTransform:'uppercase',
          }}>causal graph</div>
          <Chip t={t} tone="neutral" size="sm">
            dagre · {direction === 'LR' ? 'left → right' : 'top → down'}
          </Chip>
          <div style={{ flex:1 }}/>
          {!done && skipVisible && (
            <button onClick={() => onDone && onDone(true)}
              title="Skip the build animation and jump to the result"
              style={{
                background:'transparent', border:`1px solid ${t.line}`,
                borderRadius: t.radius.sm,
                padding:'5px 10px',
                fontFamily: t.font.mono, fontSize: 11, fontWeight: 500,
                color: t.fg2, cursor:'pointer',
                letterSpacing: 0.4,
                display:'inline-flex', alignItems:'center', gap: 6,
                transition:'color 180ms, border-color 180ms',
              }}
              onMouseOver={e => { e.currentTarget.style.color = t.accent.base; e.currentTarget.style.borderColor = t.accent.base; }}
              onMouseOut={e => { e.currentTarget.style.color = t.fg2; e.currentTarget.style.borderColor = t.line; }}>
              Skip to results <Icon name="arrow_r" size={11} color="currentColor"/>
            </button>
          )}
          {done && (
            <Button t={t} variant="primary" size="sm"
              onClick={() => onDone && onDone(true)}
              icon={<Icon name="arrow_r" size={12} color={t.mode==='dark'?'oklch(0.14 0.01 240)':'white'}/>}>
              View result
            </Button>
          )}
        </div>

        <div data-tour="live-graph" style={{ flex:1, position:'relative', minHeight:0 }}>
          <CausalGraph
            t={t}
            graph={fx.graph}
            revealedNodes={revealedN}
            revealedEdges={revealedE}
            taintPath={done && fx.finding ? fx.finding.path : null}
            pulseKey={pulseKey}
            direction={direction}
            onHoverNode={setHovered}
            hoveredNode={hovered}
            nodeStyle={nodeStyle}
          />
          {hovered && (
            <NodeTooltip t={t} node={fx.graph.nodes.find(n=>n.id===hovered)}/>
          )}
        </div>

        <GraphLegend t={t}/>
      </div>

      <style>{`
        @keyframes daev-blip { 0%,100% { opacity:0.4 } 50% { opacity:1 } }
        @keyframes daev-log-in {
          from { opacity: 0; transform: translateX(-4px); }
          to   { opacity: 1; transform: translateX(0); }
        }
      `}</style>
    </div>
  );
}

function NodeTooltip({ t, node }) {
  if (!node) return null;
  const style = ENTITY_STYLE[node.kind];
  return (
    <div style={{
      position:'absolute', top: 16, right: 16,
      background: t.bg1, border:`1px solid ${t.line2}`,
      borderRadius: t.radius.md,
      padding: '10px 12px', minWidth: 220,
      boxShadow:`0 8px 24px oklch(0 0 0 / 0.3)`,
      pointerEvents:'none',
    }}>
      <div style={{ display:'flex', alignItems:'center', gap: 8, marginBottom: 8 }}>
        <Icon name={style.glyph} size={14} color={t.accent.base}/>
        <div style={{ fontFamily: t.font.mono, fontSize: 12, fontWeight:600, color: t.fg0 }}>
          {node.label}
        </div>
      </div>
      <div style={{
        display:'grid', gridTemplateColumns:'auto 1fr', gap:'4px 12px',
        fontFamily: t.font.mono, fontSize: 10.5,
      }}>
        <span style={{ color:t.fg2 }}>entity_type</span>
        <span style={{ color:t.fg0 }}>{node.kind}</span>
        <span style={{ color:t.fg2 }}>semantic_role</span>
        <span style={{ color:t.fg0 }}>{node.role}</span>
        <span style={{ color:t.fg2 }}>source_line</span>
        <span style={{ color:t.fg0 }}>L{node.line}</span>
        <span style={{ color:t.fg2 }}>taint_label</span>
        <span style={{ color: node.taint ? t.semantic.red : t.semantic.green }}>
          {node.taint ? 'TAINTED' : 'clean'}
        </span>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Screen 3 — Result Dashboard
// ─────────────────────────────────────────────────────────────
function ResultDashboard({ t, fixtureId, mode, direction, nodeStyle, onNewAudit, onSwitchToFull, forcedTab, clearForcedTab }) {
  // Slice 6.4.7 Phase 3 (HC-55): subscribe to daev-fixtures-hydrated so
  // the dashboard re-renders when handleRunAudit replaces
  // window.DAEV_FIXTURES[fixtureId] with a freshly-wired fixture
  // (e.g. when a council-mode audit completes 30-90 s after the user
  // clicked Audit and LiveBuild's ~10 s animation already advanced).
  // Without this, CouncilPanel keeps rendering the stale prestaged
  // fixture even though handleRunAudit hydrated council_metadata.
  const [hydrationTick, setHydrationTick] = Sb(0);
  Eb(() => {
    if (typeof window === 'undefined') return undefined;
    const handler = (ev) => {
      const detail = (ev && ev.detail) || {};
      if (!detail.fixture || detail.fixture === fixtureId) {
        setHydrationTick(k => k + 1);
      }
    };
    window.addEventListener('daev-fixtures-hydrated', handler);
    return () => window.removeEventListener('daev-fixtures-hydrated', handler);
  }, [fixtureId]);
  // hydrationTick is intentionally read here so React tracks the
  // dependency even though we read fx via window directly.
  void hydrationTick;
  const fx = window.DAEV_FIXTURES[fixtureId];

  // Slice 6.4.5.3 Phase 1 (Item 1) defense-in-depth: ResultDashboard must not
  // crash if zombie state slipped through App's safeBoot validation. The lazy
  // `_github_live` fixture is the prime offender — it only exists after a
  // GitHub audit completes, so a stored daev.fixture='_github_live' on cold
  // load would crash on `fx.finding` access. Render a friendly fallback.
  if (!fx) {
    return (
      <div style={{ flex:1, display:'flex', alignItems:'center', justifyContent:'center',
                    padding: 32, textAlign: 'center', color: t?.fg2 }}>
        <div>
          <h3 style={{ marginBottom: 8 }}>// no fixture loaded</h3>
          <p style={{ marginBottom: 24 }}>Click a fixture or paste code to begin.</p>
          {onNewAudit && (
            <button onClick={onNewAudit} style={{ padding: '8px 16px', cursor: 'pointer' }}>
              New audit
            </button>
          )}
        </div>
      </div>
    );
  }
  if (fx.finding === undefined && fixtureId !== 'safe' && fixtureId !== '_user') {
    return (
      <div style={{ flex:1, display:'flex', alignItems:'center', justifyContent:'center',
                    padding: 32, textAlign: 'center', color: t?.fg2 }}>
        <div>
          <h3 style={{ marginBottom: 8 }}>// audit pending</h3>
          <p style={{ marginBottom: 24 }}>This fixture has not been audited yet. Click <strong>Audit</strong> to begin.</p>
          {onNewAudit && (
            <button onClick={onNewAudit} style={{ padding: '8px 16px', cursor: 'pointer' }}>
              New audit
            </button>
          )}
        </div>
      </div>
    );
  }

  const finding = fx.finding;
  const verdict = finding ? 'UNSAFE' : 'SAFE';
  const [expanded, setExpanded] = Sb(finding ? finding.id : null);
  const [hovered, setHovered] = Sb(null);
  const [selectedNode, setSelectedNode] = Sb(null);
  // Causal is now the primary view — that's the differentiated story.
  // Findings tab still leads with the verdict card; Causal opens with the
  // Rung-1/2/3 reasoning so visitors see the WHY first.
  // Audit fix: when selected fixture has no finding (`safe`), default to SAST
  // tab so the visitor sees the 7-tool battery clean rather than empty Findings.
  const [userTab, setUserTab] = Sb(finding ? 'causal' : 'sast');
  // Tour can force a tab (steps 10-14). User clicks reset the override.
  const tab = forcedTab || userTab;
  const setTab = (next) => {
    if (clearForcedTab) clearForcedTab();
    setUserTab(next);
  };
  const [devOpen, setDevOpen] = Sb(false);
  const [moreOpen, setMoreOpen] = Sb(false);

  // Selected node defaults to the root cause (last node in taint path) so the
  // bottom strip is never empty — investors watching a screen-share see a
  // persistent label without needing to hover.
  const defaultNodeId = finding ? finding.path[finding.path.length - 1] : (fx.graph.nodes[0] && fx.graph.nodes[0].id);
  const visibleNodeId = selectedNode || hovered || defaultNodeId;
  const visibleNode = fx.graph.nodes.find(n => n.id === visibleNodeId);

  return (
    <div style={{ flex:1, display:'flex', flexDirection:'column', minHeight:0 }}>
      {/* Verdict banner */}
      <div data-tour="verdict-banner"><VerdictBanner t={t} verdict={verdict} fx={fx} mode={mode}/></div>

      <div style={{ flex:1, display:'flex', minHeight:0 }}>
        {/* LEFT: findings / tabs */}
        <div style={{
          width: 460, borderRight:`1px solid ${t.line}`,
          display:'flex', flexDirection:'column', background: t.bg0,
          flexShrink: 0,
        }}>
          {/* PANE LABEL — // ANALYSIS — symmetrical with // CAUSAL GRAPH on right */}
          <div style={{
            padding:'10px 14px', borderBottom:`1px solid ${t.line}`,
            display:'flex', alignItems:'center', gap: 10,
            fontFamily: t.font.mono, fontSize: 11, fontWeight: 700,
            color: t.accent.base, letterSpacing: 1.4, textTransform:'uppercase',
          }}>
            <span style={{ color: t.accent.base }}>//</span> analysis
          </div>
          <div style={{
            display:'flex', alignItems:'center',
            borderBottom:`1px solid ${t.line}`,
            padding: '0 14px',
            position:'relative',
          }}>
            {[
              { id:'causal',   tour:'tab-causal',   label:'Causal',   hint:'Rung 1/2/3 reasoning · why this is the cause' },
              { id:'findings', tour:null,           label: finding ? 'Findings · 1' : 'Findings · 0', hint:'Verdict + suggested fix' },
              { id:'council',  tour:'tab-council',  label:'Council',  hint:'SecurityAgent verdict · switch to Council mode for 5-agent debate' },
              { id:'sast',     tour:'tab-sast',     label:'SAST',     hint:'Side-by-side: what static analyzers found' },
            ].map(x => (
              <button key={x.id}
                data-tour={x.tour || undefined}
                onClick={()=>{ setTab(x.id); setMoreOpen(false); }}
                title={x.hint}
                style={{
                  padding:'10px 0', marginRight: 18,
                  background:'transparent', border:'none',
                  borderBottom:`2px solid ${tab===x.id ? t.accent.base : 'transparent'}`,
                  color: tab===x.id ? t.fg0 : t.fg2,
                  fontFamily: t.font.ui, fontSize: 12, fontWeight: tab===x.id ? 600 : 500,
                  cursor:'pointer',
                }}>{x.label}</button>
            ))}
            <div style={{ flex:1 }}/>
            {/* ⋯ More — overflow tabs (Metadata / Export) */}
            <div style={{ position:'relative' }}>
              <button
                onClick={() => setMoreOpen(o => !o)}
                title="Metadata · Export"
                style={{
                  padding:'8px 10px', marginRight: 0,
                  background: (tab==='meta' || tab==='export' || moreOpen) ? t.bg2 : 'transparent',
                  border: `1px solid ${(tab==='meta' || tab==='export' || moreOpen) ? t.line2 : 'transparent'}`,
                  borderRadius: t.radius.sm,
                  color: (tab==='meta' || tab==='export') ? t.fg0 : t.fg2,
                  fontFamily: t.font.ui, fontSize: 12, fontWeight: 600,
                  cursor:'pointer',
                  letterSpacing: 1,
                }}>⋯ More</button>
              {moreOpen && (
                <>
                  {/* Click-outside scrim */}
                  <div onClick={() => setMoreOpen(false)} style={{
                    position:'fixed', inset: 0, zIndex: 9,
                  }}/>
                  <div style={{
                    position:'absolute', top: 'calc(100% + 4px)', right: 0,
                    background: t.bg1, border: `1px solid ${t.line2}`,
                    borderRadius: t.radius.md,
                    boxShadow: `0 8px 24px rgba(0,0,0,0.15)`,
                    zIndex: 10, minWidth: 180,
                    padding: 4,
                  }}>
                    <div style={{
                      display:'flex', alignItems:'center', justifyContent:'space-between',
                      padding:'6px 8px 6px 10px',
                      borderBottom:`1px solid ${t.line}`,
                      marginBottom: 4,
                    }}>
                      <span style={{
                        fontFamily: t.font.mono, fontSize: 9, fontWeight: 700,
                        color: t.fg3, letterSpacing: 1.4, textTransform:'uppercase',
                      }}>more</span>
                      <button onClick={() => setMoreOpen(false)}
                        aria-label="Close" title="Close"
                        style={{
                          width: 20, height: 20, padding: 0,
                          background:'transparent', border:'none',
                          borderRadius: t.radius.sm,
                          color: t.fg2, cursor:'pointer',
                          display:'inline-flex', alignItems:'center', justifyContent:'center',
                        }}>
                        <Icon name="x" size={12}/>
                      </button>
                    </div>
                    {[
                      { id:'meta',   label:'Metadata' },
                      { id:'export', label:'Export'   },
                    ].map(x => (
                      <button key={x.id}
                        onClick={()=>{ setTab(x.id); setMoreOpen(false); }}
                        style={{
                          display:'block', width:'100%', textAlign:'left',
                          padding:'8px 12px',
                          background: tab===x.id ? t.bg2 : 'transparent',
                          border:'none', borderRadius: t.radius.sm,
                          color: tab===x.id ? t.fg0 : t.fg1,
                          fontFamily: t.font.ui, fontSize: 12,
                          fontWeight: tab===x.id ? 600 : 500,
                          cursor:'pointer',
                        }}>{x.label}</button>
                    ))}
                  </div>
                </>
              )}
            </div>
          </div>

          <div style={{ flex:1, overflow:'auto' }}>
            {tab === 'findings' && (finding ? (
              <>
                <FindingCard t={t} finding={finding} expanded={expanded===finding.id}
                  onToggle={() => setExpanded(expanded===finding.id ? null : finding.id)}/>
                {/* "SAST signal reduction" callout — answers the steelman question
                    "why would I trust DAEV over my existing SAST investment?" */}
                <div data-tour="signal-reduction" style={{
                  margin: '0 14px 14px',
                  padding: '12px 14px',
                  background: t.bg1,
                  border: `1px solid ${t.line}`,
                  borderLeft: `3px solid ${t.accent.base}`,
                  borderRadius: t.radius.sm,
                }}>
                  <div style={{
                    fontFamily: t.font.mono, fontSize: 10, fontWeight: 700,
                    color: t.accent.base, letterSpacing: 1.4, textTransform:'uppercase',
                    marginBottom: 6,
                  }}>// signal reduction</div>
                  <div style={{
                    fontFamily: t.font.ui, fontSize: 13.5, fontWeight: 500,
                    color: t.fg0, lineHeight: 1.5,
                  }}>
                    SAST: <b style={{ color: t.semantic.amber }}>4 signals</b>
                    <span style={{ color: t.fg2, margin: '0 8px' }}>→</span>
                    DAEV: <b style={{ color: t.accent.base }}>1 confirmed cause</b>
                  </div>
                  <div style={{
                    fontFamily: t.font.mono, fontSize: 11, color: t.fg2, marginTop: 6, lineHeight: 1.5,
                  }}>
                    Pattern matchers raised 3 false positives + 1 real signal. DAEV's counterfactual proof rejected the 3 and confirmed the 1 — with a causal chain you can show your auditor.
                  </div>
                </div>
              </>
            ) : (
              <EmptyFindings t={t} fx={fx}/>
            ))}
            {tab === 'meta' && <MetaPanel t={t} fx={fx} mode={mode} verdict={verdict}/>}
            {tab === 'export' && <ExportPanel t={t} fx={fx} mode={mode}/>}
            {tab === 'council' && <CouncilPanel t={t} fx={fx} mode={mode}
              onSwitchToFull={onSwitchToFull}/>}
            {tab === 'sast' && <SastPanel t={t} fx={fx} mode={mode}/>}
            {tab === 'causal' && <CausalPanel t={t} fx={fx} mode={mode}/>}
          </div>
        </div>

        {/* RIGHT: persistent graph with finding highlight */}
        <div style={{ flex:1, display:'flex', flexDirection:'column', background: t.bg0, position:'relative' }}>
          {/* PANE LABEL — // CAUSAL GRAPH — matches site // SECTION idiom */}
          <div style={{
            padding:'10px 14px', borderBottom:`1px solid ${t.line}`,
            display:'flex', alignItems:'center', gap: 14, flexWrap:'wrap',
          }}>
            <div style={{
              fontFamily: t.font.mono, fontSize: 11, fontWeight: 700,
              color: t.accent.base, letterSpacing: 1.4, textTransform:'uppercase',
              display:'inline-flex', alignItems:'center', gap: 6,
            }}>
              <span style={{ color: t.accent.base }}>//</span> causal graph
            </div>
            <Chip t={t} tone={finding ? 'red' : 'green'} size="sm">
              {finding ? `taint path · ${finding.hops} hops` : 'no taint flow'}
            </Chip>
            {/* Slice 6.4.5.2 Phase 13 (BUG-87): truncation banner — surfaces
                when the backend graph exceeded MAX_DISPLAY_NODES (e.g.,
                pallets/click /audits/from-github → 9310 nodes downsampled
                to ≤80). Shows the original counts + a clarifying note. */}
            {fx.graph && fx.graph.truncated ? (
              <Chip t={t} tone="amber" size="sm" title={
                `Backend graph: ${fx.graph.original_node_count} nodes / ` +
                `${fx.graph.original_edge_count} edges. Downsampled via BFS ` +
                `from finding causal_path for the demo viewport.`
              }>
                ⚠ truncated · {fx.graph.original_node_count} → {fx.graph.nodes.length} nodes
              </Chip>
            ) : null}
            <div style={{ flex:1 }}/>
            <CopyLinkButton t={t} fixtureId={fixtureId} mode={mode}/>
            <Button t={t} size="sm" variant="ghost"
              icon={<Icon name="settings" size={12}/>}
              onClick={()=>setDevOpen(true)}>Dev panel</Button>
            <Button t={t} size="sm" variant="ghost"
              icon={<Icon name="plus" size={12}/>}
              onClick={onNewAudit}>New audit</Button>
          </div>
          <div data-tour="result-graph" style={{ flex:1, position:'relative', minHeight:0,
            // Light mode: subtle drop-shadow so graph lifts off the warm off-white surface
            boxShadow: t.mode === 'light' ? `inset 0 1px 0 rgba(0,0,0,0.02), 0 1px 4px rgba(0,0,0,0.04)` : 'none',
          }}>
            <CausalGraph
              t={t}
              graph={fx.graph}
              taintPath={finding ? finding.path : null}
              pulseKey={0}
              direction={direction}
              onHoverNode={setHovered}
              hoveredNode={hovered}
              /* Result graph: cards by default — labels live inside rect, no edge collision.
                 Visitor can still override via the Tweaks panel if dev iframe is on. */
              nodeStyle={nodeStyle === 'dots' ? 'cards' : nodeStyle}
            />
            {hovered && (
              <NodeTooltip t={t} node={fx.graph.nodes.find(n=>n.id===hovered)}/>
            )}

            {/* (Old result-tour overlay removed — replaced by 16-step
                guided walkthrough in tour.jsx, Step 08 covers this moment.) */}
          </div>

          {/* Persistent selected-node strip — root cause by default,
              switches on hover or click. No more "what am I looking at?" */}
          {visibleNode && (
            <div data-tour="node-strip" style={{
              padding:'9px 14px',
              borderTop:`1px solid ${t.line}`,
              background: t.bg1,
              display:'flex', alignItems:'center', gap: 14,
              minHeight: 48,
            }}>
              <div style={{
                width: 8, height: 8, borderRadius:'50%',
                background: finding && finding.path.includes(visibleNode.id) ? t.semantic.red : t.accent.base,
                boxShadow: `0 0 8px ${finding && finding.path.includes(visibleNode.id) ? t.semantic.red : t.accent.base}`,
                flexShrink: 0,
              }}/>
              <div style={{
                fontFamily: t.font.mono, fontSize: 12, fontWeight: 600,
                color: finding && finding.path.includes(visibleNode.id) ? t.semantic.red : t.fg0,
              }}>{visibleNode.label}</div>
              <div style={{ width: 1, height: 14, background: t.line }}/>
              <div style={{
                fontFamily: t.font.mono, fontSize: 10, fontWeight: 600,
                color: t.fg2, letterSpacing: 1, textTransform:'uppercase',
              }}>{visibleNode.kind}</div>
              {visibleNode.role && (
                <>
                  <div style={{ width: 1, height: 14, background: t.line }}/>
                  <div style={{ fontFamily: t.font.mono, fontSize: 11, color: t.fg2 }}>
                    {visibleNode.role}
                  </div>
                </>
              )}
              <div style={{ width: 1, height: 14, background: t.line }}/>
              <div style={{ fontFamily: t.font.mono, fontSize: 11, color: t.fg2 }}>
                line {visibleNode.line}
              </div>
              {finding && finding.path.includes(visibleNode.id) && (
                <>
                  <div style={{ flex:1 }}/>
                  <div style={{
                    fontFamily: t.font.mono, fontSize: 10, fontWeight: 700,
                    color: t.semantic.red, letterSpacing: 1.2, textTransform:'uppercase',
                    padding:'2px 8px', border:`1px solid ${t.semantic.red}`,
                    borderRadius: t.radius.sm,
                  }}>on taint path</div>
                </>
              )}
              {(!finding || !finding.path.includes(visibleNode.id)) && finding && (
                <>
                  <div style={{ flex:1 }}/>
                  <div style={{
                    fontFamily: t.font.mono, fontSize: 11, color: t.fg3,
                  }}>confidence {(finding.confidence || 0).toFixed(2)}</div>
                </>
              )}
            </div>
          )}
          <GraphLegend t={t}/>
        </div>
      </div>
      <DeveloperPanelDrawer t={t} open={devOpen} onClose={()=>setDevOpen(false)} fx={fx} mode={mode}/>
    </div>
  );
}

function VerdictBanner({ t, verdict, fx, mode }) {
  const tone = verdict === 'UNSAFE' ? 'red' : verdict === 'SAFE' ? 'green' : 'amber';
  const color = tone === 'red' ? t.semantic.red : tone === 'green' ? t.semantic.green : t.semantic.amber;
  const conf = fx.finding?.confidence ?? 0.99;
  return (
    <div style={{
      padding:'18px 24px',
      borderBottom:`1px solid ${t.line}`,
      background: `color-mix(in oklch, ${color} 6%, ${t.bg0})`,
      display:'flex', alignItems:'center', gap: 20,
    }}>
      <div style={{
        width: 42, height: 42, borderRadius: t.radius.sm,
        background: `color-mix(in oklch, ${color} 22%, ${t.bg0})`,
        border:`1px solid ${color}`,
        display:'flex', alignItems:'center', justifyContent:'center',
        color,
      }}>
        <Icon name={verdict === 'UNSAFE' ? 'warn' : 'check'} size={20} color={color}/>
      </div>
      <div>
        <div style={{
          fontFamily: t.font.ui, fontSize: 26, fontWeight: 700,
          color: color, letterSpacing: -0.3, lineHeight: 1,
        }}>{verdict}</div>
        <div style={{
          marginTop: 6,
          fontFamily: t.font.mono, fontSize: 11, color: t.fg2,
        }}>
          {fx.finding
            ? `${fx.finding.title} · ${fx.finding.cwe} · confidence ${(conf*100).toFixed(0)}%`
            : 'No unsafe taint flow reaches a sink. Sanitizer dominates query site.'}
        </div>
        {/* Causal chain footer — echoes the marketing-page hero so investors
            who saw "auth/token_refresh.rs:203 · confidence 0.97" on the site
            recognise the same artifact in the demo. */}
        {fx.finding && (() => {
          const fileMap = {
            sqli:      'payments/checkout.py',
            cmdi:      'infra/ssh_executor.py',
            pathtrav:  'storage/s3_proxy.py',
            hardcoded: 'config/secrets.py',
            'xss-js':  'frontend/Profile.tsx',
            'cmdi-js': 'ops/diag.js',
            racecond:  'billing/redeem.py',
          };
          const file = fileMap[fx.id] || (fx.id + '.py');
          return (
            <div style={{
              marginTop: 4,
              fontFamily: t.font.mono, fontSize: 11, color: t.fg2,
              letterSpacing: 0.2,
              display: 'inline-flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
            }}>
              <span>{file}:{fx.finding.line}</span>
              <span style={{ color: t.fg3 }}>·</span>
              <span>{fx.finding.hops} hops</span>
              <span style={{ color: t.fg3 }}>·</span>
              <span style={{ color: t.accent.base, fontWeight: 600 }}>counterfactual confirmed</span>
            </div>
          );
        })()}
      </div>

      <div style={{ flex:1 }}/>

      {/* stats row */}
      <div style={{ display:'flex', gap: 28 }}>
        <Stat t={t} label="nodes" value={fx.graph.total_nodes ?? fx.graph.nodes.length}/>
        <Stat t={t} label="edges" value={fx.graph.total_edges ?? fx.graph.edges.length}/>
        <Stat t={t} label="symbolic hits" value={fx.finding ? 3 : 1}/>
        <Stat t={t} label="mode" value={mode}/>
        <Stat t={t} label="duration" value={mode==='council' ? '42.3s' : '6.1s'} mono/>
      </div>
    </div>
  );
}

function Stat({ t, label, value, mono }) {
  return (
    <div style={{ textAlign:'right', minWidth: 70 }}>
      <div style={{
        fontFamily: t.font.mono, fontSize: 9,
        color: t.fg2, letterSpacing: 1, textTransform:'uppercase',
      }}>{label}</div>
      <div style={{
        fontFamily: mono ? t.font.mono : t.font.ui,
        fontSize: 16, fontWeight: 600, color: t.fg0,
        fontVariantNumeric:'tabular-nums',
        marginTop: 2,
      }}>{value}</div>
    </div>
  );
}

function FindingCard({ t, finding, expanded, onToggle }) {
  return (
    <div style={{
      margin: 14,
      background: t.bg1,
      border: `1px solid ${t.line}`,
      borderRadius: t.radius.md,
      overflow:'hidden',
    }}>
      <button onClick={onToggle} style={{
        width:'100%', textAlign:'left',
        padding: '14px 16px',
        background:'transparent', border:'none', color: t.fg0,
        cursor:'pointer',
        display:'flex', alignItems:'flex-start', gap: 12,
      }}>
        <div style={{
          marginTop: 2,
          width: 22, height: 22, borderRadius: t.radius.sm,
          background: `color-mix(in oklch, ${t.semantic.red} 22%, ${t.bg1})`,
          border:`1px solid ${t.semantic.red}`,
          display:'flex', alignItems:'center', justifyContent:'center',
          flexShrink: 0,
        }}>
          <Icon name="warn" size={12} color={t.semantic.red}/>
        </div>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{
            fontFamily: t.font.ui, fontSize: 14, fontWeight: 600,
            color: t.fg0, marginBottom: 6,
          }}>{finding.title}</div>
          <div style={{ display:'flex', gap: 6, flexWrap:'wrap', marginBottom: 8 }}>
            <Chip t={t} tone="red" size="sm">{finding.severity}</Chip>
            <Chip t={t} tone="neutral" size="sm">{finding.category}</Chip>
            {finding.cwe ? <Chip t={t} tone="neutral" size="sm">{finding.cwe}</Chip> : null}
            <Chip t={t} tone="accent" size="sm">{finding.hops} hops</Chip>
          </div>
          <div style={{
            fontFamily: t.font.mono, fontSize: 10,
            color: t.fg2, letterSpacing: 0.5,
          }}>line {finding.line} · {finding.filepath || 'handler.py'}</div>
        </div>
        <Icon name={expanded ? 'chevU' : 'chevD'} size={16} color={t.fg2}/>
      </button>

      {expanded && (
        <div style={{ borderTop:`1px solid ${t.line}`, padding: '14px 16px' }}>
          {/* Explanation */}
          <Section t={t} label="explanation">
            <div style={{
              fontFamily: t.font.ui, fontSize: 13, lineHeight: 1.6, color: t.fg1,
            }}>{finding.explanation}</div>
          </Section>

          {/* Taint chain */}
          <Section t={t} label="causal path">
            <TaintChain t={t} finding={finding}/>
          </Section>

          {/* Fix */}
          <Section t={t} label="suggested fix">
            {finding.fix && finding.fix.before
              ? <CodeDiff t={t} before={finding.fix.before} after={finding.fix.after || ''}/>
              : <SuggestionBox t={t} text={(finding.fix && finding.fix.after) || finding.intervention_recommendation || ''}/>}
          </Section>

          {/* Signals */}
          <Section t={t} label="cognitive signals">
            <SignalsStrip t={t} signals={finding.signals}/>
          </Section>
        </div>
      )}
    </div>
  );
}

function Section({ t, label, children }) {
  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{
        fontFamily: t.font.mono, fontSize: 9,
        color: t.fg2, letterSpacing: 1, textTransform:'uppercase',
        marginBottom: 8,
      }}>{label}</div>
      {children}
    </div>
  );
}

function TaintChain({ t, finding }) {
  const fx = window.DAEV_FIXTURES[Object.keys(window.DAEV_FIXTURES).find(k => window.DAEV_FIXTURES[k].finding === finding)];
  const nodes = finding.path.map(id => fx.graph.nodes.find(n => n.id === id));
  return (
    <div style={{
      background: t.bg0,
      border:`1px solid ${t.line}`,
      borderRadius: t.radius.sm,
      padding: 10,
    }}>
      {nodes.map((n, i) => (
        <div key={n.id} style={{
          display:'flex', alignItems:'center', gap: 10,
          padding: '4px 0',
          opacity: 1,
        }}>
          <div style={{
            width: 20, textAlign:'center',
            fontFamily: t.font.mono, fontSize: 10, color: t.fg3,
          }}>{String(i+1).padStart(2,'0')}</div>
          <div style={{
            width: 20, display:'flex', justifyContent:'center',
            color: t.semantic.red,
          }}>
            <Icon name={ENTITY_STYLE[n.kind].glyph} size={13} color={t.semantic.red}/>
          </div>
          <div style={{ flex:1, minWidth: 0 }}>
            <div style={{
              fontFamily: t.font.mono, fontSize: 12,
              color: t.fg0,
              whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis',
            }}>{n.label}</div>
            <div style={{
              fontFamily: t.font.mono, fontSize: 9.5,
              color: t.fg2, letterSpacing: 0.5,
              textTransform:'uppercase', marginTop: 1,
            }}>{ENTITY_STYLE[n.kind].label} · L{n.line} · {n.role}</div>
          </div>
          {i < nodes.length-1 && (
            <div style={{ color: t.semantic.red, opacity: 0.6, marginRight: 4 }}>↓</div>
          )}
        </div>
      ))}
    </div>
  );
}

// Slice 6.4.5.2 Phase 8 (BUG-80): SuggestionBox fallback for findings whose
// before-code is empty. Wired audits never carry a before/after pair from
// the LLM (only `intervention_recommendation` text), so the diff frame would
// otherwise render a blank "–" row above the recommendation.
function SuggestionBox({ t, text }) {
  const success = (t.semantic && t.semantic.green) || '#10b981';
  return (
    <div style={{
      border: `1px solid ${success}`,
      borderLeft: `4px solid ${success}`,
      padding: '12px 16px',
      borderRadius: t.radius?.sm || 4,
      background: `color-mix(in oklch, ${success} 6%, ${t.bg0})`,
      fontFamily: t.font.mono,
      fontSize: 12,
      color: t.fg0,
      whiteSpace: 'pre-wrap',
    }}>
      {text || 'No remediation suggestion available.'}
    </div>
  );
}

function CodeDiff({ t, before, after }) {
  return (
    <div style={{
      border:`1px solid ${t.line}`, borderRadius: t.radius.sm,
      overflow:'hidden',
      fontFamily: t.font.mono, fontSize: 11.5,
    }}>
      {before.split('\n').map((ln,i) => (
        <div key={'b'+i} style={{
          padding:'3px 10px',
          background: `color-mix(in oklch, ${t.semantic.red} 10%, ${t.bg0})`,
          color: t.semantic.red,
          display:'flex', gap: 10,
        }}>
          <span style={{ color: t.semantic.red, opacity: 0.6, width: 10 }}>–</span>
          <span style={{ whiteSpace:'pre', color: t.fg0 }}>{ln}</span>
        </div>
      ))}
      {after.split('\n').map((ln,i) => (
        <div key={'a'+i} style={{
          padding:'3px 10px',
          background: `color-mix(in oklch, ${t.semantic.green} 10%, ${t.bg0})`,
          color: t.semantic.green,
          display:'flex', gap: 10,
        }}>
          <span style={{ color: t.semantic.green, opacity: 0.6, width: 10 }}>+</span>
          <span style={{ whiteSpace:'pre', color: t.fg0 }}>{ln}</span>
        </div>
      ))}
    </div>
  );
}

// Slice 6.4.5.3 Phase 2 (Items 2, 3): small amber badge that signals "available
// in Council Mode (Q3 2026)". Placed near Q3-only surfaces (cognitive signals,
// the deep-audit / Council deep-debate callout, the SAST signal-reduction copy)
// to keep the demo's framing honest — the gate ships today; the council ships Q3.
function Q3Badge({ t, tooltip, label = 'Q3 · Council Mode' }) {
  const amber = (t && t.semantic && t.semantic.amber) || '#f59e0b';
  return (
    <span
      title={tooltip || 'Available Q3 2026 — Council Mode'}
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        padding: '2px 8px',
        background: `color-mix(in oklch, ${amber} 14%, transparent)`,
        color: amber,
        border: `1px solid ${amber}`,
        borderRadius: 3,
        fontFamily: (t && t.font && t.font.mono) || 'monospace',
        fontSize: 9.5,
        fontWeight: 700,
        letterSpacing: 0.6,
        textTransform: 'uppercase',
        cursor: 'help',
        whiteSpace: 'nowrap',
      }}
    >
      {label}
    </span>
  );
}

function SignalsStrip({ t, signals }) {
  // Real backend cognitive signals can return null when the source is too short
  // to compute Miller/Zipf — guard with safe defaults to avoid TypeError on
  // .toFixed() and to keep the strip rendering clean even on degenerate inputs.
  const miller = (typeof signals?.miller === 'number') ? signals.miller : 0;
  const zipf   = (typeof signals?.zipf   === 'number') ? signals.zipf   : 0;
  const system = signals?.system || 'S1';
  const items = [
    { label: 'Miller', value: miller.toFixed(1), sub: 'working-memory load', good: miller < 4 },
    { label: 'Zipf β', value: zipf.toFixed(2),   sub: 'naturalness slope', good: Math.abs(zipf + 1) < 0.3 },
    { label: system,   value: system === 'S2' ? 'deliberate' : 'reflex', sub: 'reasoning mode', good: true },
  ];
  return (
    <div style={{ display:'flex', flexDirection:'column', gap: 8 }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'flex-end' }}>
        <Q3Badge t={t}
          tooltip="Council Mode (Q3 2026) enriches these signals with multi-agent debate, sycophancy correction, and devil's advocate cross-examination."
          label="Council Mode · Q3"/>
      </div>
      <div style={{ display:'flex', gap: 8 }}>
      {items.map((it,i) => (
        <div key={i} style={{
          flex:1,
          padding:'8px 10px',
          background: t.bg0,
          border: `1px solid ${t.line}`,
          borderRadius: t.radius.sm,
        }}>
          <div style={{
            display:'flex', alignItems:'center', gap: 6,
            fontFamily: t.font.mono, fontSize: 9, color: t.fg2,
            letterSpacing: 1, textTransform:'uppercase',
          }}>
            <span style={{
              width: 5, height: 5, borderRadius: '50%',
              background: it.good ? t.semantic.green : t.semantic.amber,
            }}/>
            {it.label}
          </div>
          <div style={{
            fontFamily: t.font.mono, fontSize: 14, fontWeight: 600,
            color: t.fg0, marginTop: 4, fontVariantNumeric:'tabular-nums',
          }}>{it.value}</div>
          <div style={{
            fontFamily: t.font.mono, fontSize: 9, color: t.fg3,
            marginTop: 2, letterSpacing: 0.3,
          }}>{it.sub}</div>
        </div>
      ))}
      </div>
    </div>
  );
}

function EmptyFindings({ t, fx }) {
  return (
    <div style={{ padding: 28, textAlign:'center' }}>
      <div style={{
        width: 44, height: 44, borderRadius:'50%',
        background: `color-mix(in oklch, ${t.semantic.green} 18%, ${t.bg0})`,
        border:`1px solid ${t.semantic.green}`,
        display:'inline-flex', alignItems:'center', justifyContent:'center',
        marginBottom: 14,
      }}>
        <Icon name="check" size={22} color={t.semantic.green}/>
      </div>
      <div style={{
        fontFamily: t.font.ui, fontSize: 15, fontWeight: 600, color: t.fg0,
      }}>No causal taint path reaches a sink.</div>
      <div style={{
        marginTop: 8,
        fontFamily: t.font.ui, fontSize: 13, color: t.fg1, lineHeight: 1.5,
        maxWidth: 320, margin: '8px auto 0',
      }}>
        Input was bound, validated by a sanitizer, and passed as a parameterized query.
        Counterfactual verification confirmed sanitizer dominates the sink.
      </div>
    </div>
  );
}

function MetaPanel({ t, fx, mode, verdict }) {
  const aid = fx.audit_id || `a_0x9f2e.${fx.id}`;
  const rows = [
    ['audit_id', aid],
    ['started', '2026-04-20T14:22:08Z'],
    ['duration_ms', mode==='council' ? '42,318' : '6,142'],
    ['mode', mode],
    ['language', fx.lang],
    ['verdict', verdict],
    ['graph.nodes', fx.graph.nodes.length],
    ['graph.edges', fx.graph.edges.length],
    ['symbolic.hits', fx.finding ? 3 : 1],
    ['counterfactual.runs', fx.finding ? 4 : 2],
    ['severity.breakdown', fx.finding ? '{critical:1, high:0, medium:0}' : '{}'],
  ];
  return (
    <div style={{ padding: 14 }}>
      <div style={{
        fontFamily: t.font.mono, fontSize: 11.5, lineHeight: 1.9,
        background: t.bg1, border:`1px solid ${t.line}`, borderRadius: t.radius.sm,
        padding: '10px 14px',
      }}>
        {rows.map(([k,v],i) => (
          <div key={i} style={{
            display:'flex', justifyContent:'space-between', gap: 20,
            borderBottom: i < rows.length-1 ? `1px dashed ${t.line}` : 'none',
            paddingBottom: i < rows.length-1 ? 4 : 0,
          }}>
            <span style={{ color: t.fg2 }}>{k}</span>
            <span style={{ color: t.fg0 }}>{v}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

function ExportPanel({ t, fx, mode }) {
  const [toast, setToast] = Sb(null);
  const items = [
    { icon:'download', label:'Download PDF report', sub:'formatted for pilot handoff', euAct: true },
    { icon:'copy',     label:'Copy audit_id',        sub: fx.audit_id || `a_0x9f2e.${fx.id}` },
    { icon:'share',    label:'Shareable link',       sub:`demo.aelethion.com/a/${(fx.audit_id || '0x9f2e.'+fx.id).replace('a_','')}` },
    { icon:'hash',     label:'Full JSON response',    sub:'includes graph + finding + signals' },
  ];
  const onClick = (it) => {
    setToast({ label: it.label });
    try { window.DAEV_TRACK && window.DAEV_TRACK('export_clicked', { item: it.label }); } catch(_){}
    // 8s in demo mode — committee members read the room before looking back.
    // 4s was too short for screen-share contexts.
    setTimeout(() => setToast(null), 8000);
  };
  return (
    <div style={{ padding: 14, display:'flex', flexDirection:'column', gap: 8, position:'relative' }}>
      {items.map((it,i) => (
        <button key={i} onClick={() => onClick(it)}
          data-tour={it.euAct ? 'export-pdf' : undefined}
          style={{
          textAlign:'left', display:'flex', alignItems:'center', gap: 12,
          padding:'12px 14px',
          background: t.bg1, border:`1px solid ${t.line}`,
          borderRadius: t.radius.sm,
          color: t.fg0, cursor:'pointer',
          fontFamily: t.font.ui,
        }}>
          <Icon name={it.icon} size={16} color={t.fg1}/>
          <div style={{ flex:1 }}>
            <div style={{ fontSize: 13, fontWeight: 500, display:'inline-flex', alignItems:'center', gap: 8 }}>
              {it.label}
              {it.euAct && (
                <span style={{
                  fontFamily: t.font.mono, fontSize: 9, fontWeight: 700,
                  color: t.accent.base, letterSpacing: 1.2, textTransform:'uppercase',
                  padding:'1px 7px',
                  border:`1px solid ${t.accent.base}`,
                  borderRadius: t.radius.sm,
                }}>EU AI Act ready</span>
              )}
            </div>
            <div style={{
              fontFamily: t.font.mono, fontSize: 10.5, color: t.fg2, marginTop: 2,
            }}>{it.sub}</div>
          </div>
          <Icon name="chevR" size={14} color={t.fg3}/>
        </button>
      ))}

      {/* Q3 waitlist toast — converts a dead button into a conversion moment */}
      {toast && (
        <div style={{
          marginTop: 6,
          padding:'12px 14px',
          background: `color-mix(in oklch, ${t.accent.base} 10%, ${t.bg1})`,
          border: `1px solid ${t.accent.base}`,
          borderRadius: t.radius.md,
          fontFamily: t.font.ui, fontSize: 13,
          color: t.fg0,
          display:'flex', alignItems:'center', gap: 12,
        }}>
          <span style={{ flex:1 }}>
            <b>{toast.label}</b> ships Q3.
            <span style={{ color: t.fg2, marginLeft: 4 }}>
              Join the waitlist to get the production export pipeline first.
            </span>
          </span>
          <a href="/" onClick={() => { try { window.DAEV_TRACK && window.DAEV_TRACK('back_to_site_clicked', { from: 'export_toast' }); } catch(_){} }}
            style={{
              fontFamily: t.font.mono, fontSize: 11, fontWeight: 700,
              color: t.accent.base, textDecoration:'none',
              padding:'6px 10px', border:`1px solid ${t.accent.base}`,
              borderRadius: t.radius.sm,
              letterSpacing: 0.4,
            }}>Join waitlist →</a>
        </div>
      )}

      {/* AEGIS PREVIEW — causal-identity provenance stamp (Coming Q3 2026) */}
      <div data-tour="aegis-stamp">
      <div style={{ height: 1, background: t.line, margin: '16px 0' }}/>
      <div style={{ display:'flex', alignItems:'center', gap: 8 }}>
        <span style={{
          fontFamily: t.font.mono, fontSize: 10,
          color: t.fg3, letterSpacing: 1.2, textTransform:'uppercase',
          flex: 1,
        }}>// AEGIS — CAUSAL IDENTITY</span>
        <Chip t={t} tone="neutral" size="sm">COMING Q3 2026</Chip>
      </div>
      <div style={{
        border: `1px dashed ${t.line2}`,
        borderRadius: t.radius.md,
        padding: '12px 14px',
        marginTop: 8,
        background: `color-mix(in oklch, ${t.bg2} 80%, ${t.bg1})`,
        opacity: 0.75,
      }}>
        {[
          { k: 'AGENT_ID',    v: 'daev-auditor@v0.9.1' },
          { k: 'AUTHOR_HASH', v: 'sha256:4f2a...c891' },
          { k: 'CHAIN_SIG',   v: '0x7f3b...a2d4' },
          { k: 'VERIFIED_BY', v: 'aelethion-ca.eu · Frankfurt HSM' },
        ].map((row) => (
          <div key={row.k} style={{
            fontFamily: t.font.mono, fontSize: 11, lineHeight: 1.8,
            display:'flex', gap: 14,
          }}>
            <span style={{ color: t.fg2, minWidth: 110, letterSpacing: 0.4 }}>{row.k}</span>
            <span style={{ color: t.fg1 }}>{row.v}</span>
          </div>
        ))}
      </div>
      <div style={{
        fontFamily: t.font.mono, fontSize: 10,
        color: t.fg3, marginTop: 6, letterSpacing: 0.4,
      }}>Every action. Verified origin. Accountable agent.</div>
      </div>
    </div>
  );
}

// Copy-link button — produces a deep-link URL to this exact audit state
// (fixture + mode + result screen) so visitors can share what they're looking at.
function CopyLinkButton({ t, fixtureId, mode }) {
  const [copied, setCopied] = Sb(false);
  const onCopy = () => {
    if (typeof window === 'undefined') return;
    const base = window.location.origin + window.location.pathname;
    const url  = `${base}?screen=result&fixture=${encodeURIComponent(fixtureId)}&mode=${encodeURIComponent(mode || 'fast')}`;
    const finish = () => {
      setCopied(true);
      setTimeout(() => setCopied(false), 1400);
      try { window.DAEV_TRACK && window.DAEV_TRACK('copy_link', { fixture: fixtureId, mode }); } catch (_) {}
    };
    try {
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(url).then(finish, finish);
      } else {
        // Fallback for older browsers / file:// origins
        const el = document.createElement('textarea');
        el.value = url; document.body.appendChild(el);
        el.select(); try { document.execCommand('copy'); } catch (_) {}
        document.body.removeChild(el); finish();
      }
    } catch (_) { finish(); }
  };
  return (
    <Button t={t} size="sm" variant={copied ? 'primary' : 'ghost'}
      icon={<Icon name={copied ? 'check' : 'share'} size={12}/>}
      onClick={onCopy}
      title={copied ? 'Link copied to clipboard' : 'Copy a link to this exact audit'}>
      {copied ? 'Copied' : 'Copy link'}
    </Button>
  );
}

Object.assign(window, { LiveBuild, ResultDashboard, NodeTooltip, CopyLinkButton });
