// act3-migracion.jsx — Acto 3 (62.5s): Migración end-to-end.
//
// Sub-timeline (ABSOLUTE global times — outer Sprite window 77-139.5):
//   77.0 - 89.0    (12.0s) Phone-only — WhatsApp "Me casé!" Tomás → Amelia
//   89.0 - 91.5    (2.5s)  Modal Selector Módulo → click Migración
//   91.5 - 94.0    (2.5s)  Modal Selector Forma → click Paquete Residencia
//   94.0 - 99.0    (5.0s)  Modal Configurador → 6 checks + start
//   99.0 - 113.0   (14.0s) Dashboard waiting · phone overlays 101-113
//                           for contrato Residencia + firma + thanks (+1s hold)
//   113.0 - 120.5  (7.5s)  Modal Sophia Importar — SLOW reading (+2s, attention pulse)
//   120.5 - 133.0  (12.5s) Cascada 13 bloques + 3 cross-fill banners ILUMINADOS
//                           (cada uno respira + chispitas + shimmer) + sparkle
//                           burst en Block 11
//   133.0 - 139.5  (6.5s)  Zoom Bloque 11 · Soporte Financiero + badge
//                           "info disponible en TODOS los módulos"
//
// Rule: phone never overlaps desktop work scenes.

function Act3Migracion() {
  const { localTime } = useSprite();

  return (
    <>
      <Sprite start={77} end={89}>
        <Scene3_PhoneOpening />
      </Sprite>
      <Sprite start={89} end={91.5}>
        <Scene3a_SelectorModulo />
      </Sprite>
      <Sprite start={91.5} end={94}>
        <Scene3b_SelectorForma />
      </Sprite>
      <Sprite start={94} end={99}>
        <Scene3c_Configurador />
      </Sprite>
      <Sprite start={99} end={113}>
        <Scene3d_DashboardWaiting />
      </Sprite>
      <Sprite start={113} end={120.5}>
        <Scene3e_SophiaImportar />
      </Sprite>
      <Sprite start={120.5} end={133}>
        <Scene3f_Cascade />
      </Sprite>
      <Sprite start={133} end={139.5}>
        <Scene3g_BloqueZoom />
      </Sprite>

      <CalloutsTrack localTime={localTime} callouts={[
        // Times act-local (relative to outer Sprite start = 77)
        { appearAt: 0.7,  duration: 11,   icon: '📩', tKey: 'tomas_casado' },
        { appearAt: 12.3, duration: 9.5,  icon: '🌐', tKey: 'datos_viajan' },
        { appearAt: 23.5, duration: 12.5, icon: '✍️', tKey: 'contrato_residencia' },
        { appearAt: 36.3, duration: 7,    icon: '📂', tKey: 'sophia_importa', position: 'top-right', attention: true },
        { appearAt: 43.8, duration: 3.5,  icon: '📊', tKey: 'cascada_13',     position: 'top-right' },
        { appearAt: 56.2, duration: 6,    icon: '💵', tKey: 'bloque_11_i864', position: 'top-right' },
      ]} />

      {/* Global cursor for Act 3 — actStart=77 maps cursor t=0 to global 77 */}
      <LocalCursorTrack actStart={77} track={[
        // (Phone-only 77-89: cursor invisible — Sophia/Amelia not acting)

        // Selector Módulo (89-91.5): click Migración — act-local 12-14.5
        { t: 12.4, x: 870, y: 290 },                      // appear
        { t: 13.0, x: 870, y: 350 },                      // hover Migración
        { t: 13.7, x: 870, y: 350, click: true },         // click

        // Selector Forma (91.5-94): click Paquete Residencia — act-local 14.5-17
        { t: 15.0, x: 640, y: 290 },                      // appear
        { t: 15.6, x: 640, y: 290 },                      // hover Paquete (featured)
        { t: 16.2, x: 640, y: 290, click: true },         // click

        // Configurador (94-99): 6 checks + Empezar — act-local 17-22
        { t: 17.7, x: 470, y: 365, click: true },         // I-130A
        { t: 18.2, x: 700, y: 365, click: true },         // I-864
        { t: 18.7, x: 470, y: 425, click: true },         // I-765
        { t: 19.2, x: 700, y: 425, click: true },         // I-131
        { t: 19.7, x: 410, y: 525, click: true },         // G-1145
        { t: 20.2, x: 730, y: 535, click: true },         // Tarjeta de crédito
        { t: 21.0, x: 640, y: 645 },                      // approach Empezar
        { t: 21.5, x: 640, y: 645, click: true },         // click Empezar

        // (cursor invisible 22-35 — office is waiting, phone is active)

        // Sophia Importar (113-120.5): act-local 36-43.5 (extended +2s)
        { t: 36.5, x: 640, y: 380 },                      // appear over modal
        { t: 37.5, x: 380, y: 408, click: true },         // check W-2
        { t: 38.5, x: 380, y: 440, click: true },         // check 1099
        { t: 39.5, x: 380, y: 472, click: true },         // check 1040
        { t: 40.5, x: 380, y: 504, click: true },         // check ID
        { t: 41.5, x: 380, y: 536, click: true },         // check 1095-A
        { t: 42.5, x: 640, y: 605, click: true },         // click Importar

        // (cursor invisible during cascade 43.5-56 and zoom 56-62.5)
      ]} />
    </>
  );
}

// ── Sub-scenes ───────────────────────────────────────────────────────

// Scene 3 opener — WhatsApp arrives. Phone is the protagonist (driven by
// engine's getPhoneAt). Render a quiet, faded Migración dashboard as the
// office backdrop so the phone's backdrop dim has context.
function Scene3_PhoneOpening() {
  const { localTime, duration } = useSprite();
  const opacity = fadeInOut(localTime, duration, 0.4, 0.4);
  const blocks = BLOCKS_13.map(b => ({ status: b.na ? 'na' : 'pending' }));
  return (
    <Wrap opacity={opacity}>
      <DashboardBg progress={0} blocks={blocks} faded />
      <div style={{
        position: 'absolute',
        left: '50%',
        top: 130,
        transform: 'translateX(-50%)',
        display: 'flex',
        alignItems: 'center',
        gap: 10,
        padding: '8px 16px',
        background: '#fff',
        border: '1px solid #fcd34d',
        borderRadius: 999,
        fontSize: 12.5,
        color: '#9a3412',
        boxShadow: '0 4px 16px rgba(0,0,0,0.06)',
        zIndex: 12,
      }}>
        <span style={{ fontSize: 14 }}>📩</span>
        <span style={{ fontWeight: 500, letterSpacing: -0.005 }}>
          Mensaje entrante · TOMÁS SMITH · WhatsApp
        </span>
      </div>
    </Wrap>
  );
}

// Selector Módulo — slower hover then click on Migración.
function Scene3a_SelectorModulo() {
  const { localTime, duration } = useSprite();
  const hoverModule  = (localTime >= 0.7 && localTime < 1.7) ? 'migracion' : null;
  const clickedModule = (localTime >= 1.65 && localTime < 2.1) ? 'migracion' : null;
  const opacity = fadeInOut(localTime, duration, 0.3, 0.3);
  return <Wrap opacity={opacity}><ModalSelectorModulo hoverModule={hoverModule} clickedModule={clickedModule} /></Wrap>;
}

// Selector Forma — slower hover and click on Paquete Residencia.
function Scene3b_SelectorForma() {
  const { localTime, duration } = useSprite();
  const hovered = (localTime >= 0.65 && localTime < 1.75) ? 'paquete' : null;
  const clicked = (localTime >= 1.65 && localTime < 2.1) ? 'paquete' : null;
  const opacity = fadeInOut(localTime, duration, 0.3, 0.3);
  return <Wrap opacity={opacity}><ModalSelectorForma hoveredOption={hovered} clickedOption={clicked} /></Wrap>;
}

// Configurador — 6 checks at 0.5s intervals, more breathing room.
function Scene3c_Configurador() {
  const { localTime, duration } = useSprite();
  const checked = new Set();
  if (localTime > 0.7)  checked.add('i130a');
  if (localTime > 1.2)  checked.add('i864');
  if (localTime > 1.7)  checked.add('i765');
  if (localTime > 2.2)  checked.add('i131');
  if (localTime > 2.7)  checked.add('g1145');
  if (localTime > 3.2)  checked.add('pay-credit');

  const startHover   = localTime >= 4.0 && localTime < 4.6;
  const startClicked = localTime >= 4.5 && localTime < 4.8;

  const opacity = fadeInOut(localTime, duration, 0.3, 0.3);
  return (
    <Wrap opacity={opacity}>
      <ModalConfigurador
        checked={checked}
        startButtonHover={startHover}
        startButtonClicked={startClicked}
      />
    </Wrap>
  );
}

// Dashboard "esperando" while contrato is sent + signed on the phone.
// The phone is the protagonist 101-112; the dashboard sits dim behind it.
function Scene3d_DashboardWaiting() {
  const { localTime, duration } = useSprite();
  const blocks = BLOCKS_13.map(b => ({ status: b.na ? 'na' : 'pending' }));
  const opacity = fadeInOut(localTime, duration, 0.4, 0.4);

  // Status strip text adapts to phase
  //   0-2s   : "Caso creado · enviando contrato al cliente…"
  //   2-9s   : "✍️ Contrato enviado · esperando firma del cliente"
  //   9-13s  : "✓ Contrato firmado · armando paquete USCIS…"
  let strip;
  if (localTime < 2) {
    strip = { color: '#9a3412', bg: '#fff', border: '#fcd34d',
      icon: '📤', text: 'Caso creado · enviando contrato al cliente…' };
  } else if (localTime < 9) {
    strip = { color: '#9a3412', bg: '#fff', border: '#fcd34d',
      icon: '✍️', text: 'Contrato enviado · esperando firma del cliente desde su teléfono →' };
  } else {
    strip = { color: '#166534', bg: '#fff', border: '#86efac',
      icon: '✓', text: 'Contrato firmado · armando paquete USCIS…' };
  }

  return (
    <Wrap opacity={opacity}>
      <DashboardBg progress={0} blocks={blocks} />

      <div style={{
        position: 'absolute',
        left: '50%',
        top: 130,
        transform: 'translateX(-50%)',
        display: 'flex',
        alignItems: 'center',
        gap: 10,
        padding: '8px 16px',
        background: strip.bg,
        border: `1px solid ${strip.border}`,
        borderRadius: 999,
        fontSize: 12.5,
        color: strip.color,
        boxShadow: '0 4px 16px rgba(0,0,0,0.06)',
        zIndex: 12,
      }}>
        {localTime >= 2 && localTime < 9 ? (
          <div style={{
            width: 14, height: 14,
            border: '2px solid #fcd34d',
            borderTopColor: '#FF9500',
            borderRadius: '50%',
            animation: 'sol-spin 1s linear infinite',
          }} />
        ) : (
          <span style={{ fontSize: 14 }}>{strip.icon}</span>
        )}
        <span style={{ fontWeight: 500, letterSpacing: -0.005 }}>
          {strip.text}
        </span>
      </div>
    </Wrap>
  );
}

// Sophia Importar — slow progressive doc selection (7.5s, +2s for reading).
function Scene3e_SophiaImportar() {
  const { localTime, duration } = useSprite();
  const progress = Math.min(1, localTime / 0.5);

  const checkedDocs = new Set();
  if (localTime > 1.4)  checkedDocs.add('w2');
  if (localTime > 2.4)  checkedDocs.add('1099');
  if (localTime > 3.4)  checkedDocs.add('1040');
  if (localTime > 4.4)  checkedDocs.add('id');
  if (localTime > 5.4)  checkedDocs.add('1095a');

  const importClicked = localTime >= 6.4 && localTime < 6.8;
  const opacity = fadeInOut(localTime, duration, 0.35, 0.3);

  return (
    <Wrap opacity={opacity}>
      <ModalSophiaImportar
        progress={progress}
        checkedDocs={checkedDocs}
        importButtonClicked={importClicked}
      />
    </Wrap>
  );
}

// ── Cascada de 13 bloques (12.5s — 4 olas dramáticas + cross-fill banners
//    iluminados (pulse + chispitas + shimmer) + docs volando hacia el
//    Bloque 11 + sparkle burst en el hit) ──────────────────────────
function Scene3f_Cascade() {
  const { localTime, duration } = useSprite();

  // Wave schedule — each wave gets generous breathing room:
  // Wave 1 @ 0.7: block 1 (info)
  // Wave 2 @ 2.0: blocks 2, 3, 4 (pet, ben, addr)
  // Wave 3 @ 3.8: blocks 5, 6 (marital, employ)
  // Wave 4 @ 5.5 (DRAMATIC — docs fly + sparkles burst): block 11 (finance)
  const blocks = BLOCKS_13.map(b => {
    const status = b.na ? 'na' : 'pending';
    return { status };
  });

  function done(idx) { blocks[idx].status = 'done'; }
  if (localTime >= 0.7) done(0);
  if (localTime >= 2.0) { done(1); done(2); done(3); }
  if (localTime >= 3.8) { done(4); done(5); }
  if (localTime >= 5.5) done(10);

  // Hero flash on block 11 (longer + more dramatic — 2s ramp)
  const heroFlash = localTime >= 5.5 && localTime < 7.5
    ? Math.max(0, 1 - (localTime - 5.5) / 2)
    : 0;

  const doneCount = blocks.filter(b => b.status === 'done').length;
  const progress = (doneCount / 13) * 100;

  // Cross-fill banners — each linger ~7-9s so the reader can absorb them
  // calmly. Cascada callout fades at scene local 3.7 — banner 1 enters then.
  const banners = [];
  // Dashboard grid (DashboardBg uses left:22, right:280, 4 cols, gap:10) —
  // so the effective width is 1280 - 22 - 280 = 978, NOT 1280-44=1236.
  // Earlier this used the wrong width which placed bursts on Block 12 instead of Block 11.
  const dashboardBlocksTop = 196;
  const blockH = 124 + 10;
  const colW = (1280 - 22 - 280 - 10 * 3) / 4;

  function blockPos(idx) {
    const row = Math.floor(idx / 4);
    const col = idx % 4;
    return {
      x: 22 + col * (colW + 10) + colW * 0.7,
      y: dashboardBlocksTop + row * blockH - 8,
    };
  }

  // Banner 1: dirección heredada (block 4 — rightmost col, right-anchored).
  // Appears at 3.7 — right when Cascada callout fades.
  if (localTime >= 3.7) {
    const p = clamp((localTime - 3.7) / 8.8, 0, 1);
    banners.push({
      text: bannerText('banner_address') || 'Dirección heredada desde el módulo Taxes (TOMÁS SMITH).',
      x: 1280 - 360 - 30,
      y: blockPos(3).y - 60,
      progress: p,
    });
  }
  // Banner 2: empleo del sponsor (block 6 — middle row)
  if (localTime >= 4.6) {
    const p = clamp((localTime - 4.6) / 7.9, 0, 1);
    banners.push({
      text: bannerText('banner_employ') || 'Empleo del sponsor pre-cargado desde W-2 (Hilton Worldwide).',
      x: blockPos(5).x - 50,
      y: blockPos(5).y - 60,
      progress: p,
    });
  }
  // Banner 3: Soporte Financiero (block 11 — fires with the sparkle hit)
  if (localTime >= 5.6) {
    const p = clamp((localTime - 5.6) / 6.9, 0, 1);
    banners.push({
      text: bannerText('banner_finance') || 'Soporte Financiero — 1040, W-2 y 1099 importados desde Taxes.',
      x: blockPos(10).x - 80,
      y: blockPos(10).y - 60,
      progress: p,
    });
  }

  const opacity = fadeInOut(localTime, duration, 0.35, 0.4);
  return (
    <Wrap opacity={opacity}>
      <DashboardBg
        progress={progress}
        blocks={blocks}
        banners={banners}
        heroFlash={heroFlash}
      />

      {/* Flying docs to Bloque 11 — lands right when sparkles burst */}
      {localTime >= 4.4 && localTime < 5.8 && (
        <FlyingDocsToFinance progress={(localTime - 4.4) / 1.4} />
      )}

      {/* Sparkle / star burst when docs land on Bloque 11 (localTime 5.5 onward) */}
      {localTime >= 5.4 && localTime < 7.7 && (
        <Block11SparkleBurst localTime={localTime} startTime={5.5} />
      )}
    </Wrap>
  );
}

// Documents flying from top-left into the Finance block (block 11)
function FlyingDocsToFinance({ progress }) {
  const p = clamp(progress, 0, 1);
  const docs = [
    { name: '1040', color: '#8b5cf6' },
    { name: 'W-2',  color: '#007AFF' },
    { name: '1099', color: '#FF9500' },
  ];
  const dashboardBlocksTop = 196;
  const blockH = 124 + 10;
  // See Scene3f comment — dashboard width is 978 (right:280), not 1236.
  const colW = (1280 - 22 - 280 - 10 * 3) / 4;
  // Block 11 is index 10: row 2, col 2
  const tgtX = 22 + 2 * (colW + 10) + colW * 0.5;
  const tgtY = dashboardBlocksTop + 2 * blockH + 60;

  return (
    <>
      {docs.map((d, i) => {
        const stagger = i * 0.18;
        const localP = clamp((p - stagger) / (1 - stagger), 0, 1);
        if (localP <= 0) return null;
        const le = Easing.easeInCubic(localP);
        const srcX = 60 + i * 30;
        const srcY = 80 + i * 12;
        const x = srcX + (tgtX - srcX) * le;
        const y = srcY + (tgtY - srcY) * le;
        const opacity = localP < 0.85 ? 1 : 1 - (localP - 0.85) / 0.15;
        const scale = 1 - 0.5 * le;
        const rot = (1 - le) * 12 - 8;
        return (
          <div key={d.name} style={{
            position: 'absolute',
            left: x, top: y,
            width: 60, height: 42,
            background: '#fff',
            borderRadius: 6,
            border: `1.5px solid ${d.color}`,
            padding: 4,
            display: 'flex',
            flexDirection: 'column',
            justifyContent: 'center',
            alignItems: 'center',
            opacity,
            transform: `scale(${scale}) rotate(${rot}deg)`,
            boxShadow: `0 6px 20px ${d.color}55`,
            zIndex: 20,
            willChange: 'transform, opacity',
            pointerEvents: 'none',
          }}>
            <div style={{ fontSize: 14 }}>📄</div>
            <div style={{
              fontSize: 8,
              fontWeight: 700,
              color: d.color,
              fontFamily: 'ui-monospace, monospace',
              marginTop: 2,
            }}>{d.name}</div>
          </div>
        );
      })}
    </>
  );
}

// Zoom Bloque 11 · Soporte Financiero (6.5s)
// Phases:
//   0.0-0.6  fade in + zoom in
//   0.6-3.4  progress fills sub-sections (sponsor info, household, taxes)
//   1.5-4.5  doc badges pulse on as their corresponding section fills
//   3.4-5.9  hold — final state
//   5.9-6.5  fade out
function Scene3g_BloqueZoom() {
  const { localTime, duration } = useSprite();
  const progress = clamp((localTime - 0.6) / 2.8, 0, 1);
  const opacity = fadeInOut(localTime, duration, 0.4, 0.4);

  // Doc badge highlights — pulse when their data lands (extended windows)
  const highlights = {
    'W-2':  localTime >= 1.5 && localTime < 2.8,
    '1040': localTime >= 2.3 && localTime < 3.6,
    '1099': localTime >= 3.1 && localTime < 4.4,
  };

  return (
    <Wrap opacity={opacity}>
      <BloqueDetalleSoporteFinanciero progress={progress} />
      <DocHighlightStrip highlights={highlights} localTime={localTime} />
    </Wrap>
  );
}

// Floating strip showing which docs are feeding the form right now.
function DocHighlightStrip({ highlights, localTime }) {
  if (localTime < 0.9) return null;
  const opacity = clamp((localTime - 0.9) / 0.4, 0, 1) *
                  (localTime < 5.8 ? 1 : Math.max(0, 1 - (localTime - 5.8) / 0.5));
  const docs = [
    { name: 'W-2',  color: '#007AFF', label: 'Sponsor income' },
    { name: '1040', color: '#8b5cf6', label: 'Tax data' },
    { name: '1099', color: '#FF9500', label: 'Additional income' },
  ];
  return (
    <div style={{
      position: 'absolute',
      left: '50%', top: 90,
      transform: 'translateX(-50%)',
      display: 'flex',
      alignItems: 'center',
      gap: 12,
      padding: '10px 16px',
      background: 'rgba(255,255,255,0.95)',
      backdropFilter: 'blur(6px)',
      border: '1px solid #e8e8ed',
      borderRadius: 14,
      boxShadow: '0 8px 28px rgba(0,0,0,0.08)',
      zIndex: 30,
      opacity,
      fontSize: 11.5,
      color: '#1d1d1f',
      letterSpacing: -0.005,
    }}>
      <span style={{
        fontFamily: 'ui-monospace, monospace',
        fontSize: 10,
        textTransform: 'uppercase',
        letterSpacing: 0.14,
        color: '#6e6e73',
        fontWeight: 600,
        marginRight: 4,
      }}>Importando →</span>
      {docs.map(d => {
        const active = highlights[d.name];
        return (
          <div key={d.name} style={{
            display: 'flex',
            alignItems: 'center',
            gap: 6,
            padding: '5px 10px',
            background: active ? `${d.color}1a` : '#f5f5f7',
            border: `1px solid ${active ? d.color : '#e8e8ed'}`,
            borderRadius: 999,
            color: active ? d.color : '#6e6e73',
            fontWeight: 600,
            transform: active ? 'scale(1.06)' : 'scale(1)',
            boxShadow: active ? `0 4px 14px ${d.color}55` : 'none',
            transition: 'all 200ms ease',
          }}>
            <span style={{ fontSize: 11 }}>📄</span>
            <span style={{
              fontFamily: 'ui-monospace, monospace',
              fontSize: 10.5,
              fontWeight: 700,
              letterSpacing: 0.02,
            }}>{d.name}</span>
            <span style={{
              fontSize: 10,
              color: active ? d.color : '#a1a1a6',
              fontWeight: 500,
            }}>· {d.label}</span>
          </div>
        );
      })}
    </div>
  );
}

// Sparkle / star burst that fires when the docs land on Block 11.
// 16 four-pointed stars radiate outward, plus a radial flash + ring pulse.
// Triggers at localTime >= startTime (5.5 in Scene3f), lasting ~2.2s.
function Block11SparkleBurst({ localTime, startTime = 5.5 }) {
  const TOTAL = 2.2;
  const t = (localTime - startTime) / TOTAL;
  if (t < 0 || t > 1) return null;
  const e = Easing.easeOutCubic(clamp(t, 0, 1));

  // Block 11 position (same calc as FlyingDocsToFinance target)
  const dashboardBlocksTop = 196;
  const blockH = 124 + 10;
  // Dashboard width 978 (right:280, not 1236) — see Scene3f comment.
  const colW = (1280 - 22 - 280 - 10 * 3) / 4;
  const cx = 22 + 2 * (colW + 10) + colW * 0.5;
  const cy = dashboardBlocksTop + 2 * blockH + 60;

  const N = 16;
  const colors = ['#fbbf24', '#34C759', '#a78bfa', '#5AC8FA'];

  // 4-pointed star clip-path
  const starClip = 'polygon(50% 0%, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0% 50%, 40% 40%)';

  return (
    <>
      {/* Radial flash background — fades fast */}
      <div style={{
        position: 'absolute',
        left: cx - 160, top: cy - 160,
        width: 320, height: 320,
        borderRadius: '50%',
        background: `radial-gradient(circle, rgba(52,199,89,${Math.max(0, 0.55 - e * 0.55)}) 0%, rgba(167,139,250,${Math.max(0, 0.25 - e * 0.25)}) 35%, transparent 70%)`,
        zIndex: 22,
        pointerEvents: 'none',
        transform: `scale(${0.4 + e * 1.3})`,
        willChange: 'transform, opacity',
      }} />

      {/* Expanding ring pulse */}
      <div style={{
        position: 'absolute',
        left: cx - 100, top: cy - 100,
        width: 200, height: 200,
        borderRadius: '50%',
        border: `2px solid rgba(52, 199, 89, ${Math.max(0, 0.9 - e)})`,
        boxShadow: `0 0 ${30 + e * 60}px rgba(52, 199, 89, ${Math.max(0, 0.5 - e * 0.5)})`,
        zIndex: 23,
        pointerEvents: 'none',
        transform: `scale(${0.3 + e * 1.6})`,
        willChange: 'transform, opacity',
      }} />

      {/* 16 radiating stars */}
      {Array.from({ length: N }).map((_, i) => {
        const angle = (i / N) * Math.PI * 2;
        const stagger = (i % 3) * 0.05;
        const localT = clamp((t - stagger) / (1 - stagger), 0, 1);
        const localE = Easing.easeOutCubic(localT);
        const dist = 60 + localE * 200;
        const x = cx + Math.cos(angle) * dist;
        const y = cy + Math.sin(angle) * dist * 0.78; // slight vertical squash
        const op = localT < 0.15
          ? localT / 0.15
          : Math.max(0, 1 - (localT - 0.15) / 0.85);
        const size = 14 + (1 - localE) * 10;
        const color = colors[i % colors.length];
        const rot = i * 20 + localE * 280;
        return (
          <div key={i} style={{
            position: 'absolute',
            left: x - size / 2, top: y - size / 2,
            width: size, height: size,
            background: color,
            clipPath: starClip,
            WebkitClipPath: starClip,
            opacity: op,
            transform: `rotate(${rot}deg) scale(${0.5 + 0.5 * (1 - localT)})`,
            filter: `drop-shadow(0 0 ${6 + 6 * (1 - localT)}px ${color})`,
            zIndex: 24,
            pointerEvents: 'none',
            willChange: 'transform, opacity',
          }} />
        );
      })}

      {/* Center burst dot — bright at the moment of impact */}
      <div style={{
        position: 'absolute',
        left: cx - 28, top: cy - 28,
        width: 56, height: 56,
        borderRadius: '50%',
        background: 'radial-gradient(circle, #ffffff 0%, #34C759 40%, transparent 70%)',
        opacity: Math.max(0, 1 - e * 2.2),
        transform: `scale(${0.6 + e * 1.8})`,
        zIndex: 25,
        pointerEvents: 'none',
        filter: 'blur(0.5px)',
        willChange: 'transform, opacity',
      }} />
    </>
  );
}

// Get a migracion-section translated banner string by key (uses TimelineContext lang).
function bannerText(key) {
  const lang = (window.__solCurrentLang) || 'es';
  const i18n = (window.SOL_I18N && window.SOL_I18N[lang]) || (window.SOL_I18N && window.SOL_I18N.es) || {};
  return (i18n.migracion && i18n.migracion[key]) || null;
}

// ── Helpers ──────────────────────────────────────────────────────────
function Wrap({ children, opacity = 1 }) {
  return <div style={{ position: 'absolute', inset: 0, opacity }}>{children}</div>;
}

function fadeInOut(t, dur, fadeIn = 0.25, fadeOut = 0.25) {
  if (t < fadeIn) return Easing.easeOutCubic(Math.min(1, t / fadeIn));
  const outStart = dur - fadeOut;
  if (t > outStart) return Math.max(0, 1 - Easing.easeInCubic((t - outStart) / fadeOut));
  return 1;
}

Object.assign(window, { Act3Migracion });
