// TAROT — 22 Arcanos Mayores ANIMADOS (Canvas 2D)
// Sobrescribe TODAS las entradas mayor en PILOT_SIGILS con versiones animadas.
// Se carga DESPUÉS de sigils-majors.jsx (y sigils.jsx) para reemplazar las
// versiones SVG estáticas. Las 56 menores siguen como SVG.

// ─── Loop global: un único rAF dibuja todos los canvases registrados ────
// Evita el throttling/limitación que los browsers aplican cuando hay muchos
// rAF concurrentes. Cada AnimatedSigilCanvas se registra al mount.
const __sigilAnims = (window.__sigilAnims = window.__sigilAnims || []);
if (!window.__sigilLoopStarted) {
  window.__sigilLoopStarted = true;
  const tick = (now) => {
    for (let i = 0; i < __sigilAnims.length; i++) {
      const a = __sigilAnims[i];
      if (!a.canvas.isConnected) continue;
      if (a.box <= 0) continue;
      const ctx = a.ctx;
      const scale = (a.box / a.size) * a.dpr;
      ctx.setTransform(scale, 0, 0, scale, 0, 0);
      ctx.clearRect(0, 0, a.size, a.size);
      const elapsed = (now - a.start) / 1000;
      const t = a.period ? (elapsed % a.period) / a.period : elapsed;
      try { a.draw(ctx, a.size, t, a.color, elapsed); } catch (e) { /* swallow */ }
    }
    requestAnimationFrame(tick);
  };
  requestAnimationFrame(tick);
}

function AnimatedSigilCanvas({ size, color, draw, period }) {
  const ref = React.useRef(null);

  React.useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;
    const parent = canvas.parentElement;
    if (!parent) return;
    const dpr = window.devicePixelRatio || 1;
    const ctx = canvas.getContext('2d');
    const entry = {
      canvas, ctx, draw, size, color, period,
      dpr, box: 0, start: performance.now(),
    };

    const resize = () => {
      const w = parent.clientWidth;
      const h = parent.clientHeight;
      const box = Math.max(0, Math.min(w, h));
      if (box === entry.box) return;
      entry.box = box;
      canvas.width  = Math.round(box * dpr);
      canvas.height = Math.round(box * dpr);
      canvas.style.width  = box + 'px';
      canvas.style.height = box + 'px';
    };
    resize();

    let ro;
    if (typeof ResizeObserver !== 'undefined') {
      ro = new ResizeObserver(resize);
      ro.observe(parent);
    } else {
      window.addEventListener('resize', resize);
    }

    __sigilAnims.push(entry);

    return () => {
      const idx = __sigilAnims.indexOf(entry);
      if (idx >= 0) __sigilAnims.splice(idx, 1);
      if (ro) ro.disconnect();
      else window.removeEventListener('resize', resize);
    };
  }, []);

  return React.createElement('canvas', {
    ref,
    style: { display: 'block', margin: '0 auto' },
  });
}

// ─── Easing y utilidades ────────────────────────────────────────────────
const _ein  = (t) => t*t;
const _eout = (t) => 1 - (1-t)*(1-t);
const _eio  = (t) => t<0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2,3)/2;
const _smoothstep = (t) => t*t*(3-2*t);

function _hexA(hex, a) {
  if (hex && hex[0] === '#' && hex.length === 7) {
    const r = parseInt(hex.slice(1,3), 16);
    const g = parseInt(hex.slice(3,5), 16);
    const b = parseInt(hex.slice(5,7), 16);
    return `rgba(${r},${g},${b},${a})`;
  }
  return hex;
}

// PRNG determinista (mulberry32) por seed
function _prng(seed) {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6D2B79F5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function _arrowHead(ctx, x, y, ang, len, color) {
  const c = Math.cos(ang), s = Math.sin(ang);
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x - c*len - s*len*0.45, y - s*len + c*len*0.45);
  ctx.lineTo(x - c*len + s*len*0.45, y - s*len - c*len*0.45);
  ctx.closePath();
  ctx.fill();
}

// ============================================================
// 0 · EL CAOS — Lorenz, 3 trayectorias divergentes
// ============================================================
function drawCaos(ctx, size, t, color) {
  const cx = size/2, cy = size/2;

  // grid orbital débil
  ctx.strokeStyle = _hexA(color, 0.18);
  ctx.lineWidth = 0.4;
  [40, 80, 120].forEach(r => {
    ctx.beginPath(); ctx.arc(cx, cy, r * size/300, 0, Math.PI*2); ctx.stroke();
  });

  // Tres trayectorias Lorenz con condiciones iniciales casi idénticas
  const ks = [-0.5, 0, 0.5];
  // cuántos puntos dibujar según t
  const N = 400;
  const visible = Math.floor(_eout(t) * N);

  ks.forEach((eps, k) => {
    let x = -50 + eps*0.5, y = 0, z = 25;
    const pts = [];
    for (let i = 0; i < N; i++) {
      const dt = 0.012;
      const dx = 10*(y-x);
      const dy = x*(28-z) - y;
      const dz = x*y - 2.6*z;
      x += dx*dt; y += dy*dt; z += dz*dt;
      pts.push([cx + x*2.4*size/300, cy + (z-25)*2.4*size/300]);
    }
    ctx.strokeStyle = _hexA(color, 0.65 + k*0.1);
    ctx.lineWidth = 0.7;
    ctx.beginPath();
    for (let i = 0; i < visible; i++) {
      if (i === 0) ctx.moveTo(pts[i][0], pts[i][1]);
      else ctx.lineTo(pts[i][0], pts[i][1]);
    }
    ctx.stroke();

    // cabeza brillante en el punto actual
    if (visible > 0 && visible < N) {
      const [hx, hy] = pts[visible - 1];
      ctx.fillStyle = '#fff';
      ctx.shadowColor = color;
      ctx.shadowBlur = 8;
      ctx.beginPath(); ctx.arc(hx, hy, 2.4, 0, Math.PI*2); ctx.fill();
      ctx.shadowBlur = 0;
    }

    // cluster inicial
    ctx.fillStyle = color;
    ctx.beginPath(); ctx.arc(pts[0][0], pts[0][1], 2, 0, Math.PI*2); ctx.fill();
  });
}

// ============================================================
// I · EL OBSERVADOR — distinción esto / lo demás
// ============================================================
function drawObservador(ctx, size, t, color) {
  const cx = size/2;
  // pestañeo del ojo y pulso del círculo distinguido
  const blink = t > 0.92 ? (t - 0.92) / 0.08 : 0;
  const pulseR = (40 + Math.sin(t * Math.PI * 4) * 8) * size/300;

  // Líneas de mirada (rayos que conectan ojo y círculo)
  ctx.strokeStyle = _hexA(color, 0.45);
  ctx.lineWidth = 0.4;
  [-1, -0.5, 0, 0.5, 1].forEach((p, i) => {
    const xa = cx + p*30 * size/300;
    const ya = size*0.22 + 10 * size/300;
    const xb = cx + p*60 * size/300;
    const yb = size*0.62 - 68 * size/300;
    ctx.setLineDash([2, 3]);
    ctx.beginPath();
    ctx.moveTo(xa, ya);
    ctx.lineTo(xb, yb);
    ctx.stroke();
  });
  ctx.setLineDash([]);

  // Almendra del ojo
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.4;
  ctx.beginPath();
  const eyeY = size*0.22;
  const eyeWidth = 50 * size/300;
  ctx.moveTo(cx - eyeWidth, eyeY);
  ctx.quadraticCurveTo(cx, eyeY - 8*size/300, cx + eyeWidth, eyeY);
  ctx.quadraticCurveTo(cx, eyeY + 18*size/300, cx - eyeWidth, eyeY);
  ctx.closePath();
  ctx.stroke();

  // pupila — parpadea
  if (blink < 0.4) {
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(cx, eyeY, 6 * size/300, 0, Math.PI*2);
    ctx.fill();
    ctx.strokeStyle = color;
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.arc(cx, eyeY, 14 * size/300, 0, Math.PI*2);
    ctx.stroke();
  } else {
    // ojo cerrado: línea
    ctx.strokeStyle = color;
    ctx.lineWidth = 1.4;
    ctx.beginPath();
    ctx.moveTo(cx - 14*size/300, eyeY);
    ctx.lineTo(cx + 14*size/300, eyeY);
    ctx.stroke();
  }

  // Círculo distinguido (esto / lo demás) — pulso
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.2;
  ctx.beginPath();
  ctx.arc(cx, size*0.62, pulseR, 0, Math.PI*2);
  ctx.stroke();

  // Etiquetas
  ctx.fillStyle = _hexA(color, 0.85);
  ctx.font = `italic ${9 * size/300}px serif`;
  ctx.textAlign = 'center';
  ctx.fillText('esto', cx, size*0.62 + 4*size/300);
  ctx.fillStyle = _hexA(color, 0.5);
  ctx.fillText('lo demás', cx + 95*size/300, size*0.62 + 4*size/300);
}

// ============================================================
// II · LA INFORMACIÓN — distribución → distribución (Δ que reduce incertidumbre)
// ============================================================
function drawInformacion(ctx, size, t, color) {
  const cx = size/2, cy = size/2;
  const s = size/300;
  // pulso de δ
  const pulse = 0.4 + 0.6 * (0.5 + 0.5 * Math.sin(t * Math.PI * 4));

  ctx.strokeStyle = _hexA(color, 0.4);
  ctx.lineWidth = 0.4;
  ctx.beginPath();
  ctx.moveTo(20*s, cy + 30*s);
  ctx.lineTo(size - 20*s, cy + 30*s);
  ctx.stroke();

  // izquierda: gauss ancha (sigma 1.5)
  const bars = (sigma, scaleH) => Array.from({length:13}).map((_,i)=>{
    const u = (i-6)/6;
    return { x: i*7 - 45, h: scaleH * Math.exp(-u*u*sigma) };
  });

  // Antes (ancha)
  ctx.fillStyle = _hexA(color, 0.55);
  bars(1.5, 60).forEach(b => {
    ctx.fillRect((cx - 90*s) + b.x*s, cy + 30*s - b.h*s, 5*s, b.h*s);
  });
  ctx.fillStyle = _hexA(color, 0.85);
  ctx.font = `italic ${9*s}px serif`;
  ctx.textAlign = 'center';
  ctx.fillText('antes', cx - 90*s, cy + 50*s);

  // Después (angosta, sigma 8)
  ctx.fillStyle = color;
  bars(8, 60).forEach(b => {
    ctx.fillRect((cx + 90*s) + b.x*s, cy + 30*s - b.h*s, 5*s, b.h*s);
  });
  ctx.fillText('después', cx + 90*s, cy + 50*s);

  // flecha central δ pulsando
  ctx.strokeStyle = _hexA(color, pulse);
  ctx.lineWidth = 1.4;
  ctx.beginPath();
  ctx.moveTo(cx - 22*s, cy - 10*s);
  ctx.lineTo(cx + 22*s, cy - 10*s);
  ctx.stroke();
  _arrowHead(ctx, cx + 22*s, cy - 10*s, 0, 6*s, _hexA(color, pulse));

  ctx.fillStyle = color;
  ctx.font = `italic ${11*s}px serif`;
  ctx.fillText('δ', cx, cy - 22*s);
}

// ============================================================
// III · AUTO-ORGANIZACIÓN — gas random → lattice hexagonal
// ============================================================
function drawAutoOrg(ctx, size, t, color, elapsed) {
  const cx = size/2;
  const s = size/300;
  const a = 18 * s;

  const cells = [];
  for (let i = 0; i < 5; i++) for (let j = 0; j < 8; j++) {
    const x = size*0.6 + i*a*1.4;
    const y = size*0.1 + j*a*1.5 + (i%2 ? a*0.75 : 0);
    if (x < size-15 && y < size-15) cells.push([x, y, i, j]);
  }

  const r = _prng(3);
  const particles = Array.from({length: 32}).map(() => ({
    x: 20*s + r()*110*s,
    y: 20*s + r()*260*s,
    r: (1.5 + r()*1.5)*s,
    o: 0.5 + r()*0.4,
    ph: r() * Math.PI * 2,
  }));

  particles.forEach(p => {
    const jx = Math.cos(elapsed*2 + p.ph) * 1.6*s;
    const jy = Math.sin(elapsed*2.4 + p.ph*1.3) * 1.6*s;
    ctx.fillStyle = _hexA(color, p.o);
    ctx.beginPath();
    ctx.arc(p.x + jx, p.y + jy, p.r, 0, Math.PI*2);
    ctx.fill();
  });

  ctx.strokeStyle = color;
  ctx.lineWidth = 1.4;
  ctx.beginPath();
  ctx.moveTo(cx - 10*s, cx);
  ctx.lineTo(cx + 10*s, cx);
  ctx.stroke();
  _arrowHead(ctx, cx + 12*s, cx, 0, 6*s, color);

  const buildPhase = Math.min(1, t / 0.7);
  const fadeOut = t > 0.9 ? (1 - (t - 0.9) / 0.1) : 1;

  cells.forEach(([x, y, i, j], idx) => {
    const cellT = idx / cells.length;
    const localProgress = Math.max(0, Math.min(1, (buildPhase - cellT) * 2));
    const eased = _eout(localProgress);
    const alpha = eased * 0.85 * fadeOut;
    if (alpha <= 0.02) return;

    ctx.strokeStyle = _hexA(color, alpha);
    ctx.lineWidth = 0.55;
    ctx.beginPath();
    for (let k = 0; k < 6; k++) {
      const ang = (k/6)*Math.PI*2;
      const hx = x + Math.cos(ang)*a*0.85;
      const hy = y + Math.sin(ang)*a*0.85;
      if (k === 0) ctx.moveTo(hx, hy);
      else ctx.lineTo(hx, hy);
    }
    ctx.closePath();
    ctx.stroke();

    if ((i+j)%3 === 0) {
      ctx.strokeStyle = _hexA(color, alpha * 0.8);
      ctx.lineWidth = 0.6;
      ctx.beginPath();
      ctx.moveTo(x, y + 8*s);
      ctx.lineTo(x, y - 8*s);
      ctx.stroke();
    }
  });
}

// ============================================================
// IV · LA JERARQUÍA — cajas anidadas que se dibujan secuencialmente
// ============================================================
function drawJerarquia(ctx, size, t, color) {
  const cx = size/2, cy = size/2;
  const s = size/300;
  const stages = [0, 1, 2, 3];
  // construir y luego pulso del nivel actual
  stages.forEach((i) => {
    const stageStart = i / 5;
    const local = Math.max(0, Math.min(1, (t - stageStart) * 5));
    const alpha = _eout(local) * (1 - i*0.15);
    if (alpha <= 0.02) return;
    const w = (220 - i*52) * s;
    const h = (260 - i*58) * s;
    ctx.strokeStyle = _hexA(color, alpha);
    ctx.lineWidth = 1.4 - i*0.22;
    ctx.strokeRect(cx - w/2, cy - h/2, w, h);

    ctx.fillStyle = _hexA(color, alpha);
    ctx.font = `${8*s}px monospace`;
    ctx.textAlign = 'left';
    ctx.fillText(`L${4-i}`, cx - 95*s + i*20*s, cy - h/2 + 10*s);
  });

  // núcleo
  const coreLocal = Math.max(0, Math.min(1, (t - 0.8) * 5));
  if (coreLocal > 0) {
    ctx.fillStyle = _hexA(color, coreLocal);
    ctx.beginPath();
    ctx.arc(cx, cy, 6*s, 0, Math.PI*2);
    ctx.fill();
    ctx.fillStyle = _hexA(color, 0.7 * coreLocal);
    ctx.font = `italic ${9*s}px serif`;
    ctx.textAlign = 'left';
    ctx.fillText('L0', cx + 12*s, cy + 4*s);
  }
}

// ============================================================
// V · EL LOCK-IN — varios caminos → uno solo, cerrojo
// ============================================================
function drawLockIn(ctx, size, t, color) {
  const cx = size/2;
  const s = size/300;

  // Caminos fantasma posibles (los descartados)
  const branches = [
    [-80, -60, -50],
    [-30, -50, -90],
    [40, 80, 60],
    [80, 40, 90],
  ];

  // Antes del lock-in los caminos compiten; después de t>0.4 solo brilla uno
  const lockProg = _eio(Math.min(1, t / 0.5));

  branches.forEach((bumps, i) => {
    // alpha decrece cuando se "lockea"
    const alpha = 0.18 * (1 - lockProg * 0.85);
    if (alpha <= 0.01) return;
    ctx.strokeStyle = _hexA(color, alpha);
    ctx.lineWidth = 0.5;
    ctx.setLineDash([1, 3]);
    ctx.beginPath();
    ctx.moveTo(cx, size*0.4);
    bumps.forEach((b, k) => {
      const y = size*0.5 + k*40*s;
      ctx.lineTo(cx + b*s, y);
    });
    ctx.stroke();
  });
  ctx.setLineDash([]);

  // Rayos hacia el punto inicial
  ctx.strokeStyle = _hexA(color, 0.45);
  ctx.lineWidth = 0.5;
  ctx.setLineDash([2, 2]);
  [-80, -40, 0, 40, 80].forEach((dx) => {
    ctx.beginPath();
    ctx.moveTo(cx + dx*s, 28*s);
    ctx.lineTo(cx + dx*0.3*s, size*0.4);
    ctx.stroke();
  });
  ctx.setLineDash([]);

  // Punto inicial
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.arc(cx, size*0.4, 6*s, 0, Math.PI*2);
  ctx.fill();

  // Camino lockeado — se traza progresivamente
  const path = [
    [cx, size*0.4],
    [cx - 30*s, size*0.55],
    [cx + 10*s, size*0.7],
    [cx - 15*s, size - 50*s],
  ];
  const drawN = Math.floor(lockProg * (path.length - 1)) + 1;
  ctx.strokeStyle = _hexA(color, 0.9);
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(path[0][0], path[0][1]);
  for (let i = 1; i < drawN; i++) {
    ctx.lineTo(path[i][0], path[i][1]);
  }
  // segmento parcial al avance
  if (drawN < path.length) {
    const remainder = lockProg * (path.length - 1) - (drawN - 1);
    const p0 = path[drawN - 1];
    const p1 = path[drawN];
    ctx.lineTo(p0[0] + (p1[0]-p0[0])*remainder, p0[1] + (p1[1]-p0[1])*remainder);
  }
  ctx.stroke();

  // Cerrojo (rect + arco) — aparece al final
  if (lockProg > 0.9) {
    const lockAlpha = (lockProg - 0.9) / 0.1;
    ctx.strokeStyle = _hexA(color, lockAlpha);
    ctx.lineWidth = 1;
    ctx.strokeRect(cx - 22*s, size - 44*s, 28*s, 20*s);
    ctx.beginPath();
    ctx.moveTo(cx - 15*s, size - 44*s);
    ctx.quadraticCurveTo(cx - 15*s, size - 56*s, cx - 8*s, size - 56*s);
    ctx.quadraticCurveTo(cx - 1*s, size - 56*s, cx - 1*s, size - 44*s);
    ctx.stroke();
    ctx.fillStyle = _hexA(color, lockAlpha);
    ctx.beginPath();
    ctx.arc(cx - 8*s, size - 34*s, 1.5*s, 0, Math.PI*2);
    ctx.fill();
  }
}

// ============================================================
// VI · EL ACOPLAMIENTO — dos sistemas con pulsos mutuos
// ============================================================
function drawAcoplamiento(ctx, size, t, color, elapsed) {
  const cx = size/2, cy = size/2;
  const s = size/300;

  // dos círculos con sus contenidos
  [-65, 65].forEach((dx) => {
    ctx.strokeStyle = color;
    ctx.lineWidth = 1.3;
    ctx.beginPath();
    ctx.arc(cx + dx*s, cy, 40*s, 0, Math.PI*2);
    ctx.stroke();

    ctx.strokeStyle = _hexA(color, 0.7);
    ctx.lineWidth = 0.6;
    ctx.beginPath(); ctx.arc(cx + dx*s, cy - 8*s, 6*s, 0, Math.PI*2); ctx.stroke();
    ctx.beginPath(); ctx.arc(cx + dx*s - 8*s, cy + 8*s, 4*s, 0, Math.PI*2); ctx.stroke();
    ctx.fillStyle = _hexA(color, 0.5);
    ctx.beginPath(); ctx.arc(cx + dx*s + 8*s, cy + 10*s, 3*s, 0, Math.PI*2); ctx.fill();
  });

  // labels A B
  ctx.fillStyle = color;
  ctx.font = `italic ${11*s}px serif`;
  ctx.textAlign = 'center';
  ctx.fillText('A', cx - 65*s, cy - 52*s);
  ctx.fillText('B', cx + 65*s, cy - 52*s);

  // Arcos de acoplamiento (arriba A→B, abajo B→A)
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.2;
  // arriba
  ctx.beginPath();
  ctx.moveTo(cx - 25*s, cy - 14*s);
  ctx.quadraticCurveTo(cx, cy - 46*s, cx + 25*s, cy - 14*s);
  ctx.stroke();
  _arrowHead(ctx, cx + 25*s, cy - 14*s, Math.PI*0.25, 5*s, color);
  // abajo
  ctx.beginPath();
  ctx.moveTo(cx + 25*s, cy + 14*s);
  ctx.quadraticCurveTo(cx, cy + 46*s, cx - 25*s, cy + 14*s);
  ctx.stroke();
  _arrowHead(ctx, cx - 25*s, cy + 14*s, Math.PI + Math.PI*0.25, 5*s, color);

  // Pulsos viajando: dos partículas alternadas en los arcos
  // arco arriba: parametrizado en u∈[0,1] de A a B
  const u1 = (elapsed * 0.5) % 1;
  const u2 = ((elapsed * 0.5) + 0.5) % 1;
  const arcTop = (u) => {
    const x1 = cx - 25*s, y1 = cy - 14*s;
    const xc = cx, yc = cy - 46*s;
    const x2 = cx + 25*s, y2 = cy - 14*s;
    const mt = 1 - u;
    return [mt*mt*x1 + 2*mt*u*xc + u*u*x2, mt*mt*y1 + 2*mt*u*yc + u*u*y2];
  };
  const arcBot = (u) => {
    const x1 = cx + 25*s, y1 = cy + 14*s;
    const xc = cx, yc = cy + 46*s;
    const x2 = cx - 25*s, y2 = cy + 14*s;
    const mt = 1 - u;
    return [mt*mt*x1 + 2*mt*u*xc + u*u*x2, mt*mt*y1 + 2*mt*u*yc + u*u*y2];
  };
  const [p1x, p1y] = arcTop(u1);
  const [p2x, p2y] = arcBot(u2);
  ctx.fillStyle = '#fff';
  ctx.shadowColor = color;
  ctx.shadowBlur = 8;
  ctx.beginPath(); ctx.arc(p1x, p1y, 3*s, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(p2x, p2y, 3*s, 0, Math.PI*2); ctx.fill();
  ctx.shadowBlur = 0;
}

// ============================================================
// VII · EL ATRACTOR — partícula colapsa a ciclo límite
// ============================================================
function drawAtractor(ctx, size, t, color) {
  const cx = size/2, cy = size/2;
  const limitR = size * (55/300);

  const pts = [];
  let r = size * (110/300), theta = 0;
  for (let i = 0; i < 380; i++) {
    pts.push([cx + Math.cos(theta)*r, cy + Math.sin(theta)*r]);
    theta += 0.12;
    r = r - (r - limitR) * 0.013;
  }

  ctx.strokeStyle = _hexA(color, 0.55);
  ctx.lineWidth = 0.5;
  ctx.beginPath();
  pts.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
  ctx.stroke();

  ctx.strokeStyle = color;
  ctx.lineWidth = 1.4;
  ctx.beginPath();
  ctx.arc(cx, cy, limitR, 0, Math.PI*2);
  ctx.stroke();

  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.arc(cx, cy, 5, 0, Math.PI*2);
  ctx.fill();

  const TRAIL = 22;
  let particleIdx, useLimit;
  if (t < 0.55) {
    particleIdx = Math.floor((t / 0.55) * (pts.length - 1));
    useLimit = false;
  } else {
    particleIdx = pts.length - 1;
    useLimit = true;
  }

  ctx.lineWidth = 1.6;
  for (let k = 1; k <= TRAIL; k++) {
    let p0, p1;
    if (useLimit) {
      const lt = (t - 0.55) / 0.45;
      const ang = lt * Math.PI * 4;
      const angK0 = ang - (k-1) * 0.04;
      const angK1 = ang - k * 0.04;
      p0 = [cx + Math.cos(angK0)*limitR, cy + Math.sin(angK0)*limitR];
      p1 = [cx + Math.cos(angK1)*limitR, cy + Math.sin(angK1)*limitR];
    } else {
      const i0 = particleIdx - (k-1);
      const i1 = particleIdx - k;
      if (i0 < 0 || i1 < 0) break;
      p0 = pts[i0];
      p1 = pts[i1];
    }
    const a = (1 - k/TRAIL) * 0.85;
    ctx.strokeStyle = _hexA(color, a);
    ctx.beginPath();
    ctx.moveTo(p0[0], p0[1]);
    ctx.lineTo(p1[0], p1[1]);
    ctx.stroke();
  }

  let px, py;
  if (useLimit) {
    const lt = (t - 0.55) / 0.45;
    const ang = lt * Math.PI * 4;
    px = cx + Math.cos(ang)*limitR;
    py = cy + Math.sin(ang)*limitR;
  } else {
    [px, py] = pts[particleIdx];
  }
  ctx.shadowColor = color;
  ctx.shadowBlur = 14;
  ctx.fillStyle = '#fff';
  ctx.beginPath();
  ctx.arc(px, py, 4.5, 0, Math.PI*2);
  ctx.fill();
  ctx.shadowBlur = 0;
}

// ============================================================
// VIII · LA ROBUSTEZ — estructura hexagonal con impactos rebotando
// ============================================================
function drawRobustez(ctx, size, t, color, elapsed) {
  const cx = size/2, cy = size/2;
  const s = size/300;
  const N = 6;
  const polyR = 55 * s;

  // poly hexagonal central
  const polyPts = Array.from({length:N}).map((_,i)=>{
    const a = (i/N)*Math.PI*2 - Math.PI/2;
    return [cx + Math.cos(a)*polyR, cy + Math.sin(a)*polyR];
  });
  ctx.fillStyle = _hexA(color, 0.14);
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  polyPts.forEach((p, i) => i === 0 ? ctx.moveTo(p[0], p[1]) : ctx.lineTo(p[0], p[1]));
  ctx.closePath();
  ctx.fill();
  ctx.stroke();

  // diagonales internas
  ctx.strokeStyle = _hexA(color, 0.6);
  ctx.lineWidth = 0.5;
  for (let i = 0; i < N; i++) {
    const next = polyPts[(i+2)%N];
    ctx.beginPath();
    ctx.moveTo(polyPts[i][0], polyPts[i][1]);
    ctx.lineTo(next[0], next[1]);
    ctx.stroke();
  }

  ctx.fillStyle = color;
  ctx.beginPath(); ctx.arc(cx, cy, 4*s, 0, Math.PI*2); ctx.fill();

  // 4 impactos que se acercan, golpean y rebotan, cíclicos
  [0,1,2,3].forEach(i => {
    const a = i*Math.PI/2 + Math.PI/4;
    const phaseT = ((elapsed * 0.6) + i*0.25) % 1;
    // 0..0.5 acercándose desde lejos, 0.5..1 rebotando
    let dist;
    if (phaseT < 0.5) {
      dist = 120 - phaseT * 2 * (120 - 68);
    } else {
      const r = (phaseT - 0.5) * 2;
      dist = 68 + r * (120 - 68);
    }
    dist *= s;
    const x = cx + Math.cos(a)*dist;
    const y = cy + Math.sin(a)*dist;
    // línea desde lejos al impacto/rebote
    ctx.strokeStyle = _hexA(color, 0.7);
    ctx.lineWidth = 0.9;
    ctx.beginPath();
    ctx.moveTo(cx + Math.cos(a)*120*s, cy + Math.sin(a)*120*s);
    ctx.lineTo(x, y);
    ctx.stroke();

    // ricochet (rebote)
    if (phaseT > 0.5) {
      const r = (phaseT - 0.5) * 2;
      const rfa = a + Math.PI * 0.7;
      const x2 = (cx + Math.cos(a)*68*s) + Math.cos(rfa) * 45 * s * r;
      const y2 = (cy + Math.sin(a)*68*s) + Math.sin(rfa) * 45 * s * r;
      ctx.strokeStyle = _hexA(color, 0.5 * (1 - r*0.5));
      ctx.lineWidth = 0.5;
      ctx.setLineDash([2, 2]);
      ctx.beginPath();
      ctx.moveTo(cx + Math.cos(a)*68*s, cy + Math.sin(a)*68*s);
      ctx.lineTo(x2, y2);
      ctx.stroke();
      ctx.setLineDash([]);
    }

    // punta brillante en el impacto
    if (phaseT > 0.45 && phaseT < 0.6) {
      const brightness = 1 - Math.abs(phaseT - 0.5) / 0.075;
      ctx.fillStyle = _hexA('#ffffff', brightness * 0.9);
      ctx.shadowColor = color;
      ctx.shadowBlur = 10;
      ctx.beginPath();
      ctx.arc(cx + Math.cos(a)*68*s, cy + Math.sin(a)*68*s, 3*s, 0, Math.PI*2);
      ctx.fill();
      ctx.shadowBlur = 0;
    }
  });
}

// ============================================================
// IX · LEJOS DEL EQUILIBRIO — flujo a través de estructura disipativa
// ============================================================
function drawLejosEq(ctx, size, t, color, elapsed) {
  const cx = size/2;
  const s = size/300;

  // paredes
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.3;
  ctx.beginPath();
  ctx.moveTo(cx - 90*s, 50*s);
  ctx.lineTo(cx - 90*s, size - 50*s);
  ctx.moveTo(cx + 90*s, 50*s);
  ctx.lineTo(cx + 90*s, size - 50*s);
  ctx.stroke();

  // 3 flechas entrantes arriba (con animación de gota)
  [-50, 0, 50].forEach((dx, i) => {
    const drop = ((elapsed * 0.6) + i*0.2) % 1;
    ctx.strokeStyle = color;
    ctx.lineWidth = 1.2;
    ctx.beginPath();
    ctx.moveTo(cx + dx*s, 18*s);
    ctx.lineTo(cx + dx*s, 48*s);
    ctx.stroke();
    _arrowHead(ctx, cx + dx*s, 48*s, Math.PI/2, 5*s, color);

    // partícula bajando
    if (drop > 0 && drop < 1) {
      ctx.fillStyle = _hexA(color, 0.9);
      ctx.beginPath();
      ctx.arc(cx + dx*s, 22*s + drop*22*s, 2.2*s, 0, Math.PI*2);
      ctx.fill();
    }
  });

  // 3 niveles de elipses interconectadas
  [size*0.32, size*0.55, size*0.78].forEach((y, k) => {
    ctx.strokeStyle = color;
    ctx.lineWidth = 0.7;
    ctx.beginPath(); ctx.ellipse(cx - 44*s, y, 30*s, 14*s, 0, 0, Math.PI*2); ctx.stroke();
    ctx.beginPath(); ctx.ellipse(cx + 44*s, y, 30*s, 14*s, 0, 0, Math.PI*2); ctx.stroke();
    ctx.strokeStyle = _hexA(color, 0.7);
    ctx.lineWidth = 0.5;
    ctx.beginPath();
    ctx.moveTo(cx - 44*s, y - 12*s);
    ctx.quadraticCurveTo(cx - 28*s, y - 8*s, cx - 30*s, y + 4*s);
    ctx.stroke();
    _arrowHead(ctx, cx - 30*s, y + 4*s, Math.PI*0.7, 4*s, color);
    ctx.beginPath();
    ctx.moveTo(cx + 44*s, y + 12*s);
    ctx.quadraticCurveTo(cx + 28*s, y + 8*s, cx + 30*s, y - 4*s);
    ctx.stroke();
    _arrowHead(ctx, cx + 30*s, y - 4*s, -Math.PI*0.3, 4*s, color);
  });

  // 3 flechas salientes abajo
  [-50, 0, 50].forEach((dx) => {
    ctx.strokeStyle = color;
    ctx.lineWidth = 1.2;
    ctx.beginPath();
    ctx.moveTo(cx + dx*s, size - 48*s);
    ctx.lineTo(cx + dx*s, size - 12*s);
    ctx.stroke();
    _arrowHead(ctx, cx + dx*s, size - 12*s, Math.PI/2, 5*s, color);
  });
}

// ============================================================
// X · LA BIFURCACIÓN — pitchfork, parámetro μ atraviesa μc
// ============================================================
function drawBifurcacion(ctx, size, t, color) {
  const cx = size/2, cy = size/2;
  const s = size/300;

  // ejes
  ctx.strokeStyle = _hexA(color, 0.5);
  ctx.lineWidth = 0.5;
  ctx.beginPath();
  ctx.moveTo(30*s, cy); ctx.lineTo(size - 20*s, cy);
  ctx.moveTo(40*s, 30*s); ctx.lineTo(40*s, size - 30*s);
  ctx.stroke();

  ctx.fillStyle = _hexA(color, 0.7);
  ctx.font = `italic ${10*s}px serif`;
  ctx.textAlign = 'end';
  ctx.fillText('μ', size - 25*s, cy + 12*s);
  ctx.textAlign = 'start';
  ctx.fillText('x*', 50*s, 38*s);

  // marcador μ que se desplaza por el eje
  const muProg = _eio(t);
  const muX = 40*s + muProg * (size - 80*s);

  // rama estable antes de la bifurcación
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.7;
  ctx.beginPath();
  ctx.moveTo(40*s, cy);
  ctx.lineTo(Math.min(muX, cx), cy);
  ctx.stroke();

  // si μ > μc → pitchfork sale
  if (muProg > 0.5) {
    const branchProg = (muProg - 0.5) / 0.5;
    // rama superior
    ctx.strokeStyle = color;
    ctx.lineWidth = 1.7;
    ctx.beginPath();
    const xs = [];
    const targetXEnd = size - 30*s;
    for (let i = 0; i <= 30; i++) {
      const u = i/30 * branchProg;
      const x = cx + u * (targetXEnd - cx);
      const dy = -72*s * Math.pow(u, 0.6);
      xs.push([x, cy + dy]);
    }
    xs.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
    ctx.stroke();
    // rama inferior
    ctx.beginPath();
    xs.forEach(([x, y], i) => {
      const dy = -(y - cy);
      if (i === 0) ctx.moveTo(x, cy + dy);
      else ctx.lineTo(x, cy + dy);
    });
    ctx.stroke();

    // rama inestable (dashed) que continúa por el medio
    ctx.strokeStyle = _hexA(color, 0.55);
    ctx.lineWidth = 0.6;
    ctx.setLineDash([3, 3]);
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.lineTo(cx + branchProg * (targetXEnd - cx), cy);
    ctx.stroke();
    ctx.setLineDash([]);
  }

  // línea vertical μc
  ctx.strokeStyle = _hexA(color, 0.5);
  ctx.lineWidth = 0.3;
  ctx.setLineDash([2, 3]);
  ctx.beginPath();
  ctx.moveTo(cx, cy - 100*s);
  ctx.lineTo(cx, cy + 100*s);
  ctx.stroke();
  ctx.setLineDash([]);

  // bifurcation point
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.arc(cx, cy, 4.5*s, 0, Math.PI*2);
  ctx.fill();

  // marcador μ actual (en el eje)
  ctx.fillStyle = '#fff';
  ctx.shadowColor = color;
  ctx.shadowBlur = 8;
  ctx.beginPath();
  ctx.arc(muX, cy, 3*s, 0, Math.PI*2);
  ctx.fill();
  ctx.shadowBlur = 0;

  ctx.fillStyle = _hexA(color, 0.7);
  ctx.font = `italic ${9*s}px serif`;
  ctx.textAlign = 'center';
  ctx.fillText('μc', cx, size - 15*s);
}

// ============================================================
// XI · LA HOMEOSTASIS — oscilación amortiguada en torno al setpoint
// ============================================================
function drawHomeostasis(ctx, size, t, color, elapsed) {
  const cx = size/2, cy = size/2;
  const s = size/300;

  // setpoint
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.4;
  ctx.setLineDash([6, 3]);
  ctx.beginPath();
  ctx.moveTo(30*s, cy - 40*s);
  ctx.lineTo(size - 30*s, cy - 40*s);
  ctx.stroke();
  ctx.setLineDash([]);

  ctx.fillStyle = color;
  ctx.font = `italic ${9*s}px serif`;
  ctx.textAlign = 'end';
  ctx.fillText('setpoint', size - 30*s, cy - 46*s);

  // curva amortiguada que se redibuja con perturbación periódica
  const cycle = ((elapsed * 0.4) % 1);
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.3;
  ctx.beginPath();
  const N = 80;
  for (let i = 0; i < N; i++) {
    const x = 30*s + (i/(N-1))*(size - 60*s);
    const damping = Math.exp(-i*0.018 - cycle*0.5);
    const phase = i*0.4 + cycle * Math.PI * 4;
    const y = (cy - 40*s) + Math.sin(phase) * 18*s * damping;
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
  }
  ctx.stroke();

  // sensor + ajuste en el loop
  const lx = cx, ly = cy + 60*s;
  ctx.strokeStyle = color;
  ctx.lineWidth = 1;
  ctx.strokeRect(lx - 90*s, ly - 12*s, 50*s, 24*s);
  ctx.strokeRect(lx + 40*s, ly - 12*s, 50*s, 24*s);
  ctx.fillStyle = color;
  ctx.textAlign = 'center';
  ctx.fillText('sensor', lx - 65*s, ly + 4*s);
  ctx.fillText('ajuste', lx + 65*s, ly + 4*s);

  // flechas del loop
  ctx.lineWidth = 0.9;
  ctx.beginPath();
  ctx.moveTo(lx - 40*s, ly);
  ctx.lineTo(lx + 40*s, ly);
  ctx.stroke();
  _arrowHead(ctx, lx + 40*s, ly, 0, 5*s, color);

  ctx.beginPath();
  ctx.moveTo(lx + 90*s, ly);
  ctx.quadraticCurveTo(lx + 120*s, ly, lx + 120*s, ly - 30*s);
  ctx.quadraticCurveTo(lx + 120*s, ly - 60*s, lx - 120*s, ly - 60*s);
  ctx.quadraticCurveTo(lx - 120*s, ly, lx - 90*s, ly);
  ctx.stroke();
  _arrowHead(ctx, lx - 90*s, ly, Math.PI, 5*s, color);

  // pulso viajando por el loop
  const u = (elapsed * 0.5) % 1;
  let px, py;
  // simplificado: parameter en 4 fases
  if (u < 0.25) {
    const a = u / 0.25;
    px = lx - 40*s + a * 80*s; py = ly;
  } else if (u < 0.5) {
    const a = (u - 0.25) / 0.25;
    px = lx + 90*s + a * 30*s; py = ly + (1-a) * 0; // recto luego curva
    px = lx + 90*s + Math.sin(a*Math.PI*0.5)*30*s;
    py = ly - (1-Math.cos(a*Math.PI*0.5))*30*s;
  } else if (u < 0.75) {
    const a = (u - 0.5) / 0.25;
    px = lx + 120*s - a * 240*s; py = ly - 60*s;
  } else {
    const a = (u - 0.75) / 0.25;
    px = lx - 120*s + Math.sin(a*Math.PI*0.5)*30*s;
    py = ly - 60*s + (1-Math.cos(a*Math.PI*0.5))*60*s;
  }
  ctx.fillStyle = '#fff';
  ctx.shadowColor = color;
  ctx.shadowBlur = 8;
  ctx.beginPath();
  ctx.arc(px, py, 3*s, 0, Math.PI*2);
  ctx.fill();
  ctx.shadowBlur = 0;
}

// ============================================================
// XII · EL RE-ENCUADRE — dos triángulos cruzados, figura/fondo alternan
// ============================================================
function drawReencuadre(ctx, size, t, color) {
  const cx = size/2, cy = size/2;
  const s = size/300;

  // alterna el énfasis entre triángulo A y triángulo ¬A
  const phaseA = (Math.sin(t * Math.PI * 2) + 1) / 2; // 0..1..0
  const alphaA = 0.3 + phaseA * 0.7;
  const alphaB = 0.3 + (1 - phaseA) * 0.7;

  // triángulo A (apuntando arriba)
  ctx.strokeStyle = _hexA(color, alphaA);
  ctx.lineWidth = 1.4 + alphaA;
  ctx.beginPath();
  ctx.moveTo(cx, cy - 80*s);
  ctx.lineTo(cx - 72*s, cy + 50*s);
  ctx.lineTo(cx + 72*s, cy + 50*s);
  ctx.closePath();
  ctx.stroke();

  // triángulo ¬A (apuntando abajo)
  ctx.strokeStyle = _hexA(color, alphaB);
  ctx.lineWidth = 1.4 + alphaB;
  ctx.beginPath();
  ctx.moveTo(cx, cy + 80*s);
  ctx.lineTo(cx - 72*s, cy - 50*s);
  ctx.lineTo(cx + 72*s, cy - 50*s);
  ctx.closePath();
  ctx.stroke();

  // hexágono central que alterna fill
  const a = 32 * s;
  ctx.fillStyle = _hexA(color, 0.15 + phaseA*0.4);
  ctx.beginPath();
  for (let i = 0; i < 6; i++) {
    const ang = (i/6)*Math.PI*2 + Math.PI/6;
    const x = cx + Math.cos(ang)*a;
    const y = cy + Math.sin(ang)*a;
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
  }
  ctx.closePath();
  ctx.fill();

  // labels A / ¬A
  ctx.fillStyle = _hexA(color, alphaA);
  ctx.font = `italic ${10*s}px serif`;
  ctx.textAlign = 'center';
  ctx.fillText('A', cx, cy - 95*s);
  ctx.fillStyle = _hexA(color, alphaB);
  ctx.fillText('¬A', cx, cy + 108*s);

  // flecha rotación
  ctx.strokeStyle = _hexA(color, 0.75);
  ctx.lineWidth = 0.7;
  ctx.beginPath();
  ctx.moveTo(cx - 105*s, cy + 10*s);
  ctx.quadraticCurveTo(cx - 118*s, cy, cx - 105*s, cy - 10*s);
  ctx.stroke();
  _arrowHead(ctx, cx - 105*s, cy - 10*s, -Math.PI/2 - 0.2, 5*s, _hexA(color, 0.75));
}

// ============================================================
// XIII · LA DISOLUCIÓN — fragmentos como semillas dispersándose
// ============================================================
function drawDisolucion(ctx, size, t, color, elapsed) {
  const cx = size/2, cy = size/2;
  const s = size/300;

  // marco débil
  ctx.strokeStyle = _hexA(color, 0.45);
  ctx.lineWidth = 0.4;
  ctx.setLineDash([2, 3]);
  ctx.strokeRect(cx - 65*s, cy - 65*s, 130*s, 130*s);
  ctx.setLineDash([]);

  const r = _prng(13);
  const seeds = Array.from({length: 14}).map(() => {
    const a = r() * Math.PI * 2;
    const dMax = 40 + r() * 80;
    return { a, dMax, rExtra: r(), aJitter: r() };
  });

  // Fase de explosión + nuevo ciclo
  const burst = _eio(t);

  seeds.forEach(seed => {
    const a = seed.a + Math.sin(elapsed * 0.5 + seed.aJitter*6) * 0.05;
    const dist = (8 + burst * (seed.dMax - 8)) * s;
    const x = cx + Math.cos(a) * dist;
    const y = cy + Math.sin(a) * dist;

    // línea de salida
    ctx.strokeStyle = _hexA(color, 0.5 * (1 - burst*0.3));
    ctx.lineWidth = 0.4;
    ctx.setLineDash([1, 2]);
    ctx.beginPath();
    ctx.moveTo(cx + Math.cos(a)*18*s, cy + Math.sin(a)*18*s);
    ctx.lineTo(x, y);
    ctx.stroke();
    ctx.setLineDash([]);

    // semilla elíptica orientada radialmente
    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(a);
    ctx.fillStyle = _hexA(color, 0.85);
    ctx.beginPath();
    ctx.ellipse(0, 0, (4 + seed.rExtra*2)*s, (2 + seed.rExtra)*s, 0, 0, Math.PI*2);
    ctx.fill();
    ctx.restore();
  });

  // núcleo que se disuelve
  const coreA = 0.55 * (1 - burst*0.6);
  ctx.fillStyle = _hexA(color, coreA);
  ctx.beginPath();
  ctx.arc(cx, cy, 4*s, 0, Math.PI*2);
  ctx.fill();
}

// ============================================================
// XIV · ACOPLE DE ESCALAS — zoom progresivo macro → meso → micro
// ============================================================
function drawAcopleEsc(ctx, size, t, color) {
  const cx = size/2, cy = size/2;
  const s = size/300;

  // macro
  ctx.strokeStyle = _hexA(color, 0.55);
  ctx.lineWidth = 0.7;
  ctx.strokeRect(cx - 130*s, cy - 130*s, 260*s, 260*s);
  ctx.fillStyle = _hexA(color, 0.7);
  ctx.font = `${8*s}px monospace`;
  ctx.textAlign = 'start';
  ctx.fillText('MACRO ×1', cx - 126*s, cy - 115*s);

  // anillos guía
  [60, 90, 120].forEach((r) => {
    ctx.strokeStyle = _hexA(color, 0.25);
    ctx.lineWidth = 0.3;
    ctx.beginPath();
    ctx.arc(cx, cy, r*s, 0, Math.PI*2);
    ctx.stroke();
  });

  // meso
  ctx.strokeStyle = color;
  ctx.lineWidth = 0.95;
  ctx.strokeRect(cx - 60*s, cy - 60*s, 120*s, 120*s);
  ctx.fillStyle = _hexA(color, 0.85);
  ctx.fillText('MESO ×10', cx - 56*s, cy - 46*s);

  // diagonales conectoras macro→meso (con pulso)
  ctx.strokeStyle = _hexA(color, 0.6);
  ctx.lineWidth = 0.4;
  ctx.setLineDash([2, 2]);
  ctx.beginPath();
  ctx.moveTo(cx - 130*s, cy - 130*s); ctx.lineTo(cx - 60*s, cy - 60*s);
  ctx.moveTo(cx + 130*s, cy - 130*s); ctx.lineTo(cx + 60*s, cy - 60*s);
  ctx.stroke();
  ctx.setLineDash([]);

  // micro (zoom progresivo)
  const zoom = _eio(t);
  const microSize = (18 + zoom * 26) * s;
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.3;
  ctx.strokeRect(cx - microSize, cy - microSize, microSize*2, microSize*2);

  ctx.strokeStyle = _hexA(color, 0.7);
  ctx.lineWidth = 0.4;
  ctx.setLineDash([2, 2]);
  ctx.beginPath();
  ctx.moveTo(cx - 60*s, cy - 60*s); ctx.lineTo(cx - 22*s, cy - 22*s);
  ctx.moveTo(cx + 60*s, cy - 60*s); ctx.lineTo(cx + 22*s, cy - 22*s);
  ctx.stroke();
  ctx.setLineDash([]);

  // partículas micro (orbitan)
  [[-12,-8],[6,-10],[-4,4],[10,8]].forEach(([dx, dy], i) => {
    const angle = t * Math.PI * 2 + i * 1.6;
    const ox = Math.cos(angle) * 2 * s;
    const oy = Math.sin(angle) * 2 * s;
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(cx + dx*s + ox, cy + dy*s + oy, 1.5*s, 0, Math.PI*2);
    ctx.fill();
  });

  ctx.fillStyle = _hexA(color, 0.85);
  ctx.font = `${7*s}px monospace`;
  ctx.textAlign = 'start';
  ctx.fillText('×100', cx - 18*s, cy - 8*s);

  // marca de escala
  ctx.strokeStyle = color;
  ctx.lineWidth = 0.6;
  ctx.setLineDash([2, 2]);
  ctx.beginPath();
  ctx.moveTo(cx, cy + 22*s);
  ctx.lineTo(cx, cy + 60*s);
  ctx.moveTo(cx, cy + 60*s);
  ctx.lineTo(cx, cy + 130*s);
  ctx.stroke();
  ctx.setLineDash([]);
}

// ============================================================
// XV · EL RUNAWAY — curva exponencial creciente + bucle (+)
// ============================================================
function drawRunaway(ctx, size, t, color, elapsed) {
  const cx = size/2, cy = size/2;
  const s = size/300;

  // ejes
  ctx.strokeStyle = _hexA(color, 0.5);
  ctx.lineWidth = 0.5;
  ctx.beginPath();
  ctx.moveTo(40*s, size - 40*s);
  ctx.lineTo(size - 20*s, size - 40*s);
  ctx.moveTo(40*s, size - 40*s);
  ctx.lineTo(40*s, 28*s);
  ctx.stroke();

  // curva exp con avance progresivo
  const N = 80;
  const visible = Math.floor(_eio(t) * N);

  ctx.strokeStyle = color;
  ctx.lineWidth = 1.7;
  ctx.beginPath();
  let lx = 0, ly = 0;
  for (let i = 0; i < visible; i++) {
    const u = i/(N-1);
    const x = 40*s + u*(size - 70*s);
    const y = Math.max(22*s, (size - 40*s) - Math.exp(u*3.5)*5*s);
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
    lx = x; ly = y;
  }
  ctx.stroke();

  // punta brillante
  if (visible > 0 && visible < N) {
    ctx.fillStyle = '#fff';
    ctx.shadowColor = color;
    ctx.shadowBlur = 10;
    ctx.beginPath();
    ctx.arc(lx, ly, 3*s, 0, Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
  }

  // flecha disparada al final
  if (t > 0.85) {
    ctx.strokeStyle = color;
    ctx.lineWidth = 1.6;
    ctx.beginPath();
    ctx.moveTo(size - 30*s, 50*s);
    ctx.lineTo(size - 22*s, 26*s);
    ctx.stroke();
    _arrowHead(ctx, size - 22*s, 26*s, -Math.PI/2 - 0.3, 5*s, color);
  }

  // bucle (+) rotando
  const loopAngle = (elapsed * Math.PI * 1.2) % (Math.PI*2);
  const lcx = cx - 50*s, lcy = cy + 15*s;
  ctx.strokeStyle = color;
  ctx.lineWidth = 0.9;
  ctx.beginPath(); ctx.arc(lcx, lcy, 28*s, 0, Math.PI*2); ctx.stroke();
  // flecha rotando
  ctx.lineWidth = 1.3;
  ctx.beginPath();
  ctx.arc(lcx, lcy, 28*s, loopAngle, loopAngle + Math.PI*0.5);
  ctx.stroke();
  const ax = lcx + Math.cos(loopAngle + Math.PI*0.5) * 28*s;
  const ay = lcy + Math.sin(loopAngle + Math.PI*0.5) * 28*s;
  _arrowHead(ctx, ax, ay, loopAngle + Math.PI*0.5 + Math.PI/2, 5*s, color);

  ctx.fillStyle = color;
  ctx.font = `italic ${20*s}px serif`;
  ctx.textAlign = 'center';
  ctx.fillText('+', lcx, lcy + 5*s);
}

// ============================================================
// XVI · EL COLAPSO — Torre que se desmorona
// ============================================================
function drawColapso(ctx, size, t, color, elapsed) {
  const cx = size/2;
  const s = size/300;
  const cy = size/2;

  // Torre con bloques que caen progresivamente
  const blocks = 8;
  const bw = 70 * s;
  const bh = 22 * s;
  const baseY = size - 50*s;

  for (let i = 0; i < blocks; i++) {
    const startCollapse = i / blocks; // los de arriba colapsan primero (más bajos en idx)
    const collapseT = Math.max(0, Math.min(1, (t - 0.2 - (blocks - 1 - i)/blocks * 0.5) * 2));
    const fall = collapseT;
    const yBase = baseY - i * bh - bh;

    // posición con caída y rotación
    const dx = Math.sin(i * 1.7) * 80 * s * fall;
    const dy = fall * fall * (size - yBase - 30*s);
    const rot = (i % 2 ? 1 : -1) * fall * 0.6;

    const alpha = Math.max(0, 1 - fall * 0.5);

    ctx.save();
    ctx.translate(cx + dx, yBase + dy);
    ctx.rotate(rot);
    ctx.strokeStyle = _hexA(color, alpha);
    ctx.fillStyle = _hexA(color, 0.1 * alpha);
    ctx.lineWidth = 1.2;
    ctx.strokeRect(-bw/2, -bh, bw, bh);
    ctx.fillRect(-bw/2, -bh, bw, bh);
    // detalle (ventana)
    if (i % 2 === 0) {
      ctx.strokeRect(-bw/4, -bh + 5*s, bw/8, bh - 10*s);
    }
    ctx.restore();
  }

  // base sólida
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.moveTo(cx - 90*s, baseY);
  ctx.lineTo(cx + 90*s, baseY);
  ctx.stroke();

  // chispa / rayo arriba si está empezando el colapso
  if (t < 0.3) {
    const flash = (1 - t/0.3);
    ctx.strokeStyle = _hexA(color, flash);
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(cx - 18*s, 60*s);
    ctx.lineTo(cx + 4*s, 80*s);
    ctx.lineTo(cx - 8*s, 88*s);
    ctx.lineTo(cx + 12*s, 110*s);
    ctx.stroke();
  }
}

// ============================================================
// XVII · EL ATRACTOR EXTRAÑO — Lorenz butterfly
// ============================================================
function drawAtractorExtrano(ctx, size, t, color) {
  const cx = size/2, cy = size/2 + 10*size/300;
  // Precalcular Lorenz
  const pts = [];
  let x = 0.1, y = 0, z = 0;
  for (let i = 0; i < 1200; i++) {
    const dt = 0.008;
    const dx = 10*(y-x);
    const dy = x*(28-z) - y;
    const dz = x*y - 2.6*z;
    x += dx*dt; y += dy*dt; z += dz*dt;
    pts.push([cx + x*3*size/300, cy + (z-25)*3*size/300]);
  }
  // Dibujar todo con baja opacidad
  ctx.strokeStyle = _hexA(color, 0.35);
  ctx.lineWidth = 0.5;
  ctx.beginPath();
  pts.forEach(([px, py], i) => i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py));
  ctx.stroke();

  // Partícula viajando
  const idx = Math.floor(t * (pts.length - 1));
  const TRAIL = 60;
  ctx.lineWidth = 1.3;
  for (let k = 1; k < TRAIL; k++) {
    const i0 = idx - (k-1);
    const i1 = idx - k;
    if (i0 < 0 || i1 < 0) break;
    const a = (1 - k/TRAIL) * 0.9;
    ctx.strokeStyle = _hexA(color, a);
    ctx.beginPath();
    ctx.moveTo(pts[i0][0], pts[i0][1]);
    ctx.lineTo(pts[i1][0], pts[i1][1]);
    ctx.stroke();
  }
  // cabeza brillante
  const [hx, hy] = pts[idx];
  ctx.fillStyle = '#fff';
  ctx.shadowColor = color;
  ctx.shadowBlur = 12;
  ctx.beginPath();
  ctx.arc(hx, hy, 3.5*size/300, 0, Math.PI*2);
  ctx.fill();
  ctx.shadowBlur = 0;
}

// ============================================================
// XVIII · LA NO-LINEALIDAD — curva sigmoide + umbral
// ============================================================
function drawNoLinealidad(ctx, size, t, color, elapsed) {
  const cx = size/2, cy = size/2;
  const s = size/300;

  // ejes
  ctx.strokeStyle = _hexA(color, 0.5);
  ctx.lineWidth = 0.4;
  ctx.beginPath();
  ctx.moveTo(40*s, size - 40*s);
  ctx.lineTo(size - 20*s, size - 40*s);
  ctx.moveTo(40*s, size - 40*s);
  ctx.lineTo(40*s, 28*s);
  ctx.stroke();

  // sigmoide
  const N = 80;
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.7;
  ctx.beginPath();
  for (let i = 0; i < N; i++) {
    const u = i/(N-1);
    const x = 40*s + u*(size - 70*s);
    const v = 1/(1 + Math.exp(-(u-0.5)*14));
    const y = (size - 40*s) - v*(size - 80*s);
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
  }
  ctx.stroke();

  // ejemplo de Δx pequeño LEJOS del umbral (alternating)
  const phase = (elapsed * 0.4) % 2;
  const phaseOn = phase < 1 ? 'far' : 'near';
  const blink = phase < 1 ? (1 - phase) : (2 - phase);

  if (phaseOn === 'far') {
    ctx.strokeStyle = _hexA(color, 0.7 * blink);
    ctx.lineWidth = 0.8;
    ctx.setLineDash([1, 2]);
    ctx.beginPath();
    ctx.moveTo(80*s, size - 40*s); ctx.lineTo(80*s, size - 52*s);
    ctx.moveTo(80*s, size - 52*s); ctx.lineTo(40*s, size - 52*s);
    ctx.moveTo(95*s, size - 40*s); ctx.lineTo(95*s, size - 58*s);
    ctx.stroke();
    ctx.setLineDash([]);
    ctx.fillStyle = _hexA(color, 0.85*blink);
    ctx.font = `${7*s}px monospace`;
    ctx.textAlign = 'center';
    ctx.fillText('Δx', 88*s, size - 25*s);
  }

  // Δx alrededor del umbral (alternating, opuesto)
  if (phaseOn === 'near') {
    ctx.strokeStyle = _hexA(color, 0.9 * blink);
    ctx.lineWidth = 1;
    ctx.setLineDash([2, 2]);
    ctx.beginPath();
    ctx.moveTo(cx - 8*s, size - 40*s); ctx.lineTo(cx - 8*s, cy - 30*s);
    ctx.moveTo(cx + 8*s, size - 40*s); ctx.lineTo(cx + 8*s, cy + 40*s);
    ctx.stroke();
    ctx.setLineDash([1, 2]);
    ctx.beginPath();
    ctx.moveTo(cx - 8*s, cy - 30*s); ctx.lineTo(40*s, cy - 30*s);
    ctx.moveTo(cx + 8*s, cy + 40*s); ctx.lineTo(40*s, cy + 40*s);
    ctx.stroke();
    ctx.setLineDash([]);
    ctx.fillStyle = _hexA(color, 0.85 * blink);
    ctx.font = `${7*s}px monospace`;
    ctx.textAlign = 'center';
    ctx.fillText('Δx', cx, size - 25*s);
    ctx.font = `italic ${8*s}px serif`;
    ctx.fillStyle = _hexA(color, 0.7 * blink);
    ctx.fillText('umbral', cx - 22*s, cy - 12*s);
  }
}

// ============================================================
// XIX · LA SINCRONIZACIÓN — Kuramoto: fases random → en fase
// ============================================================
function drawSincro(ctx, size, t, color, elapsed) {
  const cx = size/2, cy = size/2;
  const R = size * (100/300);
  const N = 12;

  const phaseInit = Array.from({length: N}).map((_, i) => {
    let s = (i*9301 + 49297) % 233280;
    return (s / 233280) * Math.PI * 2;
  });
  const phaseFinal = Array.from({length: N}).map((_, i) => -Math.PI/2 + (i-5.5)*0.06);

  let sync;
  if (t < 0.55) sync = _eio(t / 0.55);
  else if (t < 0.9) sync = 1;
  else sync = 1 - (t - 0.9) / 0.1;

  ctx.strokeStyle = _hexA(color, 0.5);
  ctx.lineWidth = 0.5;
  ctx.beginPath();
  ctx.arc(cx, cy, R, 0, Math.PI*2);
  ctx.stroke();

  const positions = phaseInit.map((p0, i) => {
    const p1 = phaseFinal[i];
    let d = p1 - p0;
    while (d > Math.PI) d -= 2*Math.PI;
    while (d < -Math.PI) d += 2*Math.PI;
    return p0 + d * sync + elapsed * 0.5;
  });

  let sumX = 0, sumY = 0;
  positions.forEach(a => { sumX += Math.cos(a); sumY += Math.sin(a); });
  const meanX = sumX / N, meanY = sumY / N;
  const orderR = Math.sqrt(meanX*meanX + meanY*meanY);
  const meanAng = Math.atan2(meanY, meanX);

  positions.forEach(ang => {
    const x = cx + Math.cos(ang)*R;
    const y = cy + Math.sin(ang)*R;
    ctx.strokeStyle = _hexA(color, 0.55 + orderR * 0.4);
    ctx.lineWidth = 0.7;
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.lineTo(x, y);
    ctx.stroke();
  });

  positions.forEach(ang => {
    const x = cx + Math.cos(ang)*R;
    const y = cy + Math.sin(ang)*R;
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(x, y, 3.6, 0, Math.PI*2);
    ctx.fill();
  });

  const arrowLen = R * 0.66 * orderR;
  if (orderR > 0.05) {
    const ax = cx + Math.cos(meanAng) * arrowLen;
    const ay = cy + Math.sin(meanAng) * arrowLen;
    ctx.strokeStyle = color;
    ctx.lineWidth = 1.4 + orderR * 2.2;
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.lineTo(ax, ay);
    ctx.stroke();
    if (orderR > 0.15) {
      _arrowHead(ctx, ax, ay, meanAng, 8 + orderR * 4, color);
    }
  }

  ctx.fillStyle = _hexA(color, 0.85);
  ctx.font = `italic ${Math.round(size * (11/300))}px "EB Garamond", serif`;
  ctx.textAlign = 'left';
  ctx.fillText(`R≈${orderR.toFixed(2)}`, cx + 8, cy - 6);
}

// ============================================================
// XX · TRANSICIÓN DE FASE — desorden → orden (gas → lattice)
// ============================================================
function drawTransFase(ctx, size, t, color, elapsed) {
  const cx = size/2;
  const s = size/300;
  const a = 22 * s;

  // Lado gas (izquierda)
  const r = _prng(20);
  const gas = Array.from({length: 26}).map(() => ({
    x: 28*s + r()*(cx - 46*s),
    y: 28*s + r()*240*s,
    ph: r() * Math.PI * 2,
  }));
  gas.forEach(p => {
    const jx = Math.cos(elapsed*3 + p.ph)*2*s;
    const jy = Math.sin(elapsed*2.7 + p.ph*1.3)*2*s;
    ctx.fillStyle = _hexA(color, 0.7);
    ctx.beginPath();
    ctx.arc(p.x + jx, p.y + jy, 2.5*s, 0, Math.PI*2);
    ctx.fill();
  });

  // frontera Tc
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.5;
  ctx.setLineDash([4, 3]);
  ctx.beginPath();
  ctx.moveTo(cx, 20*s);
  ctx.lineTo(cx, size - 20*s);
  ctx.stroke();
  ctx.setLineDash([]);
  ctx.fillStyle = color;
  ctx.font = `italic ${9*s}px serif`;
  ctx.textAlign = 'center';
  ctx.fillText('Tc', cx, 16*s);

  // lado lattice — aparece progresivamente
  const lattice = [];
  for (let i = 0; i < 6; i++) for (let j = 0; j < 11; j++) {
    const x = cx + 18*s + i*a;
    const y = 30*s + j*a + (i%2 ? a/2 : 0);
    if (x < size-12*s && y < size-18*s) lattice.push([x, y, i, j]);
  }
  const totalT = (t * 1.2) % 1;
  lattice.forEach(([x, y, i, j], idx) => {
    const cellT = idx / lattice.length;
    const local = Math.max(0, Math.min(1, (totalT - cellT) * 3));
    const alpha = _eout(local);
    if (alpha <= 0.02) return;
    ctx.fillStyle = _hexA(color, alpha);
    ctx.beginPath();
    ctx.arc(x, y, 2.5*s, 0, Math.PI*2);
    ctx.fill();
  });

  // líneas verticales del lattice (más suaves)
  for (let i = 0; i < 6; i++) {
    const x = cx + 18*s + i*a;
    ctx.strokeStyle = _hexA(color, 0.35 * _eout(totalT));
    ctx.lineWidth = 0.3;
    ctx.beginPath();
    ctx.moveTo(x, 30*s);
    ctx.lineTo(x, size - 30*s);
    ctx.stroke();
  }

  // etiquetas
  ctx.fillStyle = _hexA(color, 0.7);
  ctx.font = `italic ${9*s}px serif`;
  ctx.textAlign = 'center';
  ctx.fillText('desorden', cx/2 + 10*s, size - 10*s);
  ctx.fillStyle = _hexA(color, 0.85);
  ctx.fillText('orden', cx + (size-cx)/2 + 10*s, size - 10*s);
}

// ============================================================
// XXI · CLAUSURA OPERACIONAL — ciclo cerrado de procesos
// ============================================================
function drawClausura(ctx, size, t, color, elapsed) {
  const cx = size/2, cy = size/2;
  const s = size/300;
  const N = 6;
  const R = 70 * s;

  const nodes = Array.from({length: N}).map((_, i) => {
    const a = (i/N)*Math.PI*2 - Math.PI/2;
    return { x: cx + Math.cos(a)*R, y: cy + Math.sin(a)*R, a };
  });

  // anillo exterior
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.4;
  ctx.beginPath();
  ctx.arc(cx, cy, 115*s, 0, Math.PI*2);
  ctx.stroke();

  // arcos α→β→γ→δ→ε→ζ→α
  nodes.forEach((n, i) => {
    const next = nodes[(i+1)%N];
    const mx = (n.x + next.x)/2, my = (n.y + next.y)/2;
    const ctrlAng = n.a + Math.PI/2;
    const cx2 = mx + Math.cos(ctrlAng)*16*s;
    const cy2 = my + Math.sin(ctrlAng)*16*s;
    ctx.strokeStyle = color;
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(n.x, n.y);
    ctx.quadraticCurveTo(cx2, cy2, next.x, next.y);
    ctx.stroke();
    const tangentAng = Math.atan2(next.y - cy2, next.x - cx2);
    _arrowHead(ctx, next.x, next.y, tangentAng, 5*s, color);
  });

  // nodos
  nodes.forEach((n, i) => {
    ctx.fillStyle = '#000';
    ctx.strokeStyle = color;
    ctx.lineWidth = 1.3;
    ctx.beginPath();
    ctx.arc(n.x, n.y, 12*s, 0, Math.PI*2);
    ctx.fill();
    ctx.stroke();
    ctx.fillStyle = color;
    ctx.font = `italic ${11*s}px serif`;
    ctx.textAlign = 'center';
    ctx.fillText(['α','β','γ','δ','ε','ζ'][i], n.x, n.y + 4*s);
  });

  // pulso viajando por el ciclo
  const segT = (elapsed * 0.4) % 1;
  const segIdx = Math.floor(segT * N);
  const local = (segT * N) - segIdx;
  const n0 = nodes[segIdx];
  const n1 = nodes[(segIdx+1)%N];
  const mx = (n0.x + n1.x)/2, my = (n0.y + n1.y)/2;
  const ctrlAng = n0.a + Math.PI/2;
  const cxQ = mx + Math.cos(ctrlAng)*16*s;
  const cyQ = my + Math.sin(ctrlAng)*16*s;
  const mt = 1 - local;
  const px = mt*mt*n0.x + 2*mt*local*cxQ + local*local*n1.x;
  const py = mt*mt*n0.y + 2*mt*local*cyQ + local*local*n1.y;
  ctx.fillStyle = '#fff';
  ctx.shadowColor = color;
  ctx.shadowBlur = 10;
  ctx.beginPath();
  ctx.arc(px, py, 4*s, 0, Math.PI*2);
  ctx.fill();
  ctx.shadowBlur = 0;

  // centro
  ctx.strokeStyle = _hexA(color, 0.8);
  ctx.lineWidth = 0.8;
  ctx.beginPath();
  ctx.arc(cx, cy, 6*s, 0, Math.PI*2);
  ctx.stroke();
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.arc(cx, cy, 2*s, 0, Math.PI*2);
  ctx.fill();
}

// ─── Crear componentes y registrar en PILOT_SIGILS ──────────────────────
function _mkSigil(draw, period) {
  return ({ color, size = 300 }) => React.createElement(
    AnimatedSigilCanvas, { size, color, draw, period }
  );
}

Object.assign(PILOT_SIGILS, {
  'EL CAOS':               _mkSigil(drawCaos, 10),
  'EL OBSERVADOR':         _mkSigil(drawObservador, 5),
  'LA INFORMACIÓN':        _mkSigil(drawInformacion, 4),
  'AUTO-ORGANIZACIÓN':     _mkSigil(drawAutoOrg, 6),
  'LA JERARQUÍA':          _mkSigil(drawJerarquia, 6),
  'EL LOCK-IN':            _mkSigil(drawLockIn, 6),
  'EL ACOPLAMIENTO':       _mkSigil(drawAcoplamiento, 0),
  'EL ATRACTOR':           _mkSigil(drawAtractor, 9),
  'LA ROBUSTEZ':           _mkSigil(drawRobustez, 0),
  'LEJOS DEL EQUILIBRIO':  _mkSigil(drawLejosEq, 0),
  'LA BIFURCACIÓN':        _mkSigil(drawBifurcacion, 7),
  'LA HOMEOSTASIS':        _mkSigil(drawHomeostasis, 0),
  'EL RE-ENCUADRE':        _mkSigil(drawReencuadre, 5),
  'LA DISOLUCIÓN':         _mkSigil(drawDisolucion, 7),
  'ACOPLE DE ESCALAS':     _mkSigil(drawAcopleEsc, 8),
  'EL RUNAWAY':            _mkSigil(drawRunaway, 5),
  'EL COLAPSO':            _mkSigil(drawColapso, 6),
  'EL ATRACTOR EXTRAÑO':   _mkSigil(drawAtractorExtrano, 14),
  'LA NO-LINEALIDAD':      _mkSigil(drawNoLinealidad, 0),
  'LA SINCRONIZACIÓN':     _mkSigil(drawSincro, 8),
  'TRANSICIÓN DE FASE':    _mkSigil(drawTransFase, 6),
  'CLAUSURA OPERACIONAL':  _mkSigil(drawClausura, 0),
});

Object.assign(window, {
  AnimatedSigilCanvas,
  drawCaos, drawObservador, drawInformacion, drawAutoOrg, drawJerarquia,
  drawLockIn, drawAcoplamiento, drawAtractor, drawRobustez, drawLejosEq,
  drawBifurcacion, drawHomeostasis, drawReencuadre, drawDisolucion,
  drawAcopleEsc, drawRunaway, drawColapso, drawAtractorExtrano,
  drawNoLinealidad, drawSincro, drawTransFase, drawClausura,
});
