// ════════════════════════════════════════════════════════════════════
// NexFlow · Service-demo modal + six animated mini-products
// ────────────────────────────────────────────────────────────────────
// Modal hosts six animated demos, each driven by a narration script.
// The narrator emits `{index, focus, text}` events as each sentence is
// spoken; demos subscribe and highlight the element being talked about
// (focused node glows, others dim). Clicking a focusable element jumps
// the narration to the segment that describes it. Prev/Next controls
// step through the script. Audio + speech engine lives in
// engine/demo-audio.js — `window.NF_DemoAudio`.
// Entry point: `window.NF_ServiceDemos.openDemo(serviceId)`.
// ════════════════════════════════════════════════════════════════════
const { useState: useStateD, useEffect: useEffectD, useRef: useRefD, useMemo: useMemoD } = React;
const AUD = () => window.NF_DemoAudio;

// ════════════════════════════════════════════════════════════════════
// N8N-AUTHENTIC SVG ICON SET · used inside workflow nodes
// ════════════════════════════════════════════════════════════════════
// Each icon is authored on a 24×24 viewBox and renders in white. They
// embed inside the outer SVG canvas as nested <g>s; the surrounding
// node draws the brand-coloured pill behind them.
// ────────────────────────────────────────────────────────────────────
function N8nIcon({ name }) {
  // children rendered at 24x24, scaled by the parent g
  switch (name) {
    case 'webhook': return (
      <g fill="#fff">
        <path d="M13 2.5 L4.5 14 h5.7 l-1.6 7.5 L18 10 h-5.5 z"/>
      </g>
    );
    case 'openai': return (
      <g fill="#fff">
        <path d="M12 2.4 L13.5 9.4 L20.6 11 L13.5 12.6 L12 19.6 L10.5 12.6 L3.4 11 L10.5 9.4 Z"/>
        <path d="M18.4 3.2 L19 5.4 L21.2 5.9 L19 6.5 L18.4 8.6 L17.8 6.5 L15.6 5.9 L17.8 5.4 Z"/>
      </g>
    );
    case 'globe': return (
      <g fill="none" stroke="#fff" strokeWidth="1.6" strokeLinecap="round">
        <circle cx="12" cy="12" r="8.5"/>
        <ellipse cx="12" cy="12" rx="8.5" ry="3.5"/>
        <path d="M12 3.5 c-3.5 3 -3.5 14 0 17 M12 3.5 c3.5 3 3.5 14 0 17"/>
      </g>
    );
    case 'crm': return (
      <g>
        <circle cx="9" cy="8" r="3.6" fill="#fff"/>
        <path d="M2.5 21 c0 -5.5 12.5 -5.5 12.5 0 z" fill="#fff"/>
        <path d="M19 7.5 v6 M16 10.5 h6" stroke="#fff" strokeWidth="2" strokeLinecap="round"/>
      </g>
    );
    case 'calendar': return (
      <g>
        <rect x="3" y="5" width="18" height="16" rx="2" fill="none" stroke="#fff" strokeWidth="1.6"/>
        <path d="M3 10 h18" stroke="#fff" strokeWidth="1.6"/>
        <path d="M8 3 v4 M16 3 v4" stroke="#fff" strokeWidth="1.8" strokeLinecap="round"/>
        <rect x="13" y="13" width="4.4" height="4.4" rx="1" fill="#fff"/>
      </g>
    );
    case 'sms': return (
      <g>
        <path d="M3.2 6 a2 2 0 0 1 2 -2 h13.6 a2 2 0 0 1 2 2 v8.6 a2 2 0 0 1 -2 2 h-7 l-4 3.4 v-3.4 h-2.6 a2 2 0 0 1 -2 -2 z" fill="#fff"/>
        <circle cx="9" cy="10" r="1" fill="#1A1D26"/>
        <circle cx="12" cy="10" r="1" fill="#1A1D26"/>
        <circle cx="15" cy="10" r="1" fill="#1A1D26"/>
      </g>
    );
    default: return null;
  }
}

// ════════════════════════════════════════════════════════════════════
// DEMO REGISTRY · metadata + narration scripts with focus targets
// ════════════════════════════════════════════════════════════════════
// Each narration segment carries an optional `focus` that the demo
// uses to highlight the element being described. The narrator emits
// the focus to subscribers on segment change so the visual follows the
// voice without polling.
// ────────────────────────────────────────────────────────────────────
const DEMO_META = {
  n8n: {
    title: 'Real-estate pipeline · live run',
    url: 'workflows / real-estate-pipeline.json',
    badge: 'n8n · live',
    stats: [['11.2s','avg run'],['$0.04','per lead'],['99.94%','success · 30d']],
    // Per-node payload snippets shown in the side-rail when the node is active.
    // Kept short (3–6 lines) so they read at glance — these mirror the kind
    // of data finance / ops teams see when they audit a real n8n execution.
    payloads: [
      { lang: 'json', label: 'POST /webhook/rea-leads', body: '{\n  "source": "realestate.com.au",\n  "listing": "3br-bondi-1.8M",\n  "buyer": "sophie.k@gmail.com",\n  "msg": "Saturday inspection?"\n}' },
      { lang: 'json', label: 'openai · score', body: '{\n  "score": 84,\n  "intent": "hot · inspection",\n  "reasons": ["finance pre-approved",\n    "specific listing", "timing this week"]\n}' },
      { lang: 'json', label: 'corelogic · enrich', body: '{\n  "comparables": 12,\n  "within_1km": 4,\n  "median": "$1.74M",\n  "trend_90d": "+2.1%"\n}' },
      { lang: 'json', label: 'ghl · upsert', body: '{\n  "contact_id": "ghl_8q42",\n  "pipeline": "hot_buyers",\n  "owner": "noor",\n  "tags": ["sat-inspection"]\n}' },
      { lang: 'json', label: 'cal.com · hold', body: '{\n  "slot": "2026-05-26T10:00:00+10:00",\n  "agent": "Noor",\n  "hold_ttl_min": 45,\n  "link": "cal.com/m/r-bondi-tue"\n}' },
      { lang: 'text', label: 'twilio · sms sent', body: 'To: +61 4** *** ***\nFrom: NexFlow Realty\n\nHi Sophie — confirmed for\nTue 10:00 AEST at 12 Smith St.\nReply Y to confirm, R to reschedule.' },
    ],
    narration: [
      { text: 'A new enquiry lands from realestate dot com. The trigger fires.', focus: { node: 0 } },
      { text: 'OpenAI reads the message and scores the lead — eighty-four out of one hundred. Serious buyer.', focus: { node: 1 } },
      { text: 'CoreLogic enriches the lead with twelve comparable properties within one kilometre.', focus: { node: 2 } },
      { text: 'GoHighLevel creates the contact and pushes it into the hot pipeline.', focus: { node: 3 } },
      { text: 'A viewing is auto-booked into the agent\u2019s calendar for Tuesday at ten.', focus: { node: 4 } },
      { text: 'Twilio sends the SMS confirmation. End to end, eleven seconds. Nobody touched a keyboard.', focus: { node: 5 } },
    ],
  },
  agents: {
    title: 'Support triage agent · live queue',
    url: 'agents / support-triage.yaml',
    badge: 'agentic · live',
    stats: [['1.8s','median decision'],['86%','auto-resolved'],['100%','logged']],
    // Per-worker classifier output — mirrors what the actual agent emits
    // into the audit log. Drives the "current ticket" detail card.
    classifications: {
      classify: { intent: 'refund_request',  confidence: 0.94, route: 'auto', model: 'claude-sonnet-4-5' },
      enrich:   { docs_pulled: 3,            kb_match: 'returns_policy_v3', cache_hit: true, model: 'rag·local' },
      draft:    { tone: 'apologetic_clear',  tokens_in: 412, tokens_out: 187, model: 'gpt-4o-2024-11-20' },
      dispatch: { channel: 'email',          sla_ms: 1840, cost_usd: 0.0021, status: 'sent' },
    },
    narration: [
      { text: 'Support tickets arrive faster than any human can read them.', focus: { panel: 'queue' } },
      { text: 'The agent classifies each ticket — what it is, who owns it.', focus: { worker: 'classify' } },
      { text: 'It enriches from your knowledge base — past tickets, docs, prices.', focus: { worker: 'enrich' } },
      { text: 'It drafts the right reply in your tone of voice.', focus: { worker: 'draft' } },
      { text: 'It dispatches the response back to the customer.', focus: { worker: 'dispatch' } },
      { text: 'Eighty-six percent resolve without a human ever touching them.', focus: { panel: 'done' } },
    ],
  },
  crm: {
    title: 'GoHighLevel pipeline · this week',
    url: 'crm / leads-pipeline',
    badge: 'crm · live',
    stats: [['+22%','close-rate'],['8.4h','/ wk reclaimed'],['11.2s','enrichment']],
    narration: [
      { text: 'Inbound leads land in the new column the moment they arrive.',     focus: { col: 0 } },
      { text: 'The AI scores them and enriches them with public data.',           focus: { col: 1 } },
      { text: 'Viewings are booked automatically into the agent\u2019s calendar.', focus: { col: 2 } },
      { text: 'Closed deals move to won. Twenty-two percent lift in close-rate.', focus: { col: 3 } },
      { text: 'Eight hours a week reclaimed from manual data entry.',             focus: { col: null } },
    ],
  },
  chat: {
    title: 'Nex concierge · grounded chat',
    url: 'chat / nex.session',
    badge: 'chatbot · live',
    stats: [['~8s','first reply'],['41%','book-rate'],['12','knowledge bases']],
    // RAG source chips shown under each bot reply — the visible audit trail
    // for what the chatbot was actually grounded in. Indexed by message #
    // (only bot messages have sources).
    sources: [
      ['pricing.md · §2',  'system-prompt.md'],
      ['case-studies/real-estate.md · §4', 'pricing.md · spark'],
      ['pricing.md · plans', 'pricing.md · spark', 'pricing.md · flow'],
      ['booking-flow.md', 'calendars/noor.json'],
    ],
    narration: [
      { text: 'Nex is grounded in your pricing, your docs, your case studies.', focus: { panel: 'session' } },
      { text: 'It answers honest questions with honest numbers.',               focus: { panel: 'chat' } },
      { text: 'When the visitor is ready, it books a real call into your real calendar.', focus: { panel: 'book' } },
      { text: 'If intent is high it hands off to a human in Slack.',            focus: { panel: 'handoff' } },
      { text: 'Forty-one percent of conversations end in a booked call.',       focus: { panel: 'book' } },
    ],
  },
  doc: {
    title: 'Invoice OCR → Xero · INV-2847',
    url: 'document-ai / invoice-2847.pdf',
    badge: 'ocr · live',
    stats: [['99.4%','match accuracy'],['8h','/ mo saved'],['100%','audit-logged']],
    narration: [
      { text: 'An invoice hits the inbox. The agent reads it line by line.', focus: { field: 'header' } },
      { text: 'It extracts the invoice number, the vendor, and the date.',   focus: { field: 'header-fields' } },
      { text: 'Subtotal, tax, and total — all matched to ninety-nine percent confidence.', focus: { field: 'totals' } },
      { text: 'The bill is posted to Xero before you\u2019ve finished your coffee.',         focus: { field: 'posted' } },
      { text: 'Under five hundred dollars auto-approves. Above goes to finance.',         focus: { field: 'approval' } },
    ],
  },
  report: {
    title: 'Operations digest · Monday 07:00',
    url: 'reports / weekly-ops.live',
    badge: 'reporting · live',
    stats: [['7:00','daily delivery'],['30+','sources'],['~5 KPIs','per pulse']],
    narration: [
      { text: 'Every Monday at seven, the digest lands in your inbox.', focus: { panel: 'kpis' } },
      { text: 'Revenue, orders, leads, NPS — pulled live from Stripe, Xero, your CRM.', focus: { panel: 'kpis' } },
      { text: 'The bar chart shows daily orders for the last fortnight.', focus: { panel: 'bars' } },
      { text: 'The gauge shows how much of your ops is automated.',       focus: { panel: 'gauge' } },
      { text: 'Anomalies trip alerts before your board sees them.',       focus: { panel: 'kpis' } },
    ],
  },
};

// ════════════════════════════════════════════════════════════════════
// DemoModal wrapper — chrome, captions, controls, focus relay
// ════════════════════════════════════════════════════════════════════
function DemoModal({ serviceId, onClose, opts = {} }) {
  const meta = DEMO_META[serviceId] || DEMO_META.n8n;
  const data = window.NF_DATA;
  const svc  = (data && data.SERVICES.find(s => s.id === serviceId)) || null;

  const [caption, setCaption] = useStateD('');
  const [segment, setSegment] = useStateD(null);
  const [muted,   setMuted]   = useStateD(() => AUD()?.isMuted?.() || false);
  const frameRef              = useRefD(null);

  // Audio + narration lifecycle — robust against rapid demo switching.
  useEffectD(() => {
    const A = AUD();
    if (!A) return;
    A.onCaption(setCaption);
    A.onSegment(setSegment);
    A.startPad();
    // Defer 240ms so voice list resolves and any pending cancel() drains
    const t = setTimeout(() => {
      A.speak(meta.narration || [], { loop: true });
    }, 240);
    return () => {
      clearTimeout(t);
      A.stopSpeech();
      A.onCaption(null);
      A.onSegment(null);
      A.stopPad();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [serviceId]);

  // ESC + body-scroll lock + Tab focus-trap.
  // ESC / Arrow keys live on window so they fire regardless of which
  // child has focus. The Tab handler lives on the modal frame so it
  // only intercepts focus moves inside the dialog.
  useEffectD(() => {
    function onKey(e) {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowRight') AUD()?.next?.();
      if (e.key === 'ArrowLeft')  AUD()?.prev?.();
    }
    window.addEventListener('keydown', onKey);
    document.body.style.overflow = 'hidden';

    const frame = frameRef.current;
    let cleanupFrameKey = null;
    if (frame) {
      // Pull initial focus into the dialog so screen readers + keyboard
      // users land inside it. The Close button is the safest target.
      const closeBtn = frame.querySelector('.close');
      if (closeBtn) requestAnimationFrame(() => closeBtn.focus());

      function onFrameKey(e) {
        if (e.key !== 'Tab') return;
        const fs = Array.from(frame.querySelectorAll(
          'button:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])'
        )).filter(el => el.offsetParent !== null);
        if (!fs.length) return;
        const first = fs[0], last = fs[fs.length - 1];
        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault(); last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault(); first.focus();
        }
      }
      frame.addEventListener('keydown', onFrameKey);
      cleanupFrameKey = () => frame.removeEventListener('keydown', onFrameKey);
    }

    return () => {
      window.removeEventListener('keydown', onKey);
      document.body.style.overflow = '';
      if (cleanupFrameKey) cleanupFrameKey();
    };
  }, [onClose]);

  function toggleMute() {
    const next = !muted; setMuted(next); AUD()?.setMuted(next);
    if (!next) AUD()?.speak(meta.narration || [], { loop: true });
  }

  const focus = segment?.focus || null;
  const totalSegs = (meta.narration || []).length;
  const segIndex = segment?.index ?? -1;

  const Demo =
    serviceId === 'agents' ? DemoAgents :
    serviceId === 'crm'    ? DemoCRM    :
    serviceId === 'chat'   ? DemoChat   :
    serviceId === 'doc'    ? DemoDoc    :
    serviceId === 'report' ? DemoReport :
    DemoWorkflow;

  // Demos request a specific narration index when the user clicks a node
  function jumpTo(i) { AUD()?.jumpTo(i); }

  return (
    <div className="demo-backdrop open" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div ref={frameRef} className="demo-frame" role="dialog" aria-modal="true" aria-labelledby="demo-title">
        <header className="demo-chrome">
          <div className="lights"><i/><i/><i/></div>
          <div className="url"><span className="live" aria-hidden="true"/><span>{meta.url}</span></div>
          <span className="badge">{meta.badge}</span>
          <button className={'mute ' + (muted ? 'on' : '')} onClick={toggleMute}
            aria-label={muted ? 'Unmute narration and music' : 'Mute narration and music'}
            title={muted ? 'Sound off — click to enable narration' : 'Sound on — click to mute'}>
            {muted ? (
              <svg viewBox="0 0 20 20" width="14" height="14"><path fill="currentColor" d="M3.5 7.5h2.5L9.5 4.5v11l-3.5-3H3.5v-5z"/><path stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" d="M13 8l4 4M17 8l-4 4"/></svg>
            ) : (
              <svg viewBox="0 0 20 20" width="14" height="14"><path fill="currentColor" d="M3.5 7.5h2.5L9.5 4.5v11l-3.5-3H3.5v-5z"/><path stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" fill="none" d="M12.5 7c1 .8 1.6 1.8 1.6 3s-.6 2.2-1.6 3M14.5 5c1.8 1.3 2.8 3 2.8 5s-1 3.7-2.8 5"/></svg>
            )}
          </button>
          <button className="close" onClick={onClose} aria-label="Close demo">×</button>
        </header>
        <div className="demo-body">
          <Demo meta={meta} svc={svc} focus={focus} segIndex={segIndex} jumpTo={jumpTo} key={serviceId}/>
          <div className={'demo-caption ' + (caption ? 'in' : '')} aria-live="polite">
            <button className="step prev" aria-label="Previous step"
              onClick={() => AUD()?.prev()}
              disabled={segIndex <= 0}>‹</button>
            <span className="ico" aria-hidden="true">
              <svg viewBox="0 0 14 14" width="11" height="11"><circle cx="7" cy="7" r="6" fill="none" stroke="currentColor" strokeWidth="1.2"/><path stroke="currentColor" strokeWidth="1.2" fill="none" strokeLinecap="round" d="M4.6 5.6c0-1.3 1-2.4 2.4-2.4M9.4 8.4c0 1.3-1 2.4-2.4 2.4"/></svg>
            </span>
            <span className="text">{caption}</span>
            <span className="ix">{segIndex + 1} / {totalSegs}</span>
            <button className="step next" aria-label="Next step"
              onClick={() => AUD()?.next()}
              disabled={segIndex >= totalSegs - 1}>›</button>
          </div>
        </div>
        <footer className="demo-foot">
          <div className="stats">
            {meta.stats.map(([v, k]) => (
              <div className="s" key={k}><div className="v">{v}</div><div className="k">{k}</div></div>
            ))}
          </div>
          <div className="demo-foot-actions">
            {opts.back && (
              <button className="demo-back-btn" type="button" onClick={onClose} aria-label="Close demo and return to service page">
                ← Back
              </button>
            )}
            <a href="#contact" className="cta" onClick={onClose}>
              Book a 15-min map <span className="arr" aria-hidden="true">→</span>
            </a>
          </div>
        </footer>
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════
// DEMO 1 · n8n workflow · narration-driven node focus
// ════════════════════════════════════════════════════════════════════
const N8N_PALETTE = {
  webhook: '#F8884B', openai: '#10A37F', http: '#127C92',
  crm: '#0E7C63', cal: '#1E5BC8', twilio: '#F22F46',
};
function DemoWorkflow({ meta, focus, jumpTo }) {
  const W = 196, H = 76;
  const NODES = [
    { x:   8, y:  28, name: 'REA.com',     op: 'webhook · POST',    icon: 'webhook',  color: N8N_PALETTE.webhook },
    { x: 232, y:  28, name: 'OpenAI',      op: 'score lead · 4o',   icon: 'openai',   color: N8N_PALETTE.openai },
    { x: 456, y:  28, name: 'CoreLogic',   op: 'enrich · http GET', icon: 'globe',    color: N8N_PALETTE.http },
    { x: 456, y: 156, name: 'GoHighLevel', op: 'upsert contact',    icon: 'crm',      color: N8N_PALETTE.crm },
    { x: 232, y: 156, name: 'Cal.com',     op: 'book viewing',      icon: 'calendar', color: N8N_PALETTE.cal },
    { x:   8, y: 156, name: 'Twilio',      op: 'send sms',          icon: 'sms',      color: N8N_PALETTE.twilio },
  ];
  const EDGES = [[0,1], [1,2], [2,3], [3,4], [4,5]];
  const LOG = [
    ['00:00.0', 'trigger · sophie.k@gmail.com · "3br Bondi · $1.8M"',     'now'],
    ['00:01.2', 'openai · score 84/100 · serious buyer',                  'ok'],
    ['00:02.7', 'corelogic · 12 comparables · 4 within 1km',              'ok'],
    ['00:05.4', 'ghl · contact "Sophie K." created · hot pipeline',       'ok'],
    ['00:07.9', 'cal.com · slot held · Tue 10:00 AEST',                   'ok'],
    ['00:11.2', 'twilio · sms delivered · +61 4** ***',                   'ok'],
  ];

  // Active node = the one the narrator is currently describing.
  // All earlier nodes are "done"; later nodes are dim.
  const activeNode = (focus && typeof focus.node === 'number') ? focus.node : 0;

  // Animate the packet moving along the edge into the active node
  // whenever the focus changes. Source is the previous node.
  const packetRef = useRefD(null);
  const prevNodeRef = useRefD(-1);
  useEffectD(() => {
    if (activeNode <= 0 || !packetRef.current) {
      if (packetRef.current) packetRef.current.style.visibility = 'hidden';
      prevNodeRef.current = activeNode;
      return;
    }
    const ai = activeNode - 1, bi = activeNode;
    const pa = NODES[ai], pb = NODES[bi];
    if (!pa || !pb) return;
    const tempPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    tempPath.setAttribute('d', edgePath(pa, pb));
    const len = tempPath.getTotalLength();
    let raf, start = performance.now();
    packetRef.current.style.visibility = 'visible';
    function frame(now) {
      const k = Math.min(1, (now - start) / 1100);
      const pt = tempPath.getPointAtLength(k * len);
      if (packetRef.current) packetRef.current.setAttribute('transform', `translate(${pt.x},${pt.y})`);
      if (k < 1) raf = requestAnimationFrame(frame);
      else if (packetRef.current) setTimeout(() => { if (packetRef.current) packetRef.current.style.visibility = 'hidden'; }, 400);
    }
    raf = requestAnimationFrame(frame);
    prevNodeRef.current = activeNode;
    return () => cancelAnimationFrame(raf);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [activeNode]);

  function edgePath(a, b) {
    const sx = a.x + W, sy = a.y + H / 2;
    const ex = b.x,     ey = b.y + H / 2;
    // Vertical wrap (same column, different row) — swing right and back
    if (a.x === b.x) {
      const cpx = sx + 72;
      return `M${sx},${sy} C${cpx},${sy} ${cpx},${ey} ${ex + W},${ey}`;
    }
    const dx = ex - sx;
    if (dx >= 0) {
      const cp = Math.max(40, Math.abs(dx) * 0.55);
      return `M${sx},${sy} C${sx + cp},${sy} ${ex - cp},${ey} ${ex},${ey}`;
    }
    // Backward horizontal — drop, traverse along the bottom, come back up
    const dy = (a.y < 110 ? 96 : -96);
    return `M${sx},${sy} L${sx + 16},${sy} C${sx + 56},${sy} ${sx + 56},${sy + dy} ${sx + 16},${sy + dy} L${ex - 16},${ey + dy} C${ex - 56},${ey + dy} ${ex - 56},${ey} ${ex - 16},${ey} L${ex},${ey}`;
  }

  // Payload to surface in the side-rail when activeNode changes.
  // Drives the JSON preview that makes the workflow read as a real product
  // rather than a static diagram. Defaults to the trigger payload at idx 0.
  const payload = (meta.payloads && meta.payloads[activeNode]) || (meta.payloads && meta.payloads[0]) || null;

  return (
    <div className="demo-stage dx-workflow">
      <div className="title">
        <h4>{meta.title}</h4>
        <span className="label">{activeNode >= NODES.length - 1 ? '✓ run complete' : `executing · ${NODES[activeNode]?.name}`}</span>
      </div>
      <div className="canvas">
        <div className="n8n-grid" aria-hidden="true"/>
        <svg viewBox="0 0 660 252" preserveAspectRatio="xMidYMid meet">
          <defs>
            <marker id="n8n-arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
              <path d="M0,0 L0,6 L8,3 Z" fill="rgba(247,248,250,.30)"/>
            </marker>
            <marker id="n8n-arrow-done" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
              <path d="M0,0 L0,6 L8,3 Z" fill="#0E7C63"/>
            </marker>
            <marker id="n8n-arrow-active" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
              <path d="M0,0 L0,6 L8,3 Z" fill="#fff"/>
            </marker>
            <filter id="glow-active" x="-50%" y="-50%" width="200%" height="200%">
              <feGaussianBlur stdDeviation="3" result="blur"/>
              <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
            </filter>
          </defs>

          {/* Edges */}
          {EDGES.map(([ai, bi], i) => {
            const isDone   = i <  activeNode - 1 || (activeNode > 0 && i < activeNode);
            const isActive = i === activeNode - 1 && activeNode > 0;
            const cls = isActive ? 'edge active' : (isDone ? 'edge done' : 'edge');
            const marker = isActive ? 'n8n-arrow-active' : (isDone ? 'n8n-arrow-done' : 'n8n-arrow');
            return <path key={i} className={cls} d={edgePath(NODES[ai], NODES[bi])} markerEnd={`url(#${marker})`}/>;
          })}

          {/* Nodes */}
          {NODES.map((n, i) => {
            const isDone   = i <  activeNode;
            const isActive = i === activeNode;
            const isDim    = i >  activeNode;
            return (
              <g key={n.name} className={'n8n-node ' + (isDone ? 'done ' : '') + (isActive ? 'active ' : '') + (isDim ? 'dim' : '')}
                transform={`translate(${n.x},${n.y})`}
                onClick={() => jumpTo && jumpTo(i)} role="button" tabIndex="0"
                style={{ cursor: 'pointer' }}>
                <rect className="body" x="0" y="0" width={W} height={H} rx="10"/>
                <rect className="bar"  x="0" y="0" width="4" height={H} fill={n.color}/>
                <rect className="ico-bg" x="12" y="12" width="52" height="52" rx="10" fill={n.color}/>
                <g transform="translate(24,24) scale(1.167)">
                  <N8nIcon name={n.icon}/>
                </g>
                <text className="name" x="76" y="34">{n.name}</text>
                <text className="op"   x="76" y="54">{n.op}</text>
                {isDone && !isActive && (
                  <g transform={`translate(${W - 22},14)`}>
                    <circle r="7" className="dot done"/>
                    <path d="M-3,0 L-1,2 L3,-2" stroke="#fff" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
                  </g>
                )}
                {isActive && <circle cx={W - 22} cy="14" r="6" className="dot active"/>}
                <circle cx="0"  cy={H / 2} r="4" className="port in"/>
                <circle cx={W}  cy={H / 2} r="4" className="port out"/>
              </g>
            );
          })}

          {/* Packet bead */}
          <g ref={packetRef} style={{ visibility: 'hidden' }}>
            <circle r="11" fill="rgba(14,124,99,.35)"/>
            <circle r="6" fill="#fff"/>
          </g>
        </svg>
      </div>
      <div className="log">
        <div className="label" style={{ marginBottom: 8 }}>execution log · scrubbed of PII</div>
        {LOG.slice(0, activeNode + 1).map(([ts, body, k], i) => (
          <div className="line" key={i} style={{ animationDelay: (i * 40) + 'ms' }}>
            <span className="ts">[{ts}]</span>
            <span className={k === 'ok' ? 'ok' : 'now'}>{body}</span>
          </div>
        ))}
        {payload && (
          <div className="payload" key={'p-' + activeNode}>
            <div className="head">
              <span className="tag">{payload.lang}</span>
              <span className="lbl">{payload.label}</span>
            </div>
            <pre>{payload.body}</pre>
          </div>
        )}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════
// DEMO 2 · agentic AI — narration-driven worker highlights
// ════════════════════════════════════════════════════════════════════
function DemoAgents({ meta, focus, jumpTo }) {
  const initialQueue = [
    { id:1, src:'support · email',   body:'Refund request · order #18472' },
    { id:2, src:'support · chat',    body:'How do I cancel my subscription?' },
    { id:3, src:'support · webform', body:'Login fails after password reset' },
    { id:4, src:'support · email',   body:'Wrong size — exchange for L' },
    { id:5, src:'support · chat',    body:'Tracking number not updating' },
  ];
  const WORKERS = ['classify','enrich','draft','dispatch'];
  const PANEL_TO_SEG = { queue: 0, classify: 1, enrich: 2, draft: 3, dispatch: 4, done: 5 };

  const [queue, setQueue] = useStateD(initialQueue);
  const [busy, setBusy]   = useStateD(new Set());
  const [done, setDone]   = useStateD([]);
  const counter = useRefD(initialQueue.length);

  // Light up the worker the narrator is currently describing
  const focusedWorker = focus && focus.worker;
  const focusedPanel  = focus && focus.panel;
  // Per-worker classifier preview — surfaces what the agent is actually
  // deciding right now. Without it, the four pills read as abstract "AI".
  const cls = (meta.classifications && focusedWorker && meta.classifications[focusedWorker]) || null;
  const currentTicket = queue[0] || initialQueue[0];

  useEffectD(() => {
    let cancelled = false;
    function tick() {
      if (cancelled) return;
      setQueue(q => {
        if (q.length === 0) return q;
        const next = q[0];
        const rest = q.slice(1);
        WORKERS.forEach((w, i) => {
          setTimeout(() => !cancelled && setBusy(b => { const n = new Set(b); n.add(w); return n; }), i * 350);
          setTimeout(() => !cancelled && setBusy(b => { const n = new Set(b); n.delete(w); return n; }), i * 350 + 700);
        });
        setTimeout(() => {
          if (cancelled) return;
          setDone(d => [{ ...next, status:'auto-resolved' }, ...d].slice(0, 4));
          counter.current += 1;
          const fresh = [...initialQueue][counter.current % initialQueue.length];
          setQueue(qq => qq.length < 4 ? [...qq, { ...fresh, id: counter.current * 10 + Math.random() }] : qq);
        }, WORKERS.length * 350 + 600);
        return rest;
      });
    }
    tick();
    const id = setInterval(tick, 3400);
    return () => { cancelled = true; clearInterval(id); };
  }, []);

  return (
    <div className="demo-stage dx-agents">
      <div className="title">
        <h4>{meta.title}</h4>
        <span className="label">{done.length} resolved · {queue.length} queued</span>
      </div>
      <div className={'queue ' + (focusedPanel === 'queue' ? 'fxd' : '')}
        onClick={() => jumpTo(PANEL_TO_SEG.queue)}>
        <div className="col-h"><span className="label">inbound queue</span><span className="ct">{queue.length}</span></div>
        {queue.map(t => (
          <div className="ticket" key={t.id}><span className="src">{t.src}</span><span>{t.body}</span></div>
        ))}
      </div>
      <div className="board">
        <div className="controller" aria-hidden="true"/>
        {cls && currentTicket && (
          <div className="agent-decision" key={'cls-' + focusedWorker} aria-live="polite">
            <div className="head">
              <span className="tag">{focusedWorker}</span>
              <span className="src">{currentTicket.src}</span>
            </div>
            <div className="subj">“{currentTicket.body}”</div>
            <div className="kvs">
              {Object.entries(cls).map(([k, v]) => (
                <div className="kv" key={k}>
                  <span className="k">{k}</span>
                  <span className="v">{typeof v === 'number' && v < 1 ? v.toFixed(2) : String(v)}</span>
                </div>
              ))}
            </div>
          </div>
        )}
        <div className="workers">
          {WORKERS.map(w => (
            <div className={'worker ' + (busy.has(w) ? 'busy ' : '') + (focusedWorker === w ? 'fxd' : '')}
              key={w}
              onClick={() => jumpTo(PANEL_TO_SEG[w])}>
              <span className="pulse"/>
              <div className="name">step</div>
              <div className="lbl">{w}</div>
            </div>
          ))}
        </div>
        <span className="label" style={{ marginTop: 4 }}>
          {busy.size > 0 ? '↻ executing…' : 'awaiting next ticket'}
        </span>
      </div>
      <div className={'done ' + (focusedPanel === 'done' ? 'fxd' : '')}
        onClick={() => jumpTo(PANEL_TO_SEG.done)}>
        <div className="col-h"><span className="label">resolved</span><span className="ct">{counter.current - queue.length}</span></div>
        {done.map((t, i) => (
          <div className="ticket done" key={t.id + '-' + i}><span className="src">✓ {t.status}</span><span>{t.body}</span></div>
        ))}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════
// DEMO 3 · CRM pipeline — focused column glows
// ════════════════════════════════════════════════════════════════════
function DemoCRM({ meta, focus, jumpTo }) {
  const NAMES = ['Sophie K.','Marcus L.','Aisha P.','Lukas R.','Diego M.','Wren T.','Bea C.','Jade O.','Noah W.','Pia G.'];
  const VALUES = ['$ 1.8M','$ 2.4M','$ 980k','$ 1.2M','$ 1.6M','$ 2.1M','$ 760k','$ 1.4M','$ 2.9M','$ 1.05M'];
  const initials = (n) => n.split(' ').map(w => w[0]).join('').slice(0, 2);

  function mkLead(i, col) {
    return {
      id: i + '-' + Math.random().toString(36).slice(2, 7),
      name: NAMES[i % NAMES.length],
      val: VALUES[i % VALUES.length],
      // AI fit score, 60–98. Stays stable across moves so the lead
      // "keeps its identity" as it walks across the kanban.
      score: 60 + (Math.floor(Math.random() * 39)),
      col
    };
  }
  const [cols, setCols] = useStateD(() => ([
    [mkLead(0, 0), mkLead(1, 0), mkLead(2, 0)],
    [mkLead(3, 1), mkLead(4, 1), mkLead(5, 1)],
    [mkLead(6, 2), mkLead(7, 2)],
    [mkLead(8, 3), mkLead(9, 3), mkLead(0, 3)],
  ]));
  const counter = useRefD(10);

  useEffectD(() => {
    function tick() {
      setCols(prev => {
        const next = prev.map(c => [...c]);
        const movable = next.slice(0, 3).map((c, i) => c.length > 0 ? i : -1).filter(i => i >= 0);
        if (movable.length) {
          const from = movable[Math.floor(Math.random() * movable.length)];
          const lead = next[from].shift();
          if (lead) { lead.col = from + 1; next[from + 1] = [lead, ...next[from + 1]].slice(0, 4); }
        }
        if (next[0].length < 3) next[0].push(mkLead(counter.current++, 0));
        if (next[3].length > 4) next[3] = next[3].slice(0, 4);
        return next;
      });
    }
    const id = setInterval(tick, 1800);
    return () => clearInterval(id);
  }, []);

  const TITLES = [
    { n:'01 · NEW',     k:'inbound'     },
    { n:'02 · SCORED',  k:'enriched'    },
    { n:'03 · VIEWING', k:'booked'      },
    { n:'04 · CLOSED',  k:'won this Q'  },
  ];
  const focusedCol = focus && typeof focus.col === 'number' ? focus.col : null;

  return (
    <div className="demo-stage dx-crm">
      <div className="title">
        <h4>{meta.title}</h4>
        <span className="label">
          auto-enriched · auto-routed · auto-booked
          <span className="velocity">▲ +4 this wk</span>
        </span>
      </div>
      <div className="board">
        {cols.map((leads, i) => (
          <div className={'col col-' + i + (focusedCol === i ? ' fxd' : '')} key={i}
            onClick={() => jumpTo(i)}>
            <div className="h"><span className="n">{TITLES[i].n}</span><span className="ct">{leads.length}</span></div>
            {leads.map(lead => (
              <div className="lead" key={lead.id}>
                <div className="who">
                  <div className="avatar">{initials(lead.name)}</div>
                  <span>{lead.name}</span>
                  <span className={'score ' + (lead.score >= 80 ? 'hot' : (lead.score >= 70 ? 'warm' : 'cool'))}>{lead.score}</span>
                </div>
                <span className="meta">{TITLES[i].k}</span>
                <span className="val">{lead.val}</span>
              </div>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════
// DEMO 4 · Chatbot
// ════════════════════════════════════════════════════════════════════
function DemoChat({ meta, focus, jumpTo }) {
  const SCRIPT = [
    { role:'bot',  text:"Hi — I'm Nex. Ask me about pricing, what we build, or book a 15-min map call." },
    { role:'user', text:"Do you handle real-estate workflows?" },
    { role:'bot',  text:"Yes — REA enquiry → AI score → CRM → SMS confirmation. Typical run: 11.2s. Want the live demo or pricing?" },
    { role:'user', text:"What's it cost?" },
    { role:'bot',  text:"From A$2,400 one-off (Spark) — ships in ~2 weeks, you own the code. Or A$1,800/mo on Flow for ongoing builds." },
    { role:'user', text:"Book me a 15-min map." },
    { role:'bot',  text:"On it — paid scoping call ($50 USD, fully credited if you build). Pick a time?", quicks: true },
  ];
  const PANEL_TO_SEG = { session: 0, chat: 1, book: 2, handoff: 3 };
  const focusedPanel = focus && focus.panel;
  const [msgs, setMsgs]     = useStateD([]);
  const [typing, setTyping] = useStateD(false);

  useEffectD(() => {
    let cancelled = false;
    let i = 0;
    function next() {
      if (cancelled) return;
      if (i >= SCRIPT.length) {
        setTimeout(() => { if (!cancelled) { setMsgs([]); i = 0; setTimeout(next, 600); } }, 5000);
        return;
      }
      const s = SCRIPT[i];
      if (s.role === 'bot') {
        setTyping(true);
        setTimeout(() => {
          if (cancelled) return;
          setTyping(false);
          setMsgs(m => [...m, s]); i++; setTimeout(next, 900);
        }, 950 + s.text.length * 8);
      } else {
        setTimeout(() => {
          if (cancelled) return;
          setMsgs(m => [...m, s]); i++; setTimeout(next, 700);
        }, 1100);
      }
    }
    setTimeout(next, 400);
    return () => { cancelled = true; };
  }, []);

  const last = msgs[msgs.length - 1];

  return (
    <div className="demo-stage dx-chat">
      <div className="title">
        <h4>{meta.title}</h4>
        <span className="label">grounded in your pricing · docs · case studies</span>
      </div>
      <div className="body">
        <div className={'phone ' + (focusedPanel === 'chat' ? 'fxd' : '')}
          onClick={() => jumpTo(PANEL_TO_SEG.chat)}>
          <div className="chat-head">
            <div className="avatar">N</div>
            <div className="who"><b>Nex</b><span>concierge · grounded</span></div>
            <span className="live">live</span>
          </div>
          <div className="messages">
            {msgs.map((m, i) => {
              // Source chips appear under bot replies — the visible audit
              // trail for what the chatbot was grounded in. We index by the
              // running count of bot messages, not the global index, so
              // chips stay correctly associated as the script replays.
              const botIdx = msgs.slice(0, i + 1).filter(x => x.role === 'bot').length - 1;
              const srcs   = m.role === 'bot' && meta.sources && meta.sources[botIdx];
              return (
                <div className={'msg ' + m.role} key={i}>
                  <div>{m.text}</div>
                  {srcs && (
                    <div className="sources">
                      <span className="src-lbl">grounded in</span>
                      {srcs.map((s, j) => <span className="src-chip" key={j}>{s}</span>)}
                    </div>
                  )}
                </div>
              );
            })}
            {typing && (<div className="typing"><i/><i/><i/></div>)}
          </div>
          {last && last.quicks && (
            <div className="quicks">
              <button>Tue 10:00 AEST</button>
              <button>Wed 14:00 AEST</button>
              <button>Other times…</button>
            </div>
          )}
        </div>
        <div className="side">
          <div className={'panel ' + (focusedPanel === 'session' ? 'fxd' : '')}
            onClick={() => jumpTo(PANEL_TO_SEG.session)}>
            <h5>Session</h5>
            <div className="meta">grounded.match · 0.91 confidence</div>
            <div className="kvs">
              <div className="kv"><span className="k">intent</span><span className="v">book · pricing</span></div>
              <div className="kv"><span className="k">vertical</span><span className="v">real estate</span></div>
              <div className="kv"><span className="k">price tier</span><span className="v">spark · A$2.4k</span></div>
              <div className="kv"><span className="k">sources</span><span className="v">pricing · case · faq</span></div>
            </div>
          </div>
          <div className={'panel ' + (focusedPanel === 'handoff' ? 'fxd' : '')}
            onClick={() => jumpTo(PANEL_TO_SEG.handoff)}>
            <h5>Hand-off</h5>
            <div className="meta">to human when intent · book or quote ≥ A$10k</div>
            <div className="kvs">
              <div className="kv"><span className="k">channel</span><span className="v">slack · #sales</span></div>
              <div className="kv"><span className="k">latency</span><span className="v">&lt; 1 biz day</span></div>
            </div>
          </div>
          {last && last.quicks && (
            <div className={'panel book ' + (focusedPanel === 'book' ? 'fxd' : '')}
              onClick={() => jumpTo(PANEL_TO_SEG.book)}>
              <h5>Booking initialised</h5>
              <div className="meta">→ Cal.com hold placed</div>
              <div className="kvs">
                <div className="kv"><span className="k">slot</span><span className="v">Tue 10:00 AEST</span></div>
                <div className="kv"><span className="k">payment</span><span className="v">Stripe · $50 USD</span></div>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════
// DEMO 5 · Document AI (invoice OCR → Xero) — scan progress follows
// narration; clicking a panel jumps to its segment
// ════════════════════════════════════════════════════════════════════
function DemoDoc({ meta, focus, jumpTo }) {
  const ROWS = 9;
  const FIELDS = [
    { key:'invoice no.', val:'INV-2847',              conf:'0.99', group:'header-fields' },
    { key:'vendor',      val:'Office Group Pty Ltd',  conf:'0.98', group:'header-fields' },
    { key:'date',        val:'12 / 15 / 26',          conf:'0.99', group:'header-fields' },
    { key:'subtotal',    val:'$ 350.00',              conf:'0.99', group:'totals' },
    { key:'tax · gst',   val:'$  39.40',              conf:'0.99', group:'totals' },
    { key:'total',       val:'$ 389.40',              conf:'0.99', group:'totals' },
  ];
  // Map narration field-target to a normalised scan progress (0..1).
  const PROG = { 'header': 0.18, 'header-fields': 0.42, 'totals': 0.78, 'posted': 0.98, 'approval': 0.98 };
  const PANEL_TO_SEG = { 'header': 0, 'header-fields': 1, 'totals': 2, 'posted': 3, 'approval': 4 };

  const focusedField = focus && focus.field;
  const targetT = focusedField && PROG[focusedField] != null ? PROG[focusedField] : 0;
  const [t, setT] = useStateD(0);

  // Tween t toward targetT for a smooth scan beam motion.
  useEffectD(() => {
    let raf;
    function frame() {
      setT(prev => {
        const delta = targetT - prev;
        if (Math.abs(delta) < 0.002) return targetT;
        return prev + delta * 0.06;
      });
      raf = requestAnimationFrame(frame);
    }
    raf = requestAnimationFrame(frame);
    return () => cancelAnimationFrame(raf);
  }, [targetT]);

  const scanProgress = Math.min(t / 0.85, 1);
  const scanTop = `${6 + scanProgress * 86}%`;
  const allDone = t > 0.9;

  return (
    <div className="demo-stage dx-doc">
      <div className="title">
        <h4>{meta.title}</h4>
        <span className="label">{allDone ? 'posted to xero · ✓ approved < $500' : 'scanning…'}</span>
      </div>
      <div className="stage">
        <div className="paper-wrap"
          onClick={() => jumpTo(PANEL_TO_SEG['header'])}>
          <div className="paper">
            <div className="ph">
              <div>
                <div className="co">Office Group Pty Ltd</div>
                <div className="co-meta">Supplier ID 41824753921  ·  Service account</div>
              </div>
              <div className="inv">
                <div>INV-2847</div>
                <div className="date">12 Dec 2026</div>
              </div>
            </div>
            <div className="bill-to">
              <div className="bt-lbl">Bill to</div>
              <div className="bt-val">NexFlow Pty Ltd  ·  support@nex-flow.io</div>
            </div>
            <div className="li-head">
              <span>item</span><span>qty</span><span>unit</span><span>total</span>
            </div>
            {[
              {n:'Ergonomic mesh chair · X4 Pro', q:2, u:'$ 129.00', t:'$ 258.00'},
              {n:'Standing desk converter',       q:1, u:'$  68.00', t:'$  68.00'},
              {n:'Cable management tray',         q:3, u:'$   8.00', t:'$  24.00'},
            ].map((li, i) => {
              const rowAt = 0.30 + (i / 4) * 0.32;
              return (
                <div key={i} className={'li ' + (scanProgress > rowAt ? 'scanned' : '')}>
                  <span className="li-n">{li.n}</span>
                  <span className="li-q">{li.q}</span>
                  <span className="li-u">{li.u}</span>
                  <span className="li-t">{li.t}</span>
                </div>
              );
            })}
            <div className={'sub ' + (scanProgress > 0.72 ? 'scanned' : '')}>
              <span>subtotal</span><span>$ 350.00</span>
            </div>
            <div className={'sub ' + (scanProgress > 0.80 ? 'scanned' : '')}>
              <span>GST  ·  10%</span><span>$  39.40</span>
            </div>
            <div className={'total ' + (scanProgress > 0.85 ? 'scanned' : '')}>
              <span className="lbl">total due</span><span className="v">$ 389.40</span>
            </div>
          </div>
          {/* Field-highlight box: a faint indigo rectangle that tracks the
              scan beam, calling out which region is currently being
              captured. Hidden once the run completes. */}
          {t < 0.92 && (
            <div className="hl-box" style={{ top: `calc(${4 + scanProgress * 84}% - 9px)` }}/>
          )}
          <div className="beam" style={{ top: scanTop, display: t < 0.92 ? 'block' : 'none' }}/>
        </div>
        <div className="fields">
          <div className="heading">
            <span className="label">extracted fields</span>
            <span className="label" style={{ color: allDone ? '#4FD8A3' : 'rgba(247,248,250,.5)' }}>
              {allDone ? '✓ verified' : 'live'}
            </span>
          </div>
          {FIELDS.map((f, i) => {
            const visible = (f.group === 'header-fields' && t >= PROG['header-fields'] - 0.10)
                          || (f.group === 'totals' && t >= PROG['totals'] - 0.10);
            const done    = (f.group === 'header-fields' && t >= PROG['header-fields'])
                          || (f.group === 'totals' && t >= PROG['totals']);
            const isFxd   = focusedField === f.group;
            return (
              <div key={f.key} className={'field ' + (visible ? 'in ' : '') + (done ? 'done ' : '') + (isFxd ? 'fxd' : '')}
                onClick={() => jumpTo(PANEL_TO_SEG[f.group])}>
                <span className="dot"/>
                <div>
                  <div className="key">{f.key}</div>
                  <div className="val">{f.val}</div>
                </div>
                <span className="conf">{f.conf}</span>
              </div>
            );
          })}
          <div className={'posted ' + (allDone ? 'in ' : '') + (focusedField === 'posted' || focusedField === 'approval' ? 'fxd' : '')}
            onClick={() => jumpTo(PANEL_TO_SEG['posted'])}>
            <div className="check">✓</div>
            <div>
              <div style={{ fontWeight: 500 }}>Posted to Xero · auto-approved</div>
              <div className="label" style={{ marginTop: 2 }}>amount &lt; $500 threshold · Slack #finance notified</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════
// DEMO 6 · Reporting / dashboard — focused panel glows
// ════════════════════════════════════════════════════════════════════
function DemoReport({ meta, focus, jumpTo }) {
  const KPIS = [
    { k:'revenue · MTD',   d:'+12.4%',  base:48720, scale:120 },
    { k:'orders · today',  d:'+4',      base:147,   scale:6   },
    { k:'leads · this wk', d:'+22%',    base:84,    scale:3   },
    { k:'NPS · last 100',  d:'+3',      base:72,    scale:1   },
  ];
  const PANEL_TO_SEG = { kpis: 0, bars: 2, gauge: 3 };
  const focusedPanel = focus && focus.panel;

  const [tick, setTick] = useStateD(0);
  const [bars, setBars] = useStateD(() => Array.from({ length: 14 }, () => Math.random() * 0.5 + 0.3));
  const [gv, setGv] = useStateD(0.74);

  useEffectD(() => {
    const id = setInterval(() => {
      setTick(t => t + 1);
      setBars(prev => prev.map((v) => Math.max(0.15, Math.min(1, v + (Math.random() - 0.45) * 0.18))));
      setGv(g => Math.max(0.4, Math.min(0.96, g + (Math.random() - 0.5) * 0.06)));
    }, 1100);
    return () => clearInterval(id);
  }, []);

  const C = 2 * Math.PI * 64;
  const offset = C - C * gv;

  return (
    <div className="demo-stage dx-report">
      <div className="title">
        <h4>{meta.title}</h4>
        <span className="label">delivered to inbox · slack · pdf</span>
      </div>
      <div className={'kpis ' + (focusedPanel === 'kpis' ? 'fxd' : '')}
        onClick={() => jumpTo(PANEL_TO_SEG.kpis)}>
        {KPIS.map((kpi, ki) => {
          const v = kpi.base + (tick % 7) * kpi.scale * 0.6;
          const formatted = (kpi.k.startsWith('revenue') ? '$ ' : '') + Math.round(v).toLocaleString();
          // Synthesise a 14-point sparkline per KPI, seeded by index so each
          // KPI has its own stable trend curve. The last point follows
          // `tick` so the line breathes in sync with the rest of the demo.
          const pts = Array.from({length: 14}, (_, j) => {
            const phase = (j / 13) * Math.PI * 2 + ki;
            return 0.5 + Math.sin(phase + tick * 0.4) * 0.18 + Math.sin(phase * 0.6 + ki) * 0.12;
          });
          const w = 78, h = 22;
          const path = pts.map((p, j) => {
            const x = (j / (pts.length - 1)) * w;
            const y = h - p * h;
            return (j === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1);
          }).join(' ');
          const last = pts[pts.length - 1];
          return (
            <div className="kpi" key={kpi.k}>
              <span className="k">{kpi.k}</span>
              <div className="row">
                <span className="v">{formatted}</span>
                <svg className="spark" viewBox={`0 0 ${w} ${h}`} aria-hidden="true">
                  <path d={path} fill="none" stroke="#4FD8A3" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
                  <circle cx={w} cy={h - last * h} r="2" fill="#4FD8A3"/>
                </svg>
              </div>
              <span className="d">▲ {kpi.d}</span>
            </div>
          );
        })}
      </div>
      <div className="charts">
        <div className={'chart ' + (focusedPanel === 'bars' ? 'fxd' : '')}
          onClick={() => jumpTo(PANEL_TO_SEG.bars)}>
          <div className="h"><span className="ttl">orders · 14 days</span><span className="label">live</span></div>
          <div className="bars">
            {bars.map((v, i) => (
              <div key={i} className={'bar ' + (i === bars.length - 1 ? 'hot' : '')} style={{ height: (v * 100) + '%' }}/>
            ))}
          </div>
          <div className="ticker">
            <span>last sync · just now</span>
            <span className="ok">▲ 18% wow</span>
            <span>source · stripe · ga · xero</span>
          </div>
        </div>
        <div className={'chart ' + (focusedPanel === 'gauge' ? 'fxd' : '')}
          onClick={() => jumpTo(PANEL_TO_SEG.gauge)}>
          <div className="h"><span className="ttl">automation coverage</span><span className="label">target 80%</span></div>
          <div className="gauge-wrap">
            <svg className="gauge" viewBox="0 0 160 160">
              <circle className="bg" cx="80" cy="80" r="64"/>
              <circle className="fg" cx="80" cy="80" r="64"
                strokeDasharray={C} strokeDashoffset={offset}
                style={{ stroke: gv >= 0.75 ? '#4FD8A3' : '#0E7C63' }}/>
            </svg>
            <div className="gauge-label">
              <div className="v">{Math.round(gv * 100)}%</div>
              <div className="k">of ops automated</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ── Mount helper ────────────────────────────────────────────────────
function DemoRoot() {
  const [open, setOpen] = useStateD(null);
  const [opts, setOpts] = useStateD({});
  const close = () => { setOpen(null); setOpts({}); };
  useEffectD(() => {
    window.NF_ServiceDemos = {
      openDemo: (id, nextOpts) => { setOpen(id); setOpts(nextOpts || {}); },
      closeDemo: close,
    };
    return () => { delete window.NF_ServiceDemos; };
  }, []);
  if (!open) return null;
  return <DemoModal serviceId={open} opts={opts} onClose={close} />;
}

window.NF_DemoRoot = DemoRoot;
