/**
 * workflow.jsx — Interactive workflow canvas that builds itself as the user scrolls.
 * Node & edge rendering matches the Alongtail app pixel-for-pixel.
 */

const { useState, useRef, useCallback, useEffect, useMemo } = React;

/* ─── tokens (exact from tokens.css) ─── */
const T = {
  bg: '#f0eeeb',
  dots: '#e0dcd6',
  ink: '#2c2c2e',
  inkSecondary: '#8e8e93',
  inkTertiary: '#aeaeb2',
  inkQuaternary: '#c7c7cc',
  borderPrimary: 'rgba(0,0,0,0.06)',
  borderSecondary: 'rgba(0,0,0,0.03)',
  surface: '#ffffff',
  blue: '#587ec2',
  green: '#5fa97e',
  purple: '#9678b6',
  orange: '#d4915a',
  red: '#c96b68',
  shadowNode: '0 1px 4px rgba(0,0,0,0.04)',
};

/* ─── pools & lanes ─── */
const POOLS = [
  { id: 'p1', name: 'Customer',       y: 40,  h: 130, r: 0.00 },
  { id: 'p2', name: 'Sales & CS',     y: 190, h: 250, r: 0.04 },
  { id: 'p3', name: 'Onboarding Ops', y: 460, h: 250, r: 0.25 },
  { id: 'p4', name: 'Platform',       y: 730, h: 250, r: 0.55 },
];

const LANES = [
  { id: 'l1', poolId: 'p1', name: 'Buyer',          y: 50,  h: 110, r: 0.00, color: T.blue },
  { id: 'l2', poolId: 'p2', name: 'Account Exec',   y: 200, h: 110, r: 0.05, color: T.blue },
  { id: 'l3', poolId: 'p2', name: 'Solutions Eng',   y: 320, h: 110, r: 0.12, color: T.purple },
  { id: 'l4', poolId: 'p3', name: 'Implementation',  y: 470, h: 110, r: 0.26, color: T.green },
  { id: 'l5', poolId: 'p3', name: 'Data',            y: 590, h: 110, r: 0.40, color: T.orange },
  { id: 'l6', poolId: 'p4', name: 'Infra',           y: 740, h: 110, r: 0.58, color: T.purple },
  { id: 'l7', poolId: 'p4', name: 'Integrations',    y: 860, h: 110, r: 0.70, color: T.green },
];

/* ─── nodes ─── */
const POOL_X = 40;
const POOL_W = 1500;
const LANE_X = 80;
const LANE_W = 1460;

const INITIAL_NODES = [
  // Buyer lane — center 105: event y=91, task y=78
  { id: 'n1',  type: 'event-start',       x: 100,  y: 91,  label: 'Contract signed',       r: 0.02, detail: { 'Trigger': 'DocuSign webhook', 'Frequency': '~12 / month' } },
  { id: 'n2',  type: 'task',              x: 210,  y: 78,  w: 140, label: 'Kickoff with buyer',   desc: 'Intro call, align on timeline and deliverables', subtype: 'manual',    r: 0.06, detail: { 'Duration': '45 min', 'Owner': 'Account Exec' } },
  // Account Exec lane — center 255: task y=228, data y=238, gateway y=239
  { id: 'n3',  type: 'task',              x: 420,  y: 228, w: 140, label: 'Scope & requirements', desc: 'Gather needs via Notion intake form', subtype: 'assisted',  r: 0.08, detail: { 'Duration': '3 days', 'Wait time': '1 day', 'System': 'Notion + Jira' } },
  { id: 'n4',  type: 'data',              x: 650,  y: 238, label: 'SOW draft',              r: 0.12, state: 'draft', detail: { 'State': 'draft', 'Owner': 'Account Exec' } },
  // Solutions Eng lane — center 375: task y=348, gateway y=359
  { id: 'n5',  type: 'task',              x: 420,  y: 348, w: 140, label: 'Technical discovery',  desc: 'API audit, data model review', subtype: 'manual',    r: 0.14, detail: { 'Duration': '2 days', 'Owner': 'Solutions Eng' } },
  { id: 'n6',  type: 'gateway',           x: 660,  y: 359, label: 'SSO required?',          r: 0.18, detail: { 'Type': 'Decision', 'Criteria': 'Contract > $50k OR security addendum', 'Yes': '68%', 'No': '32%' } },
  { id: 'n7',  type: 'subprocess',        x: 790,  y: 348, w: 145, label: 'Provision tenant',     desc: '6 steps, 2 gateways', r: 0.28, detail: { 'Contains': '6 steps, 2 gateways', 'Duration': '~4 hours' } },
  // Implementation lane — center 525: task y=498, event y=511
  { id: 'n8',  type: 'task',              x: 260,  y: 498, w: 140, label: 'Draft onboarding plan', desc: 'Milestones, owners, timeline', subtype: 'manual',    r: 0.30, detail: { 'Duration': '1 day', 'Owner': 'Implementation' } },
  { id: 'n9',  type: 'event-timer',       x: 490,  y: 511, label: '5 business days',        r: 0.34, detail: { 'Design wait': '5 days', 'Actual wait': '6.3 days avg' } },
  { id: 'n10', type: 'task',              x: 580,  y: 498, w: 140, label: 'Configure workspace',  desc: 'Roles, permissions, branding', subtype: 'assisted',  r: 0.36, detail: { 'Duration': '6 h', 'System': 'Internal admin', 'Error rate': '4%' } },
  // Data lane — center 645: task y=618, data y=628, gateway y=629
  { id: 'n11', type: 'task',              x: 260,  y: 618, w: 140, label: 'Import seed records',  desc: 'ETL from CSV or API source', subtype: 'automated', r: 0.42, detail: { 'Duration': '12 min', 'System': 'ETL pipeline', 'Error rate': '1.2%' } },
  { id: 'n12', type: 'data',              x: 490,  y: 628, label: 'Schema map',             r: 0.46, state: 'generated', detail: { 'State': 'generated' } },
  { id: 'n13', type: 'gateway',           x: 630,  y: 629, label: 'PII in dataset?',        r: 0.50, detail: { 'Type': 'Judgment', 'Yes': '41%', 'No': '59%' } },
  { id: 'n14', type: 'task',              x: 740,  y: 618, w: 135, label: 'Redact & review',      desc: 'Mask PII, validate output', subtype: 'assisted',  r: 0.52, detail: { 'Duration': '2 h', 'Owner': 'Data' } },
  // Infra lane — center 795: task y=768, event y=781
  { id: 'n15', type: 'task',              x: 260,  y: 768, w: 140, label: 'Spin up cluster',      desc: 'K8s namespace + resource quotas', subtype: 'automated', r: 0.60, detail: { 'Duration': '8 min', 'System': 'Kubernetes' } },
  { id: 'n16', type: 'event-interruption', x: 490, y: 781, label: 'Quota exceeded',         r: 0.64, detail: { 'Breaks flow': 'yes', 'Frequency': '2% of onboardings' } },
  { id: 'n17', type: 'task',              x: 580,  y: 768, w: 140, label: 'Escalate to capacity',  desc: 'Page infra lead, request bump', subtype: 'manual',    r: 0.66, detail: { 'Duration': '30 min', 'Owner': 'Infra lead' } },
  // Integrations lane — center 915: task y=888, data y=898
  { id: 'n18', type: 'task',              x: 260,  y: 888, w: 145, label: 'Wire CRM + SSO',       desc: 'Okta SAML config, HubSpot sync', subtype: 'assisted',  r: 0.72, detail: { 'Duration': '4 h', 'System': 'Okta + HubSpot' } },
  { id: 'n19', type: 'data',              x: 500,  y: 898, label: 'Webhook spec',           r: 0.76, state: 'signed', detail: { 'State': 'signed' } },
  { id: 'n20', type: 'task',              x: 650,  y: 888, w: 140, label: 'Run smoke tests',      desc: 'End-to-end validation suite', subtype: 'automated', r: 0.80, detail: { 'Duration': '2 min', 'Error rate': '0.5%' } },
  // Gateway in Implementation lane — center 525: gateway y=509
  { id: 'n21', type: 'gateway',           x: 1060, y: 509, label: 'All checks pass?',       r: 0.85, detail: { 'Type': 'Decision', 'Yes': '94%', 'No': '6%' } },
  // Handoff in Solutions Eng lane — center 375: task y=348
  { id: 'n22', type: 'task',              x: 1140, y: 348, w: 140, label: 'Handoff to CSM',       desc: 'Transfer context, schedule check-in', subtype: 'manual',    r: 0.90, detail: { 'Duration': '1 h', 'Owner': 'CSM' } },
  // End in Solutions Eng lane — center 375: event y=361
  { id: 'n23', type: 'event-end',         x: 1370, y: 361, label: 'Customer live',          r: 0.95, detail: { 'Outcome': 'Customer live in production' } },
];

const EDGES = [
  { from: 'n1',  to: 'n2' },
  { from: 'n2',  to: 'n3' },
  { from: 'n3',  to: 'n4' },
  { from: 'n3',  to: 'n5' },
  { from: 'n5',  to: 'n6' },
  { from: 'n6',  to: 'n7',  label: 'Yes' },
  { from: 'n4',  to: 'n8' },
  { from: 'n7',  to: 'n10' },
  { from: 'n8',  to: 'n9' },
  { from: 'n9',  to: 'n10' },
  { from: 'n8',  to: 'n11' },
  { from: 'n11', to: 'n12' },
  { from: 'n12', to: 'n13' },
  { from: 'n13', to: 'n14', label: 'Yes' },
  { from: 'n10', to: 'n15' },
  { from: 'n15', to: 'n16' },
  { from: 'n16', to: 'n17' },
  { from: 'n15', to: 'n18' },
  { from: 'n18', to: 'n19' },
  { from: 'n19', to: 'n20' },
  { from: 'n10', to: 'n21' },
  { from: 'n14', to: 'n21' },
  { from: 'n17', to: 'n21' },
  { from: 'n20', to: 'n21' },
  { from: 'n21', to: 'n22', label: 'Pass' },
  { from: 'n22', to: 'n23' },
];

/* ─── geometry helpers ─── */
// Exact node dimensions from the app
const TASK_H = 54;      // header (20) + body with desc (34)
const EVENT_R = 14;     // 28px diameter
const GATEWAY_SIZE = 32;
const DATA_ICON_W = 28;
const DATA_ICON_H = 34;

function nodeW(n) {
  if (n.type === 'gateway')  return GATEWAY_SIZE;
  if (n.type.startsWith('event')) return EVENT_R * 2;
  if (n.type === 'data')     return DATA_ICON_W;
  return n.w || 140;
}
function nodeH(n) {
  if (n.type === 'gateway')  return GATEWAY_SIZE;
  if (n.type.startsWith('event')) return EVENT_R * 2;
  if (n.type === 'data')     return DATA_ICON_H;
  return n.desc ? TASK_H : 42;
}

function nodePort(n, side) {
  const w = nodeW(n), h = nodeH(n);
  const cx = n.x + w / 2, cy = n.y + h / 2;
  if (side === 'right') return { x: n.x + w, y: cy };
  if (side === 'left')  return { x: n.x, y: cy };
  if (side === 'top')   return { x: cx, y: n.y };
  return { x: cx, y: n.y + h };
}

function edgePath(from, to) {
  const s = nodePort(from, 'right');
  const e = nodePort(to, 'left');
  const dy = Math.abs(e.y - s.y);
  const dx = e.x - s.x;
  // Backward edge (loop): route above via top ports with smooth curves
  if (dx < -20) {
    const st = nodePort(from, 'top');
    const et = nodePort(to, 'top');
    const loopY = Math.min(st.y, et.y) - 35;
    return `M${st.x},${st.y} C${st.x},${loopY} ${et.x},${loopY} ${et.x},${et.y}`;
  }
  // If target is mostly below/above and not far right, route vertically
  if (dy > 80 && dx < 60) {
    const sb = e.y > s.y ? nodePort(from, 'bottom') : nodePort(from, 'top');
    const et = e.y > s.y ? nodePort(to, 'top') : nodePort(to, 'bottom');
    const my = (sb.y + et.y) / 2;
    return `M${sb.x},${sb.y} C${sb.x},${my} ${et.x},${my} ${et.x},${et.y}`;
  }
  const cx = Math.max(Math.abs(dx) * 0.35, 40);
  return `M${s.x},${s.y} C${s.x + cx},${s.y} ${e.x - cx},${e.y} ${e.x},${e.y}`;
}

function edgeLabelPos(from, to) {
  const s = nodePort(from, 'right');
  const e = nodePort(to, 'left');
  const dy = Math.abs(e.y - s.y);
  const dx = e.x - s.x;
  // Backward edge label: centered above
  if (dx < -20) {
    const st = nodePort(from, 'top');
    const et = nodePort(to, 'top');
    const loopY = Math.min(st.y, et.y) - 35;
    return { x: (st.x + et.x) / 2, y: loopY };
  }
  if (dy > 80 && dx < 60) {
    const sb = e.y > s.y ? nodePort(from, 'bottom') : nodePort(from, 'top');
    const et = e.y > s.y ? nodePort(to, 'top') : nodePort(to, 'bottom');
    return { x: (sb.x + et.x) / 2, y: (sb.y + et.y) / 2 };
  }
  return { x: (s.x + e.x) / 2, y: (s.y + e.y) / 2 };
}

/* ─── step type icons (12x12 SVGs matching StepNode.tsx) ─── */
function StepIcon({ subtype, x, y }) {
  const color = T.green; // all step type icons use green in the header
  if (subtype === 'manual') {
    // Hand icon (matches StepNode.tsx ManualIcon — Lucide hand)
    return (
      <g transform={`translate(${x},${y}) scale(0.38)`}>
        <path d="M18 11V6a2 2 0 0 0-2-2 2 2 0 0 0-2 2v0M14 10V4a2 2 0 0 0-2-2 2 2 0 0 0-2 2v2"
          stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
        <path d="M10 10.5V6a2 2 0 0 0-2-2 2 2 0 0 0-2 2v8"
          stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
        <path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.9-5.9-2.8L3.3 16a2 2 0 0 1 3.2-2.4l.9 1.2"
          stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
      </g>
    );
  }
  if (subtype === 'assisted') {
    // Monitor/computer icon (matches StepNode.tsx AssistedIcon)
    return (
      <g transform={`translate(${x},${y}) scale(0.38)`}>
        <rect x="2" y="3" width="20" height="14" rx="2" stroke={color} strokeWidth="1.5" fill="none" />
        <path d="M8 21h8M12 17v4" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
      </g>
    );
  }
  if (subtype === 'automated') {
    // Robot icon (matches StepNode.tsx AutomatedIcon)
    return (
      <g transform={`translate(${x},${y}) scale(0.38)`}>
        <rect x="3" y="8" width="18" height="12" rx="2" stroke={color} strokeWidth="1.5" fill="none" />
        <path d="M12 2v6" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
        <circle cx="12" cy="2" r="1" fill={color} />
        <circle cx="9" cy="14" r="1.5" fill={color} />
        <circle cx="15" cy="14" r="1.5" fill={color} />
      </g>
    );
  }
  return null;
}

/* ─── type label text ─── */
function typeLabel(subtype) {
  if (subtype === 'manual')    return 'MANUAL';
  if (subtype === 'assisted')  return 'ASSISTED';
  if (subtype === 'automated') return 'AUTOMATED';
  return 'TASK';
}

/* ─── reveal easing ─── */
function revealAlpha(progress, nodeR) {
  const fadeDur = 0.05;
  const start = Math.max(0, nodeR - 0.01);
  if (progress < start) return 0;
  if (progress >= start + fadeDur) return 1;
  const t = (progress - start) / fadeDur;
  return t * t * (3 - 2 * t);
}

/* ─── Tooltip (portaled to body so CSS transforms don't break fixed positioning) ─── */
function Tooltip({ node, pos }) {
  if (!node) return null;
  const detail = node.detail || {};
  const entries = Object.entries(detail);
  const tipW = 240;
  const tipMargin = 6;
  const vw = window.innerWidth;
  const vh = window.innerHeight;
  const nr = pos.nodeRight || 0, nl = pos.nodeLeft || 0, nt = pos.nodeTop || 0, nb = pos.nodeBottom || 0;
  // Place to the right of the node; flip left if it would overflow
  const flipX = nr + tipMargin + tipW > vw - 16;
  let left = flipX ? Math.max(8, nl - tipMargin - tipW) : nr + tipMargin;
  left = Math.max(8, Math.min(left, vw - tipW - 8));
  // Align top of tooltip with top of node; clamp to viewport
  const top = Math.max(8, Math.min(nt, vh - 160));
  return ReactDOM.createPortal(
    <div style={{
      position: 'fixed', left, top, zIndex: 10000,
      background: T.surface, border: `1px solid ${T.borderPrimary}`, borderRadius: 12,
      padding: '12px 16px', boxShadow: '0 8px 28px rgba(0,0,0,0.10)',
      pointerEvents: 'none', maxWidth: tipW,
      fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif",
      fontSize: 13, color: T.ink, lineHeight: 1.5,
    }}>
      <div style={{ fontWeight: 600, marginBottom: 2, fontSize: 14 }}>{node.label}</div>
      {node.subtype && (
        <div style={{ fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.3px',
          color: T.inkSecondary, marginBottom: 4 }}>
          {typeLabel(node.subtype)}
        </div>
      )}
      {entries.length > 0 && (
        <div style={{ borderTop: `1px solid ${T.borderPrimary}`, marginTop: 6, paddingTop: 6 }}>
          {entries.map(([k, v]) => (
            <div key={k} style={{ display: 'flex', justifyContent: 'space-between', gap: 16, fontSize: 12 }}>
              <span style={{ color: T.inkSecondary }}>{k}</span>
              <span style={{ fontWeight: 500, fontFamily: "'SF Mono', ui-monospace, monospace", fontSize: 11 }}>{v}</span>
            </div>
          ))}
        </div>
      )}
    </div>,
    document.body
  );
}

/* ─── SVG filter for node shadow ─── */
const NODE_SHADOW_FILTER = `
  <filter id="nodeShadow" x="-10%" y="-10%" width="130%" height="140%">
    <feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="rgba(0,0,0,0.04)" />
  </filter>
  <filter id="nodeShadowHover" x="-10%" y="-10%" width="130%" height="150%">
    <feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="rgba(0,0,0,0.08)" />
  </filter>
`;

/* ─── main canvas ─── */
window.WorkflowCanvas = function WorkflowCanvas({ progress, hideNodeLabels, customPools, customLanes, customNodes, customEdges, customViewBox }) {
  const activePools = customPools || POOLS;
  const activeLanes = customLanes || LANES;
  const activeEdges = customEdges || EDGES;
  const [nodes, setNodes] = useState(customNodes || INITIAL_NODES);
  const [selected, setSelected] = useState(null);
  const [hovered, setHovered] = useState(null);
  const [hoverPos, setHoverPos] = useState({ x: 0, y: 0 });
  const dragRef = useRef(null);
  const svgRef = useRef(null);

  const [vp, setVp] = useState({ w: window.innerWidth, h: window.innerHeight });
  useEffect(() => {
    const fn = () => setVp({ w: window.innerWidth, h: window.innerHeight });
    window.addEventListener('resize', fn);
    return () => window.removeEventListener('resize', fn);
  }, []);

  // Camera: zoomed in so fewer elements are visible at once, pans down as you scroll
  const viewBox = useMemo(() => {
    if (customViewBox) return customViewBox;
    const aspect = vp.w / vp.h;
    const vbH = 650;
    const vbW = vbH * aspect;
    const maxPanY = 1000 - vbH + 200;
    const panY = -80 + progress * Math.max(maxPanY, 0);
    const panX = (1600 - vbW) / 2 - 40;
    return `${panX} ${panY} ${vbW} ${vbH}`;
  }, [vp, progress, customViewBox]);

  const nodeMap = useMemo(() => {
    const m = {};
    nodes.forEach(n => m[n.id] = n);
    return m;
  }, [nodes]);

  // ── Drag ──
  const onPointerDown = useCallback((e, id) => {
    e.stopPropagation();
    const svg = svgRef.current;
    const pt = svg.createSVGPoint();
    pt.x = e.clientX; pt.y = e.clientY;
    const svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
    const node = nodes.find(n => n.id === id);
    if (!node) return;
    dragRef.current = { id, ox: svgP.x - node.x, oy: svgP.y - node.y };
    setSelected(id);
  }, [nodes]);

  const onPointerMove = useCallback((e) => {
    if (!dragRef.current) return;
    const svg = svgRef.current;
    const pt = svg.createSVGPoint();
    pt.x = e.clientX; pt.y = e.clientY;
    const svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
    const { id, ox, oy } = dragRef.current;
    setNodes(prev => prev.map(n => n.id === id ? { ...n, x: svgP.x - ox, y: svgP.y - oy } : n));
  }, []);

  const onPointerUp = useCallback(() => { dragRef.current = null; }, []);

  const onHover = useCallback((e, node) => {
    setHovered(node);
    // Compute the node's screen-space bounding box so the tooltip can avoid overlapping it
    const svg = svgRef.current;
    if (svg) {
      const ctm = svg.getScreenCTM();
      const w = nodeW(node), h = nodeH(node);
      const pt1 = svg.createSVGPoint();
      pt1.x = node.x; pt1.y = node.y;
      const s1 = pt1.matrixTransform(ctm);
      const pt2 = svg.createSVGPoint();
      pt2.x = node.x + w; pt2.y = node.y + h;
      const s2 = pt2.matrixTransform(ctm);
      setHoverPos({ nodeLeft: s1.x, nodeTop: s1.y, nodeRight: s2.x, nodeBottom: s2.y });
    } else {
      setHoverPos({ nodeLeft: e.clientX, nodeTop: e.clientY, nodeRight: e.clientX, nodeBottom: e.clientY });
    }
  }, []);
  const onHoverEnd = useCallback(() => setHovered(null), []);

  // Clear hover on scroll, keydown, or click so tooltips don't stick
  useEffect(() => {
    const clearHover = () => setHovered(null);
    window.addEventListener('scroll', clearHover, { passive: true });
    window.addEventListener('keydown', clearHover, { passive: true });
    window.addEventListener('click', clearHover, { passive: true });
    return () => {
      window.removeEventListener('scroll', clearHover);
      window.removeEventListener('keydown', clearHover);
      window.removeEventListener('click', clearHover);
    };
  }, []);

  // Clear hover when the canvas becomes hidden (slide change)
  useEffect(() => {
    const svg = svgRef.current;
    if (!svg) return;
    const observer = new IntersectionObserver(([entry]) => {
      if (!entry.isIntersecting) setHovered(null);
    }, { threshold: 0 });
    observer.observe(svg);
    return () => observer.disconnect();
  }, []);

  // ── Pools — exact match: 2px solid rgba(0,0,0,0.1), radius-lg (12px),
  //    left label strip 32px wide with rgba(0,0,0,0.025) bg,
  //    vertical text: 12px/700/uppercase/0.5px spacing, color --text-secondary
  //    border-right on strip: 1px solid rgba(0,0,0,0.06) ──
  const renderPools = useMemo(() => {
    return activePools.map(pool => {
      const a = revealAlpha(progress, pool.r);
      if (a <= 0) return null;
      const labelStripW = 32;
      return (
        <g key={pool.id} opacity={a}>
          {/* Pool outer border */}
          <rect x={POOL_X} y={pool.y} width={POOL_W} height={pool.h}
            rx="12" fill="none" stroke="rgba(0,0,0,0.1)" strokeWidth="2" />
          {/* Label strip background — clipped to left side with matching radius */}
          <clipPath id={`pool-clip-${pool.id}`}>
            <rect x={POOL_X} y={pool.y} width={labelStripW} height={pool.h} rx="12" />
          </clipPath>
          <rect x={POOL_X} y={pool.y} width={labelStripW} height={pool.h}
            fill="rgba(0,0,0,0.025)" clipPath={`url(#pool-clip-${pool.id})`} />
          {/* Label strip right border */}
          <line x1={POOL_X + labelStripW} y1={pool.y + 1} x2={POOL_X + labelStripW} y2={pool.y + pool.h - 1}
            stroke="rgba(0,0,0,0.06)" strokeWidth="1" />
          {/* Vertical label — writing-mode: vertical-rl + rotate(180deg) = reading bottom-to-top */}
          <g transform={`translate(${POOL_X + labelStripW / 2}, ${pool.y + pool.h / 2})`}>
            <text
              fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
              fontSize="12" fontWeight="700" textAnchor="middle" dominantBaseline="central"
              letterSpacing="0.5" fill={T.inkSecondary}
              transform="rotate(-90)">
              {pool.name.toUpperCase()}
            </text>
          </g>
        </g>
      );
    });
  }, [progress]);

  // ── Lanes — exact match from LaneNode.tsx + nodes.css:
  //    border: 1px dashed rgba(0,0,0,0.08), radius-sm (6px),
  //    bg: pastel color at 0.06 alpha (from LANE_PASTEL_COLORS),
  //    border-color: pastel at 0.18 alpha (from LANE_BORDER_COLORS),
  //    label pill: absolute top-left (10px, 8px), 11px/600/uppercase/0.3px,
  //    color --text-tertiary (#aeaeb2), bg white, border 1px rgba(0,0,0,0.04), radius-sm ──
  const LANE_BG_COLORS = [
    'rgba(88,126,194,0.06)',   // blue
    'rgba(95,169,126,0.06)',   // green
    'rgba(150,120,182,0.06)',  // purple
    'rgba(212,145,90,0.06)',   // orange
    'rgba(201,107,104,0.06)',  // red
  ];
  const LANE_BORDER_COLORS = [
    'rgba(88,126,194,0.18)',
    'rgba(95,169,126,0.18)',
    'rgba(150,120,182,0.18)',
    'rgba(212,145,90,0.18)',
    'rgba(201,107,104,0.18)',
  ];
  // Map lane colors to palette index
  const laneColorIdx = (color) => {
    if (color === T.blue)   return 0;
    if (color === T.green)  return 1;
    if (color === T.purple) return 2;
    if (color === T.orange) return 3;
    if (color === T.red)    return 4;
    return 0;
  };

  const renderLanes = useMemo(() => {
    return activeLanes.map(lane => {
      const a = revealAlpha(progress, lane.r);
      if (a <= 0) return null;
      const ci = laneColorIdx(lane.color);
      const bg = LANE_BG_COLORS[ci];
      const bc = LANE_BORDER_COLORS[ci];
      const pillW = lane.name.length * 7.2 + 20;
      return (
        <g key={lane.id} opacity={a}>
          {/* Lane background — pastel fill + dashed border */}
          <rect x={LANE_X} y={lane.y} width={LANE_W} height={lane.h}
            rx="6" fill={bg} stroke={bc} strokeWidth="1" strokeDasharray="4 3" />
          {/* Label pill — white bg, subtle border, top-left corner */}
          <rect x={LANE_X + 10} y={lane.y + 8} width={pillW} height="18"
            rx="6" fill={T.surface} stroke="rgba(0,0,0,0.04)" strokeWidth="1" />
          <text x={LANE_X + 10 + 8} y={lane.y + 20.5}
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="11" fontWeight="600" letterSpacing="0.3"
            fill={T.inkTertiary}>
            {lane.name.toUpperCase()}
          </text>
        </g>
      );
    });
  }, [progress]);

  // ── Edges — stroke rgba(0,0,0,0.1) / 1px, selected: blue 1.5px ──
  const renderEdges = useMemo(() => {
    return activeEdges.map((edge, i) => {
      const from = nodeMap[edge.from];
      const to = nodeMap[edge.to];
      if (!from || !to) return null;
      const edgeR = Math.max(from.r || 0, to.r || 0);
      const a = revealAlpha(progress, edgeR);
      if (a <= 0) return null;
      const d = edgePath(from, to);
      const isHL = selected === edge.from || selected === edge.to;
      const lp = edgeLabelPos(from, to);
      return (
        <g key={`e-${i}`} opacity={a}>
          <path d={d} fill="none"
            stroke={isHL ? T.blue : 'rgba(0,0,0,0.1)'}
            strokeWidth={isHL ? 1.5 : 1}
            markerEnd={isHL ? 'url(#arrowBlue)' : 'url(#arrow)'} />
          {edge.label && !hideNodeLabels && (
            <g>
              <rect x={lp.x - 16} y={lp.y - 8} width="32" height="15" rx="3"
                fill={T.surface} stroke={T.borderPrimary} strokeWidth="0.5" />
              <text x={lp.x} y={lp.y + 3.5} textAnchor="middle"
                fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
                fontSize="9" fontWeight="500" fill={T.inkSecondary}>{edge.label}</text>
            </g>
          )}
        </g>
      );
    });
  }, [nodeMap, selected, progress]);

  // ── Render a single node ──
  const renderNode = useCallback((node) => {
    const a = revealAlpha(progress, node.r);
    if (a <= 0) return null;
    const isSel = selected === node.id;
    const isHov = hovered && hovered.id === node.id;
    const evts = {
      style: { cursor: 'grab' },
      onPointerDown: (e) => onPointerDown(e, node.id),
      onPointerEnter: (e) => onHover(e, node),
      onPointerLeave: onHoverEnd,
    };

    /* ── Event: Start ── */
    if (node.type === 'event-start') {
      const cx = node.x + EVENT_R, cy = node.y + EVENT_R;
      return (
        <g key={node.id} opacity={a} {...evts}>
          <circle cx={cx} cy={cy} r={EVENT_R}
            fill={T.surface} stroke={T.green}
            strokeWidth={isSel ? 2 : 1.2}
            filter="url(#nodeShadow)" />
          {!hideNodeLabels && <text x={cx} y={cy + EVENT_R + 12} textAnchor="middle"
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="9" fontWeight="600" fill={T.ink}>
            {node.label}
          </text>}
        </g>
      );
    }

    /* ── Event: End ── */
    if (node.type === 'event-end') {
      const cx = node.x + EVENT_R, cy = node.y + EVENT_R;
      return (
        <g key={node.id} opacity={a} {...evts}>
          <circle cx={cx} cy={cy} r={EVENT_R}
            fill={T.surface} stroke={T.red}
            strokeWidth={isSel ? 3 : 2.5}
            filter="url(#nodeShadow)" />
          {!hideNodeLabels && <text x={cx} y={cy + EVENT_R + 12} textAnchor="middle"
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="9" fontWeight="600" fill={T.ink}>
            {node.label}
          </text>}
        </g>
      );
    }

    /* ── Event: Timer ── */
    if (node.type === 'event-timer') {
      const cx = node.x + EVENT_R, cy = node.y + EVENT_R;
      return (
        <g key={node.id} opacity={a} {...evts}>
          <circle cx={cx} cy={cy} r={EVENT_R}
            fill={T.surface} stroke={T.purple} strokeWidth="1.5"
            filter="url(#nodeShadow)" />
          <circle cx={cx} cy={cy} r={EVENT_R - 3}
            fill="none" stroke={T.purple} strokeWidth="0.7" />
          <circle cx={cx} cy={cy} r="4" fill="none" stroke={T.purple} strokeWidth="0.8" />
          <path d={`M${cx},${cy - 3} L${cx},${cy} L${cx + 2},${cy + 1}`}
            stroke={T.purple} strokeWidth="1" strokeLinecap="round" fill="none" />
          {!hideNodeLabels && <text x={cx} y={cy + EVENT_R + 12} textAnchor="middle"
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="9" fontWeight="600" fill={T.ink}>
            {node.label}
          </text>}
        </g>
      );
    }

    /* ── Event: Interruption ── */
    if (node.type === 'event-interruption') {
      const cx = node.x + EVENT_R, cy = node.y + EVENT_R;
      return (
        <g key={node.id} opacity={a} {...evts}>
          <circle cx={cx} cy={cy} r={EVENT_R}
            fill={T.surface} stroke={T.orange} strokeWidth="1.5"
            strokeDasharray="3 2" filter="url(#nodeShadow)" />
          <path d={`M${cx + 0.5},${cy - 5} L${cx - 2},${cy + 0.5} L${cx + 0.5},${cy + 0.5} L${cx - 0.5},${cy + 5} L${cx + 2},${cy - 0.5} L${cx - 0.5},${cy - 0.5} Z`}
            stroke={T.orange} strokeWidth="0.9" fill="none" strokeLinejoin="round" />
          {!hideNodeLabels && <text x={cx} y={cy + EVENT_R + 12} textAnchor="middle"
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="9" fontWeight="600" fill={T.ink}>
            {node.label}
          </text>}
        </g>
      );
    }

    /* ── Gateway — diamond ── */
    if (node.type === 'gateway') {
      const cx = node.x + GATEWAY_SIZE / 2;
      const cy = node.y + GATEWAY_SIZE / 2;
      const half = GATEWAY_SIZE / 2;
      const pts = `${cx},${cy - half} ${cx + half},${cy} ${cx},${cy + half} ${cx - half},${cy}`;
      return (
        <g key={node.id} opacity={a} {...evts}>
          <polygon points={pts}
            fill={T.surface} stroke={T.orange}
            strokeWidth={isSel ? 2 : 1.5}
            filter="url(#nodeShadow)" />
          <text x={cx} y={cy + 5} textAnchor="middle"
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="16" fontWeight="700" fill={T.orange}>
            {'\u00D7'}
          </text>
          {!hideNodeLabels && <text x={cx} y={cy + half + 12} textAnchor="middle"
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="9" fontWeight="600" fill={T.ink}>
            {node.label}
          </text>}
          {!hideNodeLabels && isHov && <text x={cx} y={cy + half + 22} textAnchor="middle"
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="7" fontWeight="500" letterSpacing="0.3"
            fill={T.inkTertiary}>
            EXCLUSIVE
          </text>}
        </g>
      );
    }

    /* ── Data — document shape with folded corner ── */
    if (node.type === 'data') {
      const dx = node.x, dy = node.y;
      const w = DATA_ICON_W, h = DATA_ICON_H;
      const fold = 6;
      const path = `M${dx + 1},${dy + 1} L${dx + w - fold},${dy + 1} L${dx + w},${dy + 1 + fold} L${dx + w},${dy + h} L${dx + 1},${dy + h} Z`;
      const foldPath = `M${dx + w - fold},${dy + 1} L${dx + w - fold},${dy + 1 + fold} L${dx + w},${dy + 1 + fold}`;
      const cx = dx + w / 2;
      return (
        <g key={node.id} opacity={a} {...evts}>
          <path d={path} fill={T.surface} stroke={T.blue} strokeWidth="1.2"
            filter="url(#nodeShadow)" />
          <path d={foldPath} fill="none" stroke={T.blue} strokeWidth="1.2" />
          {!hideNodeLabels && <text x={cx} y={dy + h + 12} textAnchor="middle"
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="9" fontWeight="600" fill={T.ink}>
            {node.label}
          </text>}
          {!hideNodeLabels && isHov && node.state && (
            <text x={cx} y={dy + h + 22} textAnchor="middle"
              fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
              fontSize="7.5" fontWeight="500" fill={T.blue}>
              [{node.state}]
            </text>
          )}
        </g>
      );
    }

    /* ── Subprocess — rounded rect, 2px gray border ── */
    if (node.type === 'subprocess') {
      const tw = node.w || 140;
      const th = node.desc ? TASK_H : 42;
      return (
        <g key={node.id} opacity={a} {...evts}>
          <rect x={node.x} y={node.y} width={tw} height={th} rx="8"
            fill={T.surface} stroke={isSel ? T.blue : T.inkSecondary}
            strokeWidth="1.5" filter="url(#nodeShadow)" />
          <line x1={node.x} y1={node.y + 20} x2={node.x + tw} y2={node.y + 20}
            stroke={T.borderSecondary} strokeWidth="1" />
          <rect x={node.x + 7} y={node.y + 5} width="9" height="9" rx="1.5"
            stroke="#636366" strokeWidth="1" fill="none" />
          <line x1={node.x + 9.5} y1={node.y + 9.5} x2={node.x + 13.5} y2={node.y + 9.5}
            stroke="#636366" strokeWidth="1" strokeLinecap="round" />
          <line x1={node.x + 11.5} y1={node.y + 7.5} x2={node.x + 11.5} y2={node.y + 11.5}
            stroke="#636366" strokeWidth="1" strokeLinecap="round" />
          {!hideNodeLabels && isHov && <text x={node.x + 20} y={node.y + 13}
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="7" fontWeight="600" letterSpacing="0.3"
            fill={T.inkSecondary}>
            SUB-PROCESS
          </text>}
          {!hideNodeLabels && <text x={node.x + 7} y={node.y + 34}
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="9" fontWeight="600" fill={T.ink}>
            {node.label}
          </text>}
          {!hideNodeLabels && isHov && node.desc && (
            <text x={node.x + 7} y={node.y + 46}
              fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
              fontSize="7.5" fontWeight="400" fill={T.inkSecondary}>
              {node.desc}
            </text>
          )}
        </g>
      );
    }

    /* ── Task node — rounded rect with header/body split + description ── */
    const tw = node.w || 140;
    const th = node.desc ? TASK_H : 42;
    return (
      <g key={node.id} opacity={a} {...evts}>
        {/* Card background */}
        <rect x={node.x} y={node.y} width={tw} height={th} rx="8"
          fill={T.surface}
          stroke={isSel ? T.blue : T.borderPrimary}
          strokeWidth={isSel ? 1.2 : 0.8}
          filter={isHov ? 'url(#nodeShadowHover)' : 'url(#nodeShadow)'} />
        {/* Header separator */}
        <line x1={node.x} y1={node.y + 20} x2={node.x + tw} y2={node.y + 20}
          stroke={T.borderSecondary} strokeWidth="0.8" />
        {/* Header — icon + type label */}
        {!hideNodeLabels && <StepIcon subtype={node.subtype} x={node.x + 7} y={node.y + 5} />}
        {!hideNodeLabels && <text x={node.x + 20} y={node.y + 13}
          fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
          fontSize="7" fontWeight="600" letterSpacing="0.3"
          fill={T.inkSecondary}>
          {typeLabel(node.subtype)}
        </text>}
        {/* Body — title */}
        {!hideNodeLabels && <text x={node.x + 7} y={node.y + 34}
          fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
          fontSize="9" fontWeight="600" fill={T.ink}>
          {node.label.length > Math.floor(tw / 5.8) ? node.label.slice(0, Math.floor(tw / 5.8) - 1) + '\u2026' : node.label}
        </text>}
        {/* Description preview (shown on hover) */}
        {!hideNodeLabels && isHov && node.desc && (
          <text x={node.x + 7} y={node.y + 46}
            fontFamily="-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif"
            fontSize="7.5" fontWeight="400" fill={T.inkSecondary}>
            {node.desc.length > Math.floor(tw / 4.5) ? node.desc.slice(0, Math.floor(tw / 4.5) - 1) + '\u2026' : node.desc}
          </text>
        )}
        {/* Selected ring */}
        {isSel && (
          <rect x={node.x - 1} y={node.y - 1} width={tw + 2} height={th + 2} rx="9"
            fill="none" stroke={T.blue} strokeWidth="1" />
        )}
      </g>
    );
  }, [selected, hovered, progress, hideNodeLabels, onPointerDown, onHover, onHoverEnd]);

  return (
    <React.Fragment>
      <svg ref={svgRef} viewBox={viewBox}
        style={{ width: '100%', height: '100%', display: 'block', opacity: 0.55 }}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
        onPointerLeave={() => { onPointerUp(); setHovered(null); }}
        onClick={(e) => {
          if (e.target === svgRef.current ||
            (e.target.tagName === 'rect' && e.target.getAttribute('fill') === 'url(#dots)'))
            setSelected(null);
        }}>
        <defs>
          <pattern id="dots" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
            <circle cx="12" cy="12" r="0.7" fill={T.dots} />
          </pattern>
          {/* Edge arrow — rgba(0,0,0,0.1) */}
          <marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
            <path d="M0,0 L8,3 L0,6 Z" fill="rgba(0,0,0,0.25)" />
          </marker>
          {/* Selected edge arrow — blue */}
          <marker id="arrowBlue" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
            <path d="M0,0 L8,3 L0,6 Z" fill={T.blue} />
          </marker>
          {/* Node shadows */}
          <filter id="nodeShadow" x="-10%" y="-10%" width="130%" height="140%">
            <feDropShadow dx="0" dy="1" stdDeviation="2" floodColor="rgba(0,0,0,0.04)" />
          </filter>
          <filter id="nodeShadowHover" x="-10%" y="-10%" width="130%" height="150%">
            <feDropShadow dx="0" dy="4" stdDeviation="8" floodColor="rgba(0,0,0,0.08)" />
          </filter>
        </defs>
        <rect x="-2000" y="-2000" width="6000" height="6000" fill="url(#dots)" />
        {renderPools}
        {renderLanes}
        {renderEdges}
        {nodes.filter(n => !hovered || n.id !== hovered.id).map(renderNode)}
        {hovered && renderNode(nodes.find(n => n.id === hovered.id))}
      </svg>
      {!hideNodeLabels && <Tooltip node={hovered} pos={hoverPos} />}
    </React.Fragment>
  );
};
