/* Rinus hero pipeline — v4.
   960×540 native canvas.
   Layout: Lead Generation · Enrichment · Rinus (agents) · Outreach · Tasks.
   Pace halved versus v3 — cards linger long enough to read.
   Editorial register: pitch-anchored, signal-orange used as the single pulse. */

const { useEffect, useRef, useState } = React;

const PIPELINE_TOKENS = {
  PITCH_950: "var(--pitch-950)",
  PITCH_900: "var(--pitch-900)",
  PITCH_800: "var(--pitch-800)",
  PITCH_700: "var(--pitch-700)",
  PITCH_500: "var(--pitch-500)",
  PITCH_400: "var(--pitch-400)",
  PITCH_300: "var(--pitch-300)",
  PITCH_200: "var(--pitch-200)",
  PITCH_100: "var(--pitch-100)",
  PAPER_50:  "var(--paper-50)",
  PAPER_100: "var(--paper-100)",
  PAPER_200: "var(--paper-200)",
  PAPER_300: "var(--paper-300)",
  PAPER_400: "var(--paper-400)",
  PAPER_500: "var(--paper-500)",
  INK:       "var(--ink)",
  INK_MUTED: "var(--ink-muted)",
  INK_SUBTLE:"var(--ink-subtle)",
  LINE:      "var(--line)",
  LINE_STRONG:"var(--line-strong)",
  PATINA:    "var(--patina)",
  SIGNAL:    "var(--signal)",
  SIGNAL_INK:"var(--signal-ink)",
  MONO:      "var(--font-mono)",
  SANS:      "var(--font-sans)",
  DISPLAY:   "var(--font-display)",
};
const T = PIPELINE_TOKENS;

const Easing = {
  easeInCubic: (t) => t * t * t,
  easeOutCubic: (t) => --t * t * t + 1,
  easeInOutCubic: (t) => t < 0.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
  easeOutBack: (t) => { const c1=1.70158, c3=c1+1; return 1+c3*Math.pow(t-1,3)+c1*Math.pow(t-1,2); },
};
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));

/* ── Layout (5 lanes: lead-gen · enrich · Rinus · outreach · tasks) ── */
const LAYOUT = {
  topY: 22,
  labelsY: 54,
  headerLineY: 74,
  bottomLineY: 478,
  statsY: 504,

  // Column geometry (x = left, cx = center, w = width)
  cLG:  { x: 24,  cx: 102, w: 156 },
  cEN:  { x: 196, cx: 274, w: 156 },
  cRN:  { x: 376, cx: 480, w: 208 },
  cOR:  { x: 608, cx: 686, w: 156 },
  cTK:  { x: 780, cx: 858, w: 156 },

  // 6 row centers — column body 90..456 → row-h = 61
  rowY: [115, 176, 237, 298, 359, 420],
  cardW: 152,
  cardH: 54,

  agentCx: 480,
  agentCy: 276,
  agentR: 56,
};

/* ── Timing — v5: 60% más rápido que v4, 13 leads ──────────────────── */
const CYCLE = 37;
const STAGGER = 1.8;

function leadTimes(i) {
  const t0 = 0.28 + i * STAGGER;
  return {
    enter:     t0,
    inGen:     t0 + 0.39,
    moveEnr:   t0 + 1.57,
    inEnr:     t0 + 2.16,
    moveRin:   t0 + 4.23,
    atRinus:   t0 + 4.82,
    routeOut:  t0 + 5.12,
    inOut:     t0 + 5.70,
    emitTask:  t0 + 6.19,
    inTask:    t0 + 6.39,
    fadeStart: t0 + 16.0,
  };
}

/* ── Lead roster (6) ── nombres latinoamericanos ────────────────────── */
const LEADS = [
  {
    id: 1, name: "Clara Torres", co: "TechBA Labs", role: "VP Ingeniería", size: "420",
    source: { kind: "signal",   sub: "role-change", label: "Nuevo rol", icon: "briefcase", color: T.PITCH_400 },
    enrich: { hq: "Buenos Aires · AR", tech: "AWS · Postgres", funding: "Serie B · $48M" },
    qualified: true,
    channel: "linkedin",
    message: "Clara, felicitaciones por el nuevo rol. ¿15 min la semana que viene para comparar notas?",
    task: { kind: "meeting",  label: "Demo agendada",    detail: "Mar 10:00 · 30 min" },
  },
  {
    id: 2, name: "Pedro Sánchez", co: "Clarotek", role: "Founder", size: "8",
    source: { kind: "inbound",  sub: "pricing",     label: "Visita precios", icon: "eye",      color: T.PITCH_500 },
    enrich: { hq: "Ciudad de México · MX", tech: "Next · Vercel", funding: "Bootstrapped" },
    qualified: false,
    channel: "email",
    message: null,
    task: { kind: "nurture",  label: "Sumado a nurture",   detail: "Drip SMB · 60 días" },
  },
  {
    id: 3, name: "Tamara Villanueva", co: "SaludNet", role: "CTO", size: "210",
    source: { kind: "outbound", sub: "icp-match",   label: "Match ICP",    icon: "swap",      color: T.PITCH_700 },
    enrich: { hq: "Santiago · CL", tech: "Azure · .NET", funding: "Serie A · $14M" },
    qualified: true,
    channel: "email",
    message: "Tamara, vi el stack de SaludNet. Una nota rápida sobre cómo Rinus encaja con tu equipo.",
    task: { kind: "crm",      label: "Registrado en CRM",     detail: "Etapa: Discovery · Rodrigo V." },
  },
  {
    id: 4, name: "Fabricio Mendoza", co: "Carga Sur", role: "Director de Ops", size: "650",
    source: { kind: "signal",   sub: "funding",     label: "Ronda de inv.", icon: "spark",    color: T.SIGNAL },
    enrich: { hq: "Bogotá · CO", tech: "SAP · Snowflake", funding: "Serie B · $48M" },
    qualified: true,
    channel: "email",
    message: "Fabricio, felicitaciones por la ronda. ¿15 min para mapear las ops post-inversión?",
    task: { kind: "followup", label: "Seguimiento en cola",  detail: "Jue 14:00 · revisar respuesta" },
  },
  {
    id: 5, name: "Santos Romero", co: "Pixel Libre", role: "Estudiante", size: "1",
    source: { kind: "inbound",  sub: "pricing",     label: "Visita precios", icon: "eye",      color: T.PITCH_500 },
    enrich: { hq: "Montevideo · UY", tech: "Solo · Hobby", funding: "Sin financiamiento" },
    qualified: false,
    channel: "email",
    message: null,
    task: { kind: "snooze",   label: "Fuera de ICP", detail: "Solo, menos de 5 usuarios" },
  },
  {
    id: 6, name: "Valentina Cruz", co: "Cobalt & Cía.", role: "Head de Ventas", size: "180",
    source: { kind: "outbound", sub: "icp-match",   label: "Match ICP",    icon: "swap",      color: T.PITCH_700 },
    enrich: { hq: "Lima · PE", tech: "GCP · Python", funding: "Serie A · $9M" },
    qualified: true,
    channel: "email",
    message: "Valentina, Cobalt es exactamente el tipo de equipo para el que construimos Rinus. ¿Viernes 09:30?",
    task: { kind: "monitor",  label: "Monitoreando respuesta",     detail: "Esperando confirmación · 48h" },
  },
  {
    id: 7, name: "Lucía Fernández", co: "FactorHR", role: "VP de Operaciones", size: "340",
    source: { kind: "signal",   sub: "role-change", label: "Nuevo rol",    icon: "briefcase", color: T.PITCH_400 },
    enrich: { hq: "Buenos Aires · AR", tech: "Salesforce · Slack", funding: "Serie A · $22M" },
    qualified: true,
    channel: "email",
    message: "Lucía, vi tu cambio de rol en FactorHR. ¿15 min para conectar esta semana?",
    task: { kind: "meeting",  label: "Demo agendada",    detail: "Mié 11:00 · 30 min" },
  },
  {
    id: 8, name: "Martín Acosta", co: "DevFlow", role: "Desarrollador", size: "3",
    source: { kind: "inbound",  sub: "pricing",     label: "Visita precios", icon: "eye",     color: T.PITCH_500 },
    enrich: { hq: "Rosario · AR", tech: "React · Firebase", funding: "Sin financiamiento" },
    qualified: false,
    channel: "email",
    message: null,
    task: { kind: "snooze",   label: "Fuera de ICP", detail: "Solo, menos de 5 usuarios" },
  },
  {
    id: 9, name: "Andrea Gutiérrez", co: "PharmaLink", role: "Gerente Comercial", size: "280",
    source: { kind: "outbound", sub: "icp-match",   label: "Match ICP",    icon: "swap",      color: T.PITCH_700 },
    enrich: { hq: "Guadalajara · MX", tech: "HubSpot · Intercom", funding: "Serie B · $31M" },
    qualified: true,
    channel: "linkedin",
    message: "Andrea, PharmaLink encaja con lo que Rinus automatiza. ¿Hablamos el viernes?",
    task: { kind: "crm",      label: "Registrado en CRM",     detail: "Etapa: Discovery · Sara P." },
  },
  {
    id: 10, name: "Felipe Ríos", co: "CloudBase", role: "CTO", size: "190",
    source: { kind: "signal",   sub: "funding",     label: "Ronda de inv.", icon: "spark",    color: T.SIGNAL },
    enrich: { hq: "Bogotá · CO", tech: "AWS · Kubernetes", funding: "Serie A · $18M" },
    qualified: true,
    channel: "email",
    message: "Felipe, felicitaciones por la ronda. Veamos cómo Rinus puede escalar tu GTM.",
    task: { kind: "followup", label: "Seguimiento en cola",  detail: "Lun 09:00 · revisar respuesta" },
  },
  {
    id: 11, name: "Natalia López", co: "SoloApp", role: "Freelancer", size: "1",
    source: { kind: "inbound",  sub: "pricing",     label: "Visita precios", icon: "eye",     color: T.PITCH_500 },
    enrich: { hq: "Medellín · CO", tech: "WordPress", funding: "Sin financiamiento" },
    qualified: false,
    channel: "email",
    message: null,
    task: { kind: "nurture",  label: "Sumado a nurture",   detail: "Drip SMB · 60 días" },
  },
  {
    id: 12, name: "Diego Vargas", co: "LogisCorp", role: "Director de Ventas", size: "520",
    source: { kind: "outbound", sub: "icp-match",   label: "Match ICP",    icon: "swap",      color: T.PITCH_700 },
    enrich: { hq: "Lima · PE", tech: "SAP · Pipedrive", funding: "Serie B · $55M" },
    qualified: true,
    channel: "email",
    message: "Diego, encontré LogisCorp buscando equipos de ventas a escalar. ¿15 min?",
    task: { kind: "monitor",  label: "Monitoreando respuesta",     detail: "Esperando confirmación · 72h" },
  },
  {
    id: 13, name: "Sofía Morales", co: "EdPlatform", role: "Head de Marketing", size: "160",
    source: { kind: "signal",   sub: "role-change", label: "Nuevo rol",    icon: "briefcase", color: T.PITCH_400 },
    enrich: { hq: "Santiago · CL", tech: "Marketo · Intercom", funding: "Serie A · $12M" },
    qualified: true,
    channel: "linkedin",
    message: "Sofía, tu perfil me llegó hoy. Creo que Rinus puede ahorrarle tiempo a tu equipo.",
    task: { kind: "meeting",  label: "Demo agendada",    detail: "Jue 14:00 · 30 min" },
  },
  {
    id: 14, name: "Camila Reyes", co: "InsuraTech", role: "CEO", size: "95",
    source: { kind: "signal",   sub: "funding",     label: "Ronda de inv.", icon: "spark",    color: T.SIGNAL },
    enrich: { hq: "Buenos Aires · AR", tech: "AWS · Python", funding: "Seed · $3.2M" },
    qualified: true,
    channel: "email",
    message: "Camila, vimos la ronda de InsuraTech. ¿15 min para ver cómo Rinus puede acompañar el crecimiento?",
    task: { kind: "meeting",  label: "Demo agendada",    detail: "Lun 10:00 · 30 min" },
  },
  {
    id: 15, name: "Rafael Mora", co: "BuilderApp", role: "Pasante", size: "2",
    source: { kind: "inbound",  sub: "pricing",     label: "Visita precios", icon: "eye",     color: T.PITCH_500 },
    enrich: { hq: "Bogotá · CO", tech: "PHP · MySQL", funding: "Sin financiamiento" },
    qualified: false,
    channel: "email",
    message: null,
    task: { kind: "snooze",   label: "Fuera de ICP",    detail: "Solo, menos de 5 usuarios" },
  },
  {
    id: 16, name: "Elena Castillo", co: "FinanceHub", role: "VP de Estrategia", size: "310",
    source: { kind: "outbound", sub: "icp-match",   label: "Match ICP",    icon: "swap",      color: T.PITCH_700 },
    enrich: { hq: "Ciudad de México · MX", tech: "Salesforce · Tableau", funding: "Serie B · $38M" },
    qualified: true,
    channel: "linkedin",
    message: "Elena, FinanceHub tiene exactamente el perfil para el que construimos Rinus. ¿Hablamos esta semana?",
    task: { kind: "crm",      label: "Registrado en CRM",     detail: "Etapa: Discovery · Lucas M." },
  },
  {
    id: 17, name: "Tomás Ibáñez", co: "LogisPrime", role: "Director Comercial", size: "430",
    source: { kind: "signal",   sub: "role-change", label: "Nuevo rol",    icon: "briefcase", color: T.PITCH_400 },
    enrich: { hq: "Santiago · CL", tech: "HubSpot · Zapier", funding: "Serie A · $15M" },
    qualified: true,
    channel: "email",
    message: "Tomás, felicitaciones por el nuevo rol. ¿15 min para ver cómo Rinus puede apoyar a tu equipo comercial?",
    task: { kind: "followup", label: "Seguimiento en cola",  detail: "Mar 11:00 · revisar respuesta" },
  },
  {
    id: 18, name: "Pilar Vargas", co: "NutriFlow", role: "Co-founder", size: "12",
    source: { kind: "inbound",  sub: "pricing",     label: "Visita precios", icon: "eye",     color: T.PITCH_500 },
    enrich: { hq: "Montevideo · UY", tech: "Shopify · Klaviyo", funding: "Bootstrapped" },
    qualified: false,
    channel: "email",
    message: null,
    task: { kind: "nurture",  label: "Sumado a nurture",   detail: "Drip SMB · 60 días" },
  },
  {
    id: 19, name: "Marcos Delgado", co: "SecureCloud", role: "Head de Seguridad", size: "550",
    source: { kind: "outbound", sub: "icp-match",   label: "Match ICP",    icon: "swap",      color: T.PITCH_700 },
    enrich: { hq: "Lima · PE", tech: "AWS · Kubernetes", funding: "Serie B · $62M" },
    qualified: true,
    channel: "email",
    message: "Marcos, SecureCloud encaja con los equipos que más usan Rinus. ¿Una llamada rápida esta semana?",
    task: { kind: "monitor",  label: "Monitoreando respuesta",     detail: "Esperando confirmación · 48h" },
  },
  {
    id: 20, name: "Valentina Ortiz", co: "MedSync", role: "CRO", size: "280",
    source: { kind: "signal",   sub: "funding",     label: "Ronda de inv.", icon: "spark",    color: T.SIGNAL },
    enrich: { hq: "Buenos Aires · AR", tech: "Intercom · Segment", funding: "Serie A · $21M" },
    qualified: true,
    channel: "linkedin",
    message: "Valentina, la ronda de MedSync llegó a nuestro radar. ¿15 min para ver cómo Rinus puede acompañar el crecimiento?",
    task: { kind: "meeting",  label: "Demo agendada",    detail: "Jue 09:00 · 30 min" },
  },
];

const SOURCE_META = {
  signal:   { label: "SEÑAL",    color: T.SIGNAL },
  inbound:  { label: "INBOUND",  color: T.PITCH_400 },
  outbound: { label: "OUTBOUND", color: T.PITCH_700 },
};

/* ── Generic chip ─────────────────────────────────────────────────── */
function Pill({ label, value, accent }) {
  return (
    <span style={{
      display: "inline-flex", alignItems: "baseline", gap: 3, padding: "1px 5px",
      background: accent ? `color-mix(in oklch, ${accent} 12%, transparent)` : T.PAPER_100,
      borderRadius: 2, fontFamily: T.MONO, fontSize: 8.5, lineHeight: 1.15,
      border: `1px solid ${accent ? "transparent" : T.LINE}`,
    }}>
      <span style={{ color: T.INK_SUBTLE, textTransform: "uppercase", letterSpacing: "0.08em" }}>{label}</span>
      <span style={{ color: accent || T.INK, fontWeight: 600 }}>{value}</span>
    </span>
  );
}

function CheckIcon({ color, size=8 }) {
  return <svg width={size} height={size} viewBox="0 0 9 9"><path d="M1 4.5 L3.5 7 L8 1.5" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"></path></svg>;
}
function ChannelDot({ channel }) {
  const colors = { email: T.PITCH_700, linkedin: T.PITCH_400, sms: T.PITCH_300 };
  return <span style={{ width: 6, height: 6, borderRadius: 3, background: colors[channel] || T.PITCH_700, display: "inline-block" }}></span>;
}

/* ── Source icons (lead-gen badges) ───────────────────────────────── */
function SourceIcon({ kind, color, size=11 }) {
  const stroke = { stroke: color, strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", fill: "none" };
  if (kind === "briefcase") return <svg width={size} height={size} viewBox="0 0 16 16"><rect x="2" y="5" width="12" height="9" rx="1.5" {...stroke}></rect><path d="M6 5V3.5h4V5" {...stroke}></path><path d="M2 9h12" {...stroke}></path></svg>;
  if (kind === "eye")       return <svg width={size} height={size} viewBox="0 0 16 16"><path d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8 12 12.5 8 12.5 1.5 8 1.5 8z" {...stroke}></path><circle cx="8" cy="8" r="2" {...stroke}></circle></svg>;
  if (kind === "swap")      return <svg width={size} height={size} viewBox="0 0 16 16"><path d="M2 5h10l-2.5-2.5" {...stroke}></path><path d="M14 11H4l2.5 2.5" {...stroke}></path></svg>;
  if (kind === "spark")     return <svg width={size} height={size} viewBox="0 0 16 16"><path d="M8 1.5v3M8 11.5v3M1.5 8h3M11.5 8h3M3.5 3.5l2 2M10.5 10.5l2 2M3.5 12.5l2-2M10.5 5.5l2-2" {...stroke}></path></svg>;
  return null;
}

/* ── Task palette — distinct colors per label ─────────────────────── */
const TASK_META = {
  meeting:  { color: T.SIGNAL,    label: "Reunión",    icon: "cal"   },
  nurture:  { color: T.PITCH_400, label: "Nurture",    icon: "cycle" },
  crm:      { color: T.PITCH_700, label: "CRM",        icon: "db"    },
  monitor:  { color: T.PITCH_300, label: "Monitoreo",  icon: "eye"   },
  followup: { color: "#B98A2A",   label: "Seguimiento",icon: "clock" },
  snooze:   { color: T.PAPER_500, label: "Pospuesto",  icon: "z"     },
};

function TaskIcon({ kind, color }) {
  const s = { stroke: color, strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", fill: "none" };
  if (kind === "cal")   return <svg width="10" height="10" viewBox="0 0 12 12"><rect x="1.5" y="2.5" width="9" height="8" rx="1" {...s}></rect><path d="M4 1.5v2M8 1.5v2M1.5 5h9" {...s}></path></svg>;
  if (kind === "cycle") return <svg width="10" height="10" viewBox="0 0 12 12"><path d="M2 6a4 4 0 0 1 7-2.5M10 6a4 4 0 0 1-7 2.5" {...s}></path><path d="M9 1.5v2H7M3 10.5v-2h2" {...s}></path></svg>;
  if (kind === "db")    return <svg width="10" height="10" viewBox="0 0 12 12"><ellipse cx="6" cy="2.5" rx="4" ry="1.2" {...s}></ellipse><path d="M2 2.5v7c0 .7 1.8 1.2 4 1.2s4-.5 4-1.2v-7M2 6c0 .7 1.8 1.2 4 1.2s4-.5 4-1.2" {...s}></path></svg>;
  if (kind === "clock") return <svg width="10" height="10" viewBox="0 0 12 12"><circle cx="6" cy="6" r="4.2" {...s}></circle><path d="M6 3.5V6l1.8 1" {...s}></path></svg>;
  if (kind === "eye")   return <svg width="10" height="10" viewBox="0 0 12 12"><path d="M1.5 6s1.8-3 4.5-3 4.5 3 4.5 3-1.8 3-4.5 3-4.5-3-4.5-3z" {...s}></path><circle cx="6" cy="6" r="1.4" {...s}></circle></svg>;
  if (kind === "z")     return <svg width="10" height="10" viewBox="0 0 12 12"><path d="M3 3.5h5L3 9h5" {...s}></path></svg>;
  return null;
}

/* ── Column headers + frames ──────────────────────────────────────── */
function StageLabels() {
  const labels = [
    { x: LAYOUT.cLG.cx, text: "GENERACIÓN" },
    { x: LAYOUT.cEN.cx, text: "DATOS" },
    { x: LAYOUT.cRN.cx, text: "Rinus", isRinus: true },
    { x: LAYOUT.cOR.cx, text: "SECUENCIAS" },
    { x: LAYOUT.cTK.cx, text: "TAREAS" },
  ];
  return (
    <React.Fragment>
      {labels.map((l, i) => (
        <div key={i} style={{
          position: "absolute", left: l.x,
          top: l.isRinus ? (LAYOUT.topY + LAYOUT.headerLineY) / 2 : LAYOUT.labelsY,
          transform: l.isRinus ? "translate(-50%, -50%)" : "translateX(-50%)",
          fontFamily: l.isRinus ? T.DISPLAY : T.MONO,
          fontSize: l.isRinus ? 26 : 9,
          letterSpacing: l.isRinus ? "-0.02em" : "0.16em",
          color: l.isRinus ? T.INK : T.INK_SUBTLE,
          fontWeight: l.isRinus ? 600 : 500,
          textTransform: l.isRinus ? "none" : "uppercase",
          whiteSpace: "nowrap",
        }}>{l.text}</div>
      ))}
    </React.Fragment>
  );
}

function Dividers() {
  // Vertical separators between columns.
  const xs = [
    LAYOUT.cLG.x + LAYOUT.cLG.w + 8,  // 188
    LAYOUT.cEN.x + LAYOUT.cEN.w + 8,  // 360
    LAYOUT.cRN.x + LAYOUT.cRN.w + 8,  // 592
    LAYOUT.cOR.x + LAYOUT.cOR.w + 8,  // 772
  ];
  return (
    <React.Fragment>
      {xs.map((x, i) => (
        <div key={i} style={{
          position: "absolute", left: x, top: LAYOUT.headerLineY + 2,
          bottom: 540 - LAYOUT.bottomLineY, width: 1,
          background: `linear-gradient(to bottom, transparent, ${T.LINE} 14%, ${T.LINE} 86%, transparent)`,
        }}></div>
      ))}
      <div style={{ position: "absolute", left: 24, right: 24, top: LAYOUT.headerLineY, height: 1, background: T.INK }}></div>
      <div style={{ position: "absolute", left: 24, right: 24, top: LAYOUT.bottomLineY, height: 1, background: T.LINE }}></div>
    </React.Fragment>
  );
}

/* ── Agent node — five dots, navy, each pulses on its own event ─── */
const RINUS_DOTS = [
  { cx: 14, cy: 14, r: 9 },  // 0: TL — pulses on card entering Generación
  { cx: 62, cy: 14, r: 9 },  // 1: TR — pulses on card arriving at Datos
  { cx: 14, cy: 74, r: 9 },  // 2: BL — pulses when message sent (qualified)
  { cx: 62, cy: 74, r: 9 },  // 3: BR — pulses when task added
];
const RINUS_CENTER = { cx: 38, cy: 44, r: 12 };
const AGENT_NAVY = "#1a2d47";
const PULSE_DUR = 0.6;

function AgentNode({ tCycle, time }) {
  function maxPulse(eventFn) {
    let best = 0;
    for (let i = 0; i < LEADS.length; i++) {
      const dt = tCycle - eventFn(i);
      if (dt >= 0 && dt < PULSE_DUR) best = Math.max(best, 1 - dt / PULSE_DUR);
    }
    return best;
  }
  function maxPulseQualified(eventFn) {
    let best = 0;
    for (let i = 0; i < LEADS.length; i++) {
      if (!LEADS[i].qualified) continue;
      const dt = tCycle - eventFn(i);
      if (dt >= 0 && dt < PULSE_DUR) best = Math.max(best, 1 - dt / PULSE_DUR);
    }
    return best;
  }

  // Per-node pulse intensities
  const p0 = maxPulse(i => leadTimes(i).enter);        // TL: card enters Gen
  const p1 = maxPulse(i => leadTimes(i).inOut);        // TR: card enters Secuencias
  const p2 = maxPulse(i => leadTimes(i).atRinus);      // Center: card enters logo
  const p3 = maxPulse(i => leadTimes(i).inEnr);        // BL: card arrives Datos
  const p4 = maxPulse(i => leadTimes(i).inTask);       // BR: task added
  const dotPulses = [p0, p1, p3, p4];

  const cx = LAYOUT.agentCx, cy = LAYOUT.agentCy;
  const SVG_W = 124, SVG_H = 140;

  return (
    <React.Fragment>
      {/* Faint outer ambient rings */}
      {[0, 1].map((i) => {
        const phase = ((time * 0.28 + i * 0.5) % 1);
        const r = 70 + phase * 70;
        const op = (1 - phase) * 0.12;
        return (
          <div key={i} style={{
            position: "absolute", left: cx, top: cy,
            width: r * 2, height: r * 2,
            transform: "translate(-50%, -50%)",
            borderRadius: "50%", border: `1px solid ${T.PITCH_400}`,
            opacity: op, pointerEvents: "none",
          }}></div>
        );
      })}
      {/* Center-pulse signal halo */}
      <div style={{
        position: "absolute", left: cx, top: cy,
        width: (80 + p2 * 70) * 2,
        height: (80 + p2 * 70) * 2,
        transform: "translate(-50%, -50%)", borderRadius: "50%",
        background: `radial-gradient(circle, color-mix(in oklch, ${T.SIGNAL} ${p2 * 28}%, transparent) 0%, transparent 60%)`,
        pointerEvents: "none",
      }}></div>
      {/* Logo */}
      <div style={{
        position: "absolute", left: cx, top: cy,
        width: SVG_W, height: SVG_H,
        transform: `translate(-50%, -50%) scale(${1 + p2 * 0.06})`,
        transformOrigin: "center",
      }}>
        <svg width={SVG_W} height={SVG_H} viewBox="-12 -12 104 114" style={{ display: "block" }} aria-hidden="true">
          {RINUS_DOTS.map((d, i) => {
            const p = dotPulses[i];
            return (
              <g key={i}>
                {p > 0 && <circle cx={d.cx} cy={d.cy} r={d.r + 7} fill={T.SIGNAL} opacity={p * 0.28}></circle>}
                <circle cx={d.cx} cy={d.cy} r={d.r + p}
                  fill={p > 0.15 ? T.SIGNAL : AGENT_NAVY}
                  style={{ transition: "r 200ms ease-out, fill 200ms ease-out" }}></circle>
              </g>
            );
          })}
          {p2 > 0 && (
            <circle cx={RINUS_CENTER.cx} cy={RINUS_CENTER.cy} r={RINUS_CENTER.r + 7} fill={T.SIGNAL} opacity={p2 * 0.28}></circle>
          )}
          <circle cx={RINUS_CENTER.cx} cy={RINUS_CENTER.cy}
            r={RINUS_CENTER.r + p2}
            fill={T.SIGNAL}
            style={{ transition: "r 200ms ease-out" }}></circle>
        </svg>
      </div>
    </React.Fragment>
  );
}

/* ── Radial Loader — minimal spinning arc for GenCard ────────────── */
function RadialLoader({ time, size = 12, color }) {
  const R = (size - 2) / 2;
  const CIRC = 2 * Math.PI * R;
  const angle = ((time % 0.84) / 0.84) * 360;
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}
      style={{ transform: `rotate(${angle}deg)`, transformOrigin: "center", display: "block", flexShrink: 0 }}>
      <circle cx={size / 2} cy={size / 2} r={R}
        fill="none" stroke={color} strokeWidth="1.5"
        strokeDasharray={`${CIRC * 0.6} ${CIRC * 0.4}`}
        strokeLinecap="round" />
    </svg>
  );
}

/* ── Card components ─────────────────────────────────────────────── */
function GenCard({ lead, x, y, scale, opacity, rotate=0, time=0, loading=false }) {
  const src = SOURCE_META[lead.source.kind];
  return (
    <div style={{
      position: "absolute", left: x, top: y,
      width: LAYOUT.cardW, height: LAYOUT.cardH,
      transform: `translate(-50%, -50%) scale(${scale}) rotate(${rotate}deg)`,
      opacity, background: T.PAPER_50,
      border: `1px solid ${T.LINE_STRONG}`,
      borderLeft: `2px solid ${src.color}`,
      borderRadius: 4, padding: "6px 9px",
      fontFamily: T.SANS, willChange: "transform, opacity",
    }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 6 }}>
        <div style={{ minWidth: 0, flex: 1 }}>
          <div style={{ fontSize: 11, fontWeight: 600, color: T.INK, lineHeight: 1.15, letterSpacing: "-0.005em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{lead.name}</div>
          <div style={{ fontSize: 9.5, color: T.INK_MUTED, marginTop: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{lead.role} · {lead.co}</div>
        </div>
        <span style={{
          fontFamily: T.MONO, fontSize: 7.5, letterSpacing: "0.14em",
          color: src.color, fontWeight: 600, textTransform: "uppercase",
          background: `color-mix(in oklch, ${src.color} 10%, transparent)`,
          padding: "1px 4px", borderRadius: 2, flexShrink: 0,
        }}>{src.label}</span>
      </div>
      <div style={{ display: "flex", gap: 5, marginTop: 5, alignItems: "center", justifyContent: "space-between" }}>
        <span style={{ display: "inline-flex", alignItems: "center", gap: 3 }}>
          <SourceIcon kind={lead.source.icon} color={src.color} size={10} />
          <span style={{ fontFamily: T.MONO, fontSize: 8.5, color: T.INK_SUBTLE, letterSpacing: "0.06em", textTransform: "uppercase", fontWeight: 600 }}>{lead.source.label}</span>
        </span>
        {loading && <RadialLoader time={time} size={10} color={T.SIGNAL} />}
      </div>
    </div>
  );
}

function EnrichCard({ lead, x, y, opacity, enrichP }) {
  const src = SOURCE_META[lead.source.kind];
  // enrichP 0..1 — three rows reveal in turn.
  const r1 = clamp(enrichP / 0.33, 0, 1);
  const r2 = clamp((enrichP - 0.33) / 0.33, 0, 1);
  const r3 = clamp((enrichP - 0.66) / 0.34, 0, 1);
  const Row = ({ p, label, value }) => (
    <div style={{
      display: "flex", justifyContent: "space-between", gap: 6,
      opacity: p, transform: `translateX(${(1 - p) * -4}px)`,
      fontFamily: T.MONO, fontSize: 8.5, lineHeight: 1.25,
      color: T.INK_SUBTLE, letterSpacing: "0.04em",
    }}>
      <span style={{ textTransform: "uppercase" }}>{label}</span>
      <span style={{ color: T.INK, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", textAlign: "right", maxWidth: 100, fontFamily: T.SANS, fontSize: 9.5, letterSpacing: "-0.005em" }}>{value}</span>
    </div>
  );
  const H = 76;
  return (
    <div style={{
      position: "absolute", left: x, top: y,
      width: LAYOUT.cardW, height: H,
      transform: "translate(-50%, -50%)",
      opacity, background: T.PAPER_50,
      border: `1px solid ${T.LINE_STRONG}`,
      borderLeft: `2px solid ${src.color}`,
      borderRadius: 4, padding: "5px 9px 6px",
      fontFamily: T.SANS,
    }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 6, marginBottom: 4 }}>
        <span style={{ fontSize: 11, fontWeight: 600, color: T.INK, lineHeight: 1.15, letterSpacing: "-0.005em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flex: 1 }}>{lead.co}</span>
        <span style={{
          fontFamily: T.MONO, fontSize: 7.5, letterSpacing: "0.14em",
          color: T.PITCH_400, fontWeight: 600, textTransform: "uppercase",
          background: `color-mix(in oklch, ${T.PITCH_400} 12%, transparent)`,
          padding: "1px 4px", borderRadius: 2, flexShrink: 0,
        }}>+Datos</span>
      </div>
      <div style={{ display: "flex", flexDirection: "column", gap: 2, paddingTop: 3, borderTop: `1px dashed ${T.PAPER_300}` }}>
        <Row p={r1} label="HQ" value={lead.enrich.hq} />
        <Row p={r2} label="Stack" value={lead.enrich.tech} />
        <Row p={r3} label="Fund" value={lead.enrich.funding} />
      </div>
    </div>
  );
}

const ICP_LABELS = ["ICP A", "ICP B", "ICP C"];
const ICP_COLORS = [T.PITCH_400, "#B98A2A", "#8B5CF6"];

function OutreachCard({ lead, x, y, sendStart, tCycle, fadeOp }) {
  const dt = tCycle - sendStart;
  const enterT = clamp(dt * 2.5, 0, 1);
  const e = Easing.easeOutCubic(enterT);
  const typeProgress = clamp(dt / 1.4, 0, 1);
  const charCount = Math.floor(lead.message.length * typeProgress);
  const typed = lead.message.slice(0, charCount);
  const showCursor = typeProgress < 1 && Math.floor(dt * 4) % 2 === 0;
  const sent = dt > 1.6;
  const icpIdx = lead.id % 3;
  const icpLabel = ICP_LABELS[icpIdx];
  const icpColor = ICP_COLORS[icpIdx];
  const H = 54;
  return (
    <div style={{
      position: "absolute", left: x, top: y,
      width: LAYOUT.cardW, height: H,
      transform: `translate(-50%, -50%) translateY(${(1 - e) * 4}px)`,
      opacity: e * fadeOp,
      background: T.PAPER_50,
      border: `1px solid ${T.LINE_STRONG}`, borderLeft: `2px solid ${icpColor}`,
      borderRadius: 4, padding: "5px 9px 6px",
      fontFamily: T.SANS, color: T.INK, overflow: "hidden",
    }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 6, marginBottom: 3 }}>
        <span style={{ fontSize: 11, fontWeight: 600, color: T.INK, lineHeight: 1.15, letterSpacing: "-0.005em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flex: 1 }}>{lead.name}</span>
        <span style={{
          fontFamily: T.MONO, fontSize: 7.5, letterSpacing: "0.14em",
          color: icpColor, fontWeight: 600, textTransform: "uppercase",
          background: `color-mix(in oklch, ${icpColor} 12%, transparent)`,
          padding: "1px 4px", borderRadius: 2, flexShrink: 0,
        }}>{icpLabel}</span>
      </div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 3, paddingTop: 3, borderTop: `1px dashed ${T.PAPER_300}` }}>
        <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
          <ChannelDot channel={lead.channel} />
          <span style={{ fontFamily: T.MONO, fontSize: 8, letterSpacing: "0.12em", textTransform: "uppercase", color: sent ? "#2db87a" : T.INK_SUBTLE, fontWeight: 600 }}>{lead.channel} · {sent ? "enviado" : "escribiendo"}</span>
        </span>
        {sent && <CheckIcon color="#2db87a" />}
      </div>
      <div style={{ fontSize: 9.5, color: T.INK, lineHeight: 1.3, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
        {typed}
        {showCursor && <span style={{ color: T.PITCH_400, marginLeft: 1 }}>▍</span>}
      </div>
    </div>
  );
}

function DropCard({ lead, x, y, sendStart, tCycle, fadeOp, dropFadeAge = -1 }) {
  const dt = tCycle - sendStart;
  const enterT = clamp(dt * 2.5, 0, 1);
  const e = Easing.easeOutCubic(enterT);
  const localFade = dropFadeAge > 0 ? Math.max(0, 1 - dropFadeAge / 0.5) : 1;
  const H = 50;
  return (
    <div style={{
      position: "absolute", left: x, top: y,
      width: LAYOUT.cardW, height: H,
      transform: `translate(-50%, -50%) translateY(${(1 - e) * 4}px)`,
      opacity: e * fadeOp * localFade * 0.65,
      background: T.PAPER_100,
      border: `1px dashed ${T.PAPER_400}`,
      borderRadius: 4, padding: "5px 9px 6px",
      fontFamily: T.SANS, color: T.INK_MUTED,
    }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 6 }}>
        <span style={{ fontSize: 10.5, fontWeight: 500, color: T.INK_MUTED, lineHeight: 1.15, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flex: 1 }}>{lead.name}</span>
        <span style={{
          fontFamily: T.MONO, fontSize: 7.5, letterSpacing: "0.14em",
          color: T.PAPER_500, fontWeight: 600, textTransform: "uppercase",
          padding: "1px 4px", borderRadius: 2, flexShrink: 0,
        }}>DESCARTADO</span>
      </div>
      <div style={{ fontFamily: T.MONO, fontSize: 8.5, color: T.INK_SUBTLE, letterSpacing: "0.06em", textTransform: "uppercase", marginTop: 4 }}>
        Fuera del ICP · sin outreach
      </div>
    </div>
  );
}

function TaskCard({ lead, x, y, entry, opacity }) {
  const meta = TASK_META[lead.task.kind] || TASK_META.crm;
  const ty = (1 - entry) * -10;
  const sc = 0.96 + entry * 0.04;
  const H = 60;
  return (
    <div style={{
      position: "absolute", left: x, top: y, width: LAYOUT.cardW, height: H,
      transform: `translate(-50%, -50%) translateY(${ty}px) scale(${sc})`,
      opacity: entry * opacity,
      background: T.PAPER_50,
      border: `1px solid ${T.LINE_STRONG}`,
      borderLeft: `2px solid ${meta.color}`,
      borderRadius: 4, padding: "5px 9px 6px",
      fontFamily: T.SANS,
    }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 6, marginBottom: 3 }}>
        <span style={{ fontSize: 11, fontWeight: 600, color: T.INK, lineHeight: 1.15, letterSpacing: "-0.005em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flex: 1 }}>{lead.task.label}</span>
        <span style={{
          fontFamily: T.MONO, fontSize: 7.5, letterSpacing: "0.14em",
          color: meta.color, fontWeight: 700, textTransform: "uppercase",
          background: `color-mix(in oklch, ${meta.color} 14%, transparent)`,
          padding: "1px 4px", borderRadius: 2, flexShrink: 0,
        }}>{meta.label}</span>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 5, paddingTop: 3, borderTop: `1px dashed ${T.PAPER_300}` }}>
        <TaskIcon kind={meta.icon} color={meta.color} />
        <span style={{ fontFamily: T.MONO, fontSize: 8, letterSpacing: "0.10em", textTransform: "uppercase", color: T.INK_SUBTLE, fontWeight: 600 }}>→ {lead.name.split(" ")[0].replace("Maya","Maya").replace("Theo","Theo").replace("Lina","Lina").replace("Aisha","Aisha").replace("Jonas","Jonas").replace("Priya","Priya")}</span>
      </div>
      <div style={{ fontSize: 9.5, color: T.INK_MUTED, lineHeight: 1.25, marginTop: 3, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{lead.task.detail}</div>
    </div>
  );
}

/* ── Kanban presence functions — continuous 0-1 weight in each column ── */
function lgPresence(i, tc) {
  const Lt = leadTimes(i);
  if (tc < Lt.enter - 0.4) return 0;
  if (tc < Lt.inGen) return Easing.easeOutCubic(clamp((tc - (Lt.enter - 0.4)) / (Lt.inGen - (Lt.enter - 0.4)), 0, 1));
  if (tc < Lt.moveEnr) return 1;
  if (tc < Lt.inEnr) return 1 - Easing.easeInOutCubic(clamp((tc - Lt.moveEnr) / (Lt.inEnr - Lt.moveEnr), 0, 1));
  return 0;
}
function enPresence(i, tc) {
  const Lt = leadTimes(i);
  if (tc < Lt.inEnr) return 0;
  if (tc < Lt.moveRin) return 1;
  if (tc < Lt.atRinus) return 1 - Easing.easeInOutCubic(clamp((tc - Lt.moveRin) / (Lt.atRinus - Lt.moveRin), 0, 1));
  return 0;
}
function orPresence(i, tc) {
  const Lt = leadTimes(i);
  if (tc < Lt.inOut) return 0;
  if (!LEADS[i].qualified) {
    const df = Lt.inOut + 3.0;
    if (tc > df) return Math.max(0, 1 - (tc - df) / 0.5);
  }
  if (tc < Lt.fadeStart) return 1;
  return 1 - clamp((tc - Lt.fadeStart) / 0.8, 0, 1);
}
function tkPresence(i, tc) {
  const Lt = leadTimes(i);
  if (tc < Lt.inTask) return 0;
  if (tc < Lt.fadeStart) return 1;
  return 1 - clamp((tc - Lt.fadeStart) / 0.8, 0, 1);
}
// y-center of lead leadIdx stacked in a column (counts presences of leads above it)
const COL_TOP_Y = 115;
const COL_SPACING = 61;
function getColY(leadIdx, presenceFn, tc, spacing = COL_SPACING) {
  let rank = 0;
  for (let j = 0; j < leadIdx; j++) rank += presenceFn(j, tc);
  return COL_TOP_Y + rank * spacing;
}

/* ── Lead traveler — handles the full lifecycle for each lead ────── */
function LeadTraveler({ tCycle }) {
  return (
    <React.Fragment>
      {LEADS.map((lead, i) => {
        const Lt = leadTimes(i);
        if (tCycle < Lt.enter - 0.4) return null;
        const startX = -120;
        const lgX = LAYOUT.cLG.cx;
        const enX = LAYOUT.cEN.cx;
        const rnX = LAYOUT.agentCx;
        const orX = LAYOUT.cOR.cx;
        const rnY = LAYOUT.agentCy;

        // Phase 1: enter Lead Gen
        if (tCycle < Lt.inGen) {
          const t = clamp((tCycle - (Lt.enter - 0.4)) / (Lt.inGen - (Lt.enter - 0.4)), 0, 1);
          const e = Easing.easeOutCubic(t);
          const x = startX + (lgX - startX) * e;
          const y = getColY(i, lgPresence, tCycle);
          return <GenCard key={lead.id} lead={lead} x={x} y={y} scale={0.85 + 0.15 * e} opacity={e} rotate={(1 - e) * -2} />;
        }
        // Phase 2: sit in Lead Gen (show radial loader while waiting)
        if (tCycle < Lt.moveEnr) {
          const y = getColY(i, lgPresence, tCycle);
          return <GenCard key={lead.id} lead={lead} x={lgX} y={y} scale={1} opacity={1} time={tCycle} loading={true} />;
        }
        // Phase 3: slide Lead Gen → Enrichment (cross-fade at midpoint)
        if (tCycle < Lt.inEnr) {
          const t = clamp((tCycle - Lt.moveEnr) / (Lt.inEnr - Lt.moveEnr), 0, 1);
          const e = Easing.easeInOutCubic(t);
          const x = lgX + (enX - lgX) * e;
          const lgY = getColY(i, lgPresence, tCycle);
          const enY = getColY(i, enPresence, tCycle, 82);
          const y = lgY + (enY - lgY) * e;
          if (t < 0.5) {
            return <GenCard key={lead.id} lead={lead} x={x} y={y} scale={1} opacity={1 - t * 1.4} />;
          } else {
            return <EnrichCard key={lead.id} lead={lead} x={x} y={y} opacity={(t - 0.5) * 2} enrichP={0} />;
          }
        }
        // Phase 4: sit in Enrichment with enrichment reveal
        if (tCycle < Lt.moveRin) {
          const localT = clamp((tCycle - Lt.inEnr) / (Lt.moveRin - Lt.inEnr), 0, 1);
          const y = getColY(i, enPresence, tCycle, 82);
          return <EnrichCard key={lead.id} lead={lead} x={enX} y={y} opacity={1} enrichP={clamp(localT / 0.6, 0, 1)} />;
        }
        // Phase 5: slide Enrichment → Rinus (shrinking)
        if (tCycle < Lt.atRinus) {
          const t = clamp((tCycle - Lt.moveRin) / (Lt.atRinus - Lt.moveRin), 0, 1);
          const e = Easing.easeInOutCubic(t);
          const x = enX + (rnX - enX) * e;
          const enY = getColY(i, enPresence, tCycle, 82);
          const bob = Math.sin((tCycle - Lt.moveRin) * 4 + i) * 1.2 * (1 - t);
          const y = enY + bob + (rnY - enY) * e;
          return <EnrichCard key={lead.id} lead={lead} x={x} y={y} opacity={(1 - 0.85 * e) * 0.9} enrichP={1} />;
        }
        // Phase 6: inside agent — nothing visible
        if (tCycle < Lt.routeOut) return null;
        // Phase 7: emit from agent → Outreach (arc trajectory)
        if (tCycle < Lt.inOut) {
          const t = clamp((tCycle - Lt.routeOut) / (Lt.inOut - Lt.routeOut), 0, 1);
          const e = Easing.easeInOutCubic(t);
          const x = rnX + (orX - rnX) * e;
          const orY = getColY(i, orPresence, tCycle);
          const arc = lead.qualified ? -28 : 20;
          const y = rnY + (orY - rnY) * e + Math.sin(e * Math.PI) * arc;
          if (!lead.qualified) {
            return <DropCard key={lead.id} lead={lead} x={x} y={y} sendStart={Lt.routeOut} tCycle={tCycle} fadeOp={e} />;
          }
          return (
            <div key={lead.id} style={{
              position: "absolute", left: x, top: y,
              width: 120, height: 28,
              transform: `translate(-50%, -50%) scale(${0.5 + e * 0.5})`,
              opacity: e,
              background: T.PAPER_50,
              border: `1px solid ${T.LINE_STRONG}`,
              borderLeft: `2px solid ${T.PITCH_700}`,
              borderRadius: 4, padding: "4px 8px",
              fontFamily: T.SANS, fontSize: 10.5, color: T.INK,
              whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
            }}>{lead.name}</div>
          );
        }
        // Phase 8: card rests in Outreach/Drop column
        const fadeOp = 1 - clamp((tCycle - Lt.fadeStart) / 0.8, 0, 1);
        const orY = getColY(i, orPresence, tCycle);
        if (!lead.qualified) {
          const dropFadeAge = tCycle - (Lt.inOut + 3.0);
          return <DropCard key={lead.id} lead={lead} x={orX} y={orY} sendStart={Lt.routeOut} tCycle={tCycle} fadeOp={fadeOp} dropFadeAge={dropFadeAge} />;
        }
        return <OutreachCard key={lead.id} lead={lead} x={orX} y={orY} sendStart={Lt.inOut} tCycle={tCycle} fadeOp={fadeOp} />;
      })}
    </React.Fragment>
  );
}

function TasksTray({ tCycle }) {
  return (
    <React.Fragment>
      {LEADS.map((lead, i) => {
        const Lt = leadTimes(i);
        if (tCycle < Lt.inTask) return null;
        const dt = tCycle - Lt.inTask;
        const entryT = clamp(dt / 0.45, 0, 1);
        const e = Easing.easeOutBack(entryT);
        const fadeOp = 1 - clamp((tCycle - Lt.fadeStart) / 0.8, 0, 1);
        const y = getColY(i, tkPresence, tCycle);
        return <TaskCard key={lead.id} lead={lead} x={LAYOUT.cTK.cx} y={y} entry={e} opacity={fadeOp} />;
      })}
    </React.Fragment>
  );
}

/* ── Flow lines — subtle dashed connectors between columns ───────── */
function FlowLines({ time }) {
  const dashOffset = -((time * 24) % 24);
  const y = LAYOUT.agentCy;
  return (
    <svg width="960" height="540" style={{ position: "absolute", inset: 0, pointerEvents: "none" }}>
      <path d={`M ${LAYOUT.cEN.x + LAYOUT.cEN.w + 4} ${y} L ${LAYOUT.agentCx - 70} ${y}`}
        fill="none" stroke={T.PITCH_400} strokeWidth="1" strokeDasharray="2 5" strokeDashoffset={dashOffset} opacity="0.4"></path>
      <path d={`M ${LAYOUT.agentCx + 70} ${y} L ${LAYOUT.cOR.x - 4} ${y}`}
        fill="none" stroke={T.PITCH_400} strokeWidth="1" strokeDasharray="2 5" strokeDashoffset={dashOffset} opacity="0.45"></path>
      <path d={`M ${LAYOUT.cOR.x + LAYOUT.cOR.w + 4} ${y} L ${LAYOUT.cTK.x - 4} ${y}`}
        fill="none" stroke={T.PITCH_400} strokeWidth="1" strokeDasharray="2 5" strokeDashoffset={dashOffset * 1.2} opacity="0.35"></path>
    </svg>
  );
}

/* ── Stats footer ────────────────────────────────────────────────── */
function Stat({ label, value, accent }) {
  return (
    <div style={{ display: "flex", alignItems: "baseline", gap: 6 }}>
      <span style={{ fontFamily: T.MONO, fontSize: 8.5, letterSpacing: "0.16em", textTransform: "uppercase", color: T.INK_SUBTLE, fontWeight: 500 }}>{label}</span>
      <span style={{ fontFamily: T.DISPLAY, fontSize: 18, fontWeight: 600, color: accent, fontVariantNumeric: "tabular-nums", letterSpacing: "-0.02em" }}>
        {String(value).padStart(2, "0")}
      </span>
    </div>
  );
}
function StatsFooter({ tCycle }) {
  const received  = LEADS.filter((_, i) => tCycle > leadTimes(i).inGen).length;
  const enriched  = LEADS.filter((_, i) => tCycle > leadTimes(i).inEnr).length;
  const qualified = LEADS.filter((l, i) => tCycle > leadTimes(i).routeOut && l.qualified).length;
  const tasks     = LEADS.filter((_, i) => tCycle > leadTimes(i).inTask).length;
  return (
    <div style={{ position: "absolute", left: 24, top: LAYOUT.statsY, right: 24, display: "flex", gap: 32, alignItems: "baseline" }}>
      <Stat label="recibidos"   value={received}  accent={T.PITCH_400} />
      <Stat label="enriquecidos" value={enriched}  accent={T.PITCH_400} />
      <Stat label="calificados" value={qualified} accent={T.PITCH_700} />
      <Stat label="tareas"      value={tasks}     accent={T.SIGNAL} />
    </div>
  );
}

/* ── Background ──────────────────────────────────────────────────── */
function BackgroundGrid() {
  return (
    <React.Fragment>
      <div style={{
        position: "absolute", inset: 0,
        backgroundImage: `radial-gradient(${T.LINE_STRONG} 0.8px, transparent 0.8px)`,
        backgroundSize: "24px 24px", opacity: 0.5,
        maskImage: "radial-gradient(ellipse at center, black 30%, transparent 78%)",
        WebkitMaskImage: "radial-gradient(ellipse at center, black 30%, transparent 78%)",
        pointerEvents: "none",
      }}></div>
      <div style={{
        position: "absolute", left: LAYOUT.agentCx - 200, top: LAYOUT.agentCy - 200,
        width: 400, height: 400,
        background: `radial-gradient(circle, color-mix(in oklch, ${T.PITCH_100} 60%, transparent) 0%, transparent 70%)`,
        pointerEvents: "none",
      }}></div>
    </React.Fragment>
  );
}

/* ── Root ────────────────────────────────────────────────────────── */
function HeroPipeline() {
  const [time, setTime] = useState(0);
  const [scale, setScale] = useState(1);
  const containerRef = useRef(null);

  useEffect(() => {
    let raf = 0;
    let last = null;
    const step = (ts) => {
      if (last == null) last = ts;
      const dt = (ts - last) / 1000;
      last = ts;
      setTime((t) => t + dt);
      raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, []);

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const measure = () => setScale(el.clientWidth / 960);
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const tCycle = time % CYCLE;

  return (
    <div className="hero-pipeline" ref={containerRef}>
      <div className="hero-pipeline-canvas" style={{ transform: `scale(${scale})` }}>
        <BackgroundGrid />
        <Dividers />
        <FlowLines time={time} />
        <StageLabels />
        <AgentNode tCycle={tCycle} time={time} />

        <LeadTraveler tCycle={tCycle} />
        <TasksTray tCycle={tCycle} />
        <StatsFooter tCycle={tCycle} />
      </div>
    </div>
  );
}

window.HeroPipeline = HeroPipeline;
