// scenes-v2-a.jsx — Scenes 1–4 of the CAMS animation
// 1. Intro · 2. ¿Qué es CAMS? · 3. Satélites + IFS · 4. Aerosoles globales (Windy)

// ─── Scene 1: Intro ─────────────────────────────────────────────────

function S1Intro() {
  const { localTime, duration } = useSprite();
  const time = useTime();

  // Ambient flow field for backdrop
  const field = React.useMemo(() =>
    directionalField(Math.PI * 0.95, 0.0035, 0.18, 0.55, 0.45), []);

  const tTitle = Easing.easeOutCubic(clamp((localTime - 0.6) / 0.8, 0, 1));
  const tKick = Easing.easeOutCubic(clamp((localTime - 0.3) / 0.6, 0, 1));
  const tSub = Easing.easeOutCubic(clamp((localTime - 1.4) / 0.7, 0, 1));
  const tFoot = Easing.easeOutCubic(clamp((localTime - 2.0) / 0.7, 0, 1));
  const fadeOut = clamp((duration - localTime) / 0.6, 0, 1);

  return (
    <SceneFade>
      {/* Background flow */}
      <WindyCanvas
        width={1280} height={720}
        count={900}
        field={field}
        palette={[C.cyan, C.sky, C.orange, C.amber, C.purple]}
        fade={0.08}
        speed={0.7}
        life={130}
        lineWidth={1}
        opacity={0.7}
        background={C.navy}
      />

      {/* Soft vignette */}
      <div style={{
        position: 'absolute', inset: 0,
        background: 'radial-gradient(ellipse at 50% 50%, rgba(5,10,30,0) 30%, rgba(5,10,30,0.85) 90%)',
        pointerEvents: 'none',
      }} />

      {/* Kicker */}
      <div style={{
        position: 'absolute', left: '50%', top: 215,
        transform: 'translate(-50%,' + ((1 - tKick) * 10) + 'px)',
        opacity: tKick * fadeOut,
        fontFamily: MONO, fontSize: 12, fontWeight: 600,
        letterSpacing: '0.32em', textTransform: 'uppercase',
        color: C.cyan,
      }}>
        Pronóstico de Calidad del Aire · SIATA
      </div>

      {/* Title */}
      <div style={{
        position: 'absolute', left: '50%', top: 268,
        transform: 'translate(-50%,' + ((1 - tTitle) * 24) + 'px)',
        opacity: tTitle * fadeOut,
        fontFamily: FONT, fontSize: 54, fontWeight: 800,
        color: C.white, textAlign: 'center', lineHeight: 1.08,
        letterSpacing: '-0.02em', whiteSpace: 'nowrap',
        textShadow: '0 4px 24px rgba(5,10,30,0.8)',
      }}>
        Cómo funciona el pronóstico
      </div>
      <div style={{
        position: 'absolute', left: '50%', top: 338,
        transform: 'translate(-50%,' + ((1 - tTitle) * 24) + 'px)',
        opacity: tTitle * fadeOut,
        fontFamily: FONT, fontSize: 54, fontWeight: 800,
        color: C.white, textAlign: 'center', lineHeight: 1.08,
        letterSpacing: '-0.02em', whiteSpace: 'nowrap',
        textShadow: '0 4px 24px rgba(5,10,30,0.8)',
      }}>
        estadístico de PM<span style={{ fontSize: 36 }}>2.5</span>
      </div>
      <div style={{
        position: 'absolute', left: '50%', top: 408,
        transform: 'translate(-50%,' + ((1 - tTitle) * 24) + 'px)',
        opacity: tTitle * fadeOut,
        fontFamily: FONT, fontSize: 54, fontWeight: 800,
        textAlign: 'center', lineHeight: 1.08,
        letterSpacing: '-0.02em', whiteSpace: 'nowrap',
        background: 'linear-gradient(90deg,' + C.cyan + ',' + C.orange + ')',
        WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
        backgroundClip: 'text', color: 'transparent',
      }}>
        en el Valle de Aburrá
      </div>

      {/* Subtitle */}
      <div style={{
        position: 'absolute', left: '50%', top: 510,
        transform: 'translateX(-50%)',
        opacity: tSub * fadeOut,
        fontFamily: FONT, fontSize: 17, fontWeight: 400,
        color: C.gray, textAlign: 'center',
        maxWidth: 780, lineHeight: 1.55,
      }}>
        Cuatro fuentes — CAMS, WRF, estaciones SIATA y variables temporales —
        integradas por aprendizaje automatizado.
      </div>

      {/* Foot tag with pulsing dot */}
      <div style={{
        position: 'absolute', left: '50%', top: 615,
        transform: 'translateX(-50%)',
        opacity: tFoot * fadeOut,
        fontFamily: MONO, fontSize: 11, letterSpacing: '0.28em',
        textTransform: 'uppercase', color: C.dim,
        display: 'flex', alignItems: 'center', gap: 14,
      }}>
        <span style={{
          display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
          background: C.orange,
          boxShadow: '0 0 12px ' + C.orange + 'aa',
          opacity: 0.6 + Math.sin(time * 3) * 0.4,
        }} />
        SIATA · Sistema de Alerta Temprana del Valle de Aburrá
      </div>
    </SceneFade>
  );
}

// ─── Scene 2: ¿Qué es CAMS? ────────────────────────────────────────

function S2What() {
  const { localTime, duration } = useSprite();
  const time = useTime();

  const tKick = Easing.easeOutCubic(clamp((localTime - 0.2) / 0.5, 0, 1));
  const tHead = Easing.easeOutCubic(clamp((localTime - 0.6) / 0.7, 0, 1));
  const tBody = Easing.easeOutCubic(clamp((localTime - 1.2) / 0.7, 0, 1));
  const tDiag = Easing.easeOutCubic(clamp((localTime - 1.8) / 0.6, 0, 1));
  const fadeOut = clamp((duration - localTime) / 0.45, 0, 1);

  // Animated path drawing for "flow" diagram
  const dur = duration;
  const flowT = clamp((localTime - 2.3) / 2.5, 0, 1);

  // Diagram nodes (right side)
  const cx = 880, cy = 360;
  const nodes = [
    { id: 'sat',  x: cx - 240, y: cy - 130, r: 28, color: C.cyan,   label: 'Satélites',   sub: 'MODIS · TROPOMI', delay: 0 },
    { id: 'grd',  x: cx - 240, y: cy,       r: 28, color: C.amber,  label: 'Obs. terrestres', sub: 'AERONET · IAGOS', delay: 0.15 },
    { id: 'snd',  x: cx - 240, y: cy + 130, r: 28, color: C.purple, label: 'Modelo IFS',  sub: 'ECMWF', delay: 0.3 },
    { id: 'cams', x: cx + 30, y: cy, r: 56, color: C.orange, label: 'CAMS', sub: 'Análisis · Pronóstico', delay: 0.6 },
    { id: 'out',  x: cx + 280, y: cy, r: 26, color: C.green, label: 'Variables', sub: 'PM, O₃, NO₂, CO, BC...', delay: 1.0 },
  ];

  return (
    <SceneFade>
      <GridBg cell={48} color="rgba(255,255,255,0.018)" />

      {/* Left text column */}
      <div style={{
        position: 'absolute', left: 80, top: 150, width: 460,
        opacity: fadeOut,
      }}>
        <div style={{
          fontFamily: MONO, fontSize: 11, fontWeight: 600,
          letterSpacing: '0.26em', textTransform: 'uppercase',
          color: C.cyan,
          opacity: tKick, transform: 'translateY(' + ((1 - tKick) * 8) + 'px)',
        }}>
          Capítulo I · ¿Qué es CAMS?
        </div>

        <div style={{
          marginTop: 18,
          fontFamily: FONT, fontSize: 44, fontWeight: 700,
          color: C.white, lineHeight: 1.1, letterSpacing: '-0.015em',
          opacity: tHead, transform: 'translateY(' + ((1 - tHead) * 16) + 'px)',
        }}>
          El modelo<br />
          atmosférico<br />
          de <span style={{ color: C.orange }}>Copernicus</span>.
        </div>

        <div style={{
          marginTop: 32,
          fontFamily: FONT, fontSize: 16, fontWeight: 400,
          color: C.gray, lineHeight: 1.6, maxWidth: 420,
          opacity: tBody, transform: 'translateY(' + ((1 - tBody) * 12) + 'px)',
          textWrap: 'pretty',
        }}>
          CAMS opera en el <span style={{ color: C.white }}>ECMWF</span> desde 2015.
          Combina observaciones satelitales, sondeos y estaciones
          en un mismo modelo numérico para describir la composición
          de la atmósfera en todo el planeta.
        </div>

        {/* Stats pills */}
        <div style={{
          marginTop: 36, display: 'flex', gap: 10, flexWrap: 'wrap',
          opacity: tBody,
        }}>
          {[
            { v: '~40 km', l: 'resolución global' },
            { v: '120 h', l: 'horizonte' },
            { v: '2×/día', l: 'corridas' },
          ].map((s, i) => (
            <div key={i} style={{
              padding: '10px 14px',
              border: '1px solid ' + C.faint,
              borderRadius: 8, background: 'rgba(255,255,255,0.025)',
            }}>
              <div style={{ fontFamily: FONT, fontSize: 18, fontWeight: 700, color: C.white }}>{s.v}</div>
              <div style={{ fontFamily: MONO, fontSize: 10, color: C.gray, letterSpacing: '0.1em', textTransform: 'uppercase', marginTop: 2 }}>{s.l}</div>
            </div>
          ))}
        </div>
      </div>

      {/* Right diagram */}
      <div style={{ position: 'absolute', inset: 0, opacity: tDiag * fadeOut }}>
        {/* Connecting lines */}
        <svg width="1280" height="720" style={{ position: 'absolute', inset: 0 }}>
          <defs>
            <linearGradient id="flowGrad" x1="0%" y1="0%" x2="100%" y2="0%">
              <stop offset="0%" stopColor={C.cyan} stopOpacity="0.05" />
              <stop offset="50%" stopColor={C.cyan} stopOpacity="0.5" />
              <stop offset="100%" stopColor={C.orange} stopOpacity="0.5" />
            </linearGradient>
          </defs>
          {/* Inputs → CAMS */}
          {[0, 1, 2].map(i => {
            const n = nodes[i];
            const cam = nodes[3];
            const apT = clamp((flowT - i * 0.08) * 1.6, 0, 1);
            const len = Math.hypot(cam.x - n.x, cam.y - n.y);
            const dashOff = (1 - apT) * len;
            return (
              <line key={i} x1={n.x} y1={n.y} x2={cam.x} y2={cam.y}
                stroke={n.color} strokeWidth={1.5} strokeOpacity={0.55}
                strokeDasharray={len + ' ' + len} strokeDashoffset={dashOff} />
            );
          })}
          {/* CAMS → output */}
          {(() => {
            const cam = nodes[3], out = nodes[4];
            const apT = clamp((flowT - 0.5) * 1.6, 0, 1);
            const len = Math.hypot(out.x - cam.x, out.y - cam.y);
            return (
              <line x1={cam.x} y1={cam.y} x2={out.x} y2={out.y}
                stroke={C.green} strokeWidth={1.8} strokeOpacity={0.7}
                strokeDasharray={len + ' ' + len} strokeDashoffset={(1 - apT) * len} />
            );
          })()}
          {/* Travelling data pulses */}
          {[0, 1, 2].map(i => {
            const n = nodes[i];
            const cam = nodes[3];
            const ph = (time * 0.7 + i * 0.4) % 1;
            const px = n.x + (cam.x - n.x) * ph;
            const py = n.y + (cam.y - n.y) * ph;
            const apT = clamp((flowT - i * 0.08) * 1.6, 0, 1);
            return (
              <circle key={'p' + i} cx={px} cy={py} r={2.5}
                fill={n.color} opacity={apT * (0.85 - ph * 0.6)} />
            );
          })}
        </svg>

        {/* Nodes */}
        {nodes.map((n, i) => {
          const ap = Easing.easeOutBack(clamp((localTime - 1.8 - (n.delay || 0)) / 0.5, 0, 1));
          const isCenter = n.id === 'cams';
          const pulse = isCenter ? (1 + Math.sin(time * 1.8) * 0.04) : 1;
          return (
            <div key={n.id} style={{
              position: 'absolute', left: n.x, top: n.y,
              transform: 'translate(-50%,-50%) scale(' + (ap * pulse) + ')',
              opacity: ap,
            }}>
              <div style={{
                width: n.r * 2, height: n.r * 2, borderRadius: '50%',
                background: isCenter ? n.color : 'rgba(255,255,255,0.04)',
                border: isCenter ? 'none' : '1.5px solid ' + n.color,
                boxShadow: isCenter
                  ? '0 0 30px ' + n.color + '60, 0 0 70px ' + n.color + '20'
                  : '0 0 14px ' + n.color + '30',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}>
                {isCenter && (
                  <div style={{
                    fontFamily: FONT, fontSize: 19, fontWeight: 800,
                    color: C.white, letterSpacing: '0.04em',
                  }}>CAMS</div>
                )}
              </div>
              {/* Label */}
              {!isCenter && (
                <div style={{
                  position: 'absolute', top: n.r * 2 + 8, left: '50%',
                  transform: 'translateX(-50%)',
                  textAlign: 'center', whiteSpace: 'nowrap',
                }}>
                  <div style={{ fontFamily: FONT, fontSize: 12, fontWeight: 600, color: n.color }}>{n.label}</div>
                  <div style={{ fontFamily: MONO, fontSize: 9.5, color: C.gray, marginTop: 3, letterSpacing: '0.04em' }}>{n.sub}</div>
                </div>
              )}
              {isCenter && (
                <div style={{
                  position: 'absolute', top: n.r * 2 + 12, left: '50%',
                  transform: 'translateX(-50%)',
                  fontFamily: MONO, fontSize: 10, color: C.gray,
                  textAlign: 'center', letterSpacing: '0.06em',
                  whiteSpace: 'nowrap',
                }}>{n.sub}</div>
              )}
            </div>
          );
        })}
      </div>
    </SceneFade>
  );
}

// ─── Scene 3: Satélites + IFS asimilación ───────────────────────────

function S3Satellites() {
  const { localTime, duration } = useSprite();
  const time = useTime();

  const tKick = Easing.easeOutCubic(clamp((localTime - 0.2) / 0.5, 0, 1));
  const tHead = Easing.easeOutCubic(clamp((localTime - 0.5) / 0.6, 0, 1));
  const tGlobe = Easing.easeOutCubic(clamp((localTime - 1.0) / 0.7, 0, 1));
  const tSat = Easing.easeOutCubic(clamp((localTime - 1.6) / 0.6, 0, 1));
  const tStats = Easing.easeOutCubic(clamp((localTime - 4.5) / 0.6, 0, 1));
  const fadeOut = clamp((duration - localTime) / 0.45, 0, 1);

  const cx = 640, cy = 405;
  const globeR = 165;

  const satellites = [
    { name: 'Sentinel-5P', sub: 'TROPOMI · NO₂, CO, O₃', ang: -2.4, r: 245, color: C.cyan },
    { name: 'MetOp',       sub: 'IASI · CO, CH₄',        ang: -1.6, r: 280, color: C.amber },
    { name: 'EOS Aqua',    sub: 'MODIS · AOD',           ang: -0.7, r: 252, color: C.orange },
    { name: 'Sentinel-3',  sub: 'OLCI · aerosoles',      ang: 0.2,  r: 275, color: C.purple },
    { name: 'GOES-16',     sub: 'ABI · humo, polvo',     ang: 1.0,  r: 240, color: C.rose },
  ];

  // Latitude / longitude grid
  const lats = [-60, -30, 0, 30, 60];
  const lons = [-150, -90, -30, 30, 90, 150];

  return (
    <SceneFade>
      <GridBg cell={48} color="rgba(255,255,255,0.018)" />

      {/* Header */}
      <div style={{
        position: 'absolute', left: '50%', top: 64,
        transform: 'translateX(-50%)', textAlign: 'center',
        opacity: fadeOut,
      }}>
        <div style={{
          fontFamily: MONO, fontSize: 11, fontWeight: 600,
          letterSpacing: '0.26em', textTransform: 'uppercase',
          color: C.cyan,
          opacity: tKick, transform: 'translateY(' + ((1 - tKick) * 6) + 'px)',
        }}>Capítulo II · Asimilación 4D-Var</div>
        <div style={{
          marginTop: 14,
          fontFamily: FONT, fontSize: 36, fontWeight: 700,
          color: C.white, letterSpacing: '-0.015em',
          opacity: tHead, transform: 'translateY(' + ((1 - tHead) * 12) + 'px)',
        }}>
          Cinco constelaciones alimentan el modelo
        </div>
      </div>

      {/* Globe (stylised) */}
      <div style={{ position: 'absolute', left: cx, top: cy, opacity: tGlobe * fadeOut }}>
        <svg width={globeR * 2 + 40} height={globeR * 2 + 40} style={{
          position: 'absolute', left: -(globeR + 20), top: -(globeR + 20),
        }}>
          <defs>
            <radialGradient id="globeFill" cx="40%" cy="35%">
              <stop offset="0%" stopColor={C.sky} stopOpacity="0.35" />
              <stop offset="60%" stopColor={C.sky} stopOpacity="0.08" />
              <stop offset="100%" stopColor={C.navy} stopOpacity="0" />
            </radialGradient>
            <radialGradient id="globeRim" cx="50%" cy="50%">
              <stop offset="85%" stopColor={C.cyan} stopOpacity="0" />
              <stop offset="100%" stopColor={C.cyan} stopOpacity="0.5" />
            </radialGradient>
          </defs>
          {/* Sphere fill */}
          <circle cx={globeR + 20} cy={globeR + 20} r={globeR} fill="url(#globeFill)" />
          {/* Equator/longitude lines */}
          {lats.map((lat, i) => {
            const ry = globeR * Math.cos(lat * Math.PI / 180);
            const y = (globeR + 20) - globeR * Math.sin(lat * Math.PI / 180);
            return (
              <ellipse key={'lat' + i} cx={globeR + 20} cy={y} rx={globeR * Math.cos(lat * Math.PI / 180) * 0.97} ry={1}
                fill="none" stroke={C.cyan} strokeOpacity={lat === 0 ? 0.35 : 0.15} strokeWidth={lat === 0 ? 1.2 : 1} />
            );
          })}
          {lons.map((lon, i) => {
            const phase = (lon + time * 6) * Math.PI / 180;
            const rx = Math.abs(Math.sin(phase)) * globeR;
            return (
              <ellipse key={'lon' + i} cx={globeR + 20} cy={globeR + 20}
                rx={rx} ry={globeR}
                fill="none" stroke={C.cyan} strokeOpacity={0.12 + Math.sin(phase) * 0.05} strokeWidth={1}
                transform={'rotate(' + 0 + ' ' + (globeR + 20) + ' ' + (globeR + 20) + ')'} />
            );
          })}
          {/* Outer rim */}
          <circle cx={globeR + 20} cy={globeR + 20} r={globeR}
            fill="none" stroke={C.cyan} strokeOpacity={0.55} strokeWidth={1.2} />
          <circle cx={globeR + 20} cy={globeR + 20} r={globeR + 4}
            fill="url(#globeRim)" />

          {/* Random sampling dots — representing observation density */}
          {(() => {
            const dots = [];
            const r = rand(7);
            for (let i = 0; i < 90; i++) {
              const theta = r() * Math.PI * 2;
              const phi = (r() - 0.5) * Math.PI;
              const xs = Math.cos(phi) * Math.sin(theta + time * 0.1);
              const ys = Math.sin(phi);
              const zs = Math.cos(phi) * Math.cos(theta + time * 0.1);
              if (zs < 0) continue;
              const px = (globeR + 20) + xs * globeR * 0.95;
              const py = (globeR + 20) - ys * globeR * 0.95;
              const flash = 0.4 + Math.sin(time * 2 + i) * 0.4;
              dots.push(
                <circle key={'d' + i} cx={px} cy={py} r={1.3 + zs * 0.6}
                  fill={C.amber} opacity={zs * flash * 0.7} />
              );
            }
            return dots;
          })()}
        </svg>

        {/* Center label */}
        <div style={{
          position: 'absolute', left: 0, top: 0,
          transform: 'translate(-50%,-50%)',
          fontFamily: FONT, fontSize: 13, fontWeight: 700,
          color: C.white, letterSpacing: '0.18em',
          textAlign: 'center', textShadow: '0 0 12px rgba(5,10,30,0.9)',
        }}>
          IFS-COMPO
          <div style={{ fontFamily: MONO, fontSize: 9, color: C.cyan, marginTop: 3, letterSpacing: '0.12em' }}>
            modelo global ECMWF
          </div>
        </div>
      </div>

      {/* Satellites orbiting */}
      {satellites.map((s, i) => {
        const ap = Easing.easeOutCubic(clamp((localTime - 1.6 - i * 0.15) / 0.6, 0, 1));
        const ang = s.ang + time * 0.12;
        const sx = cx + Math.cos(ang) * s.r;
        const sy = cy + Math.sin(ang) * s.r * 0.78;
        const labelRight = Math.cos(ang) >= 0;
        return (
          <React.Fragment key={i}>
            {/* Data line to globe */}
            <svg width="1280" height="720" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
              <line x1={sx} y1={sy} x2={cx} y2={cy}
                stroke={s.color} strokeWidth={1} strokeOpacity={ap * 0.25}
                strokeDasharray="2 6" />
              {/* Travelling dot */}
              {(() => {
                const k = ((time * 0.55 + i * 0.18) % 1);
                const px = sx + (cx - sx) * k;
                const py = sy + (cy - sy) * k;
                return <circle cx={px} cy={py} r={2.2} fill={s.color} opacity={ap * (1 - k * 0.7)} />;
              })()}
            </svg>
            {/* Satellite dot */}
            <div style={{
              position: 'absolute', left: sx, top: sy,
              transform: 'translate(-50%,-50%) scale(' + ap + ')',
              opacity: ap,
            }}>
              <div style={{
                width: 12, height: 12, borderRadius: 2,
                background: s.color,
                boxShadow: '0 0 14px ' + s.color + '70',
                transform: 'rotate(45deg)',
              }} />
              {/* Solar panel bars */}
              <div style={{
                position: 'absolute', left: -14, top: 4, width: 10, height: 4,
                background: s.color, opacity: 0.4,
              }} />
              <div style={{
                position: 'absolute', left: 16, top: 4, width: 10, height: 4,
                background: s.color, opacity: 0.4,
              }} />

              {/* Label */}
              <div style={{
                position: 'absolute',
                left: labelRight ? 26 : 'auto',
                right: labelRight ? 'auto' : 26,
                top: '50%', transform: 'translateY(-50%)',
                whiteSpace: 'nowrap',
                textAlign: labelRight ? 'left' : 'right',
              }}>
                <div style={{ fontFamily: FONT, fontSize: 12, fontWeight: 700, color: s.color }}>{s.name}</div>
                <div style={{ fontFamily: MONO, fontSize: 9.5, color: C.gray, letterSpacing: '0.05em', marginTop: 2 }}>{s.sub}</div>
              </div>
            </div>
          </React.Fragment>
        );
      })}

      {/* Bottom stats */}
      <div style={{
        position: 'absolute', left: '50%', bottom: 70,
        transform: 'translateX(-50%)',
        display: 'flex', gap: 56, alignItems: 'baseline',
        opacity: tStats * fadeOut,
      }}>
        {[
          { v: '~40 M', l: 'observaciones/día', c: C.cyan },
          { v: '4D-Var', l: 'asimilación variacional', c: C.amber },
          { v: '60+', l: 'especies químicas', c: C.orange },
        ].map((s, i) => (
          <div key={i} style={{ textAlign: 'center' }}>
            <div style={{ fontFamily: FONT, fontSize: 30, fontWeight: 800, color: s.c, lineHeight: 1 }}>{s.v}</div>
            <div style={{ fontFamily: MONO, fontSize: 10, color: C.gray, marginTop: 5, letterSpacing: '0.12em', textTransform: 'uppercase' }}>{s.l}</div>
          </div>
        ))}
      </div>
    </SceneFade>
  );
}

// ─── Scene 4: Aerosoles globales · El momento Windy ─────────────────

function S4Aerosols() {
  const { localTime, duration } = useSprite();
  const time = useTime();

  // Map projection rectangle (full canvas area)
  const W = 1280, H = 720;

  // Source plumes (in screen coords on stylised map)
  const sources = React.useMemo(() => [
    {
      name: 'Polvo del Sahara', tag: 'AOD 1.2', color: C.orange,
      x: 1010, y: 290, spread: 80, weight: 1.0,
    },
    {
      name: 'Quemas · Amazonía', tag: 'BC + OC', color: C.coral,
      x: 720, y: 470, spread: 70, weight: 0.8,
    },
    {
      name: 'Aerosol marino', tag: 'sea salt', color: C.cyan,
      x: 900, y: 540, spread: 90, weight: 0.6,
    },
    {
      name: 'Volcán Popocatépetl', tag: 'SO₂', color: C.purple,
      x: 605, y: 332, spread: 38, weight: 0.35,
    },
  ], []);

  // Composite flow field — directional (trade winds E → W in tropics)
  // plus per-source contributions
  const field = React.useMemo(() => {
    return (x, y, t) => {
      // Latitude band (closer to equator y≈400) — westward trade winds
      const lat = (H / 2 - y) / (H / 2);  // -1 (south) to +1 (north)
      // Easterlies dominate near equator
      const trade = Math.exp(-lat * lat * 2);
      const vx = -1 * trade - 0.25;
      // Slight equatorward drift
      const vy = lat * 0.25;
      // Curl noise perturbation
      const tt = t * 0.08;
      const swirl = 0.45;
      const cx2 = Math.sin(x * 0.005 + tt) + Math.cos(y * 0.006 - tt * 0.7);
      const cy2 = Math.cos(x * 0.0055 - tt * 0.5) + Math.sin(y * 0.0048 + tt);
      return [vx + cx2 * swirl, vy + cy2 * swirl];
    };
  }, []);

  // Particles get colored by which source they last touched.
  // For simplicity, spawn particles at sources weighted by `weight`.
  const totalW = sources.reduce((s, p) => s + p.weight, 0);
  const spawnFn = React.useMemo(() => (t, i) => {
    let pick = (i * 0.6180339 + t * 0.001) % 1;
    pick *= totalW;
    let acc = 0;
    for (const src of sources) {
      acc += src.weight;
      if (pick <= acc) {
        const r = rand(i * 13 + Math.floor(t * 100));
        const ang = r() * Math.PI * 2;
        const rad = Math.sqrt(r()) * src.spread;
        return {
          x: src.x + Math.cos(ang) * rad,
          y: src.y + Math.sin(ang) * rad,
        };
      }
    }
    return { x: sources[0].x, y: sources[0].y };
  }, [sources, totalW]);

  // Phase timing
  const tKick = Easing.easeOutCubic(clamp((localTime - 0.3) / 0.5, 0, 1));
  const tHead = Easing.easeOutCubic(clamp((localTime - 0.7) / 0.7, 0, 1));
  const tMap = Easing.easeOutCubic(clamp((localTime - 1.0) / 0.8, 0, 1));
  const tSrc = Easing.easeOutCubic(clamp((localTime - 2.3) / 0.5, 0, 1));
  const tPin = Easing.easeOutCubic(clamp((localTime - 4.0) / 0.6, 0, 1));
  const tFact = Easing.easeOutCubic(clamp((localTime - 6.0) / 0.6, 0, 1));
  const fadeOut = clamp((duration - localTime) / 0.5, 0, 1);

  // Valle de Aburrá ~ NW South America: ~6°N, 75.5°W. On our stylised map
  // we'll place it at (700, 425) — roughly upper-NW South America.
  const valle = { x: 700, y: 425 };

  return (
    <SceneFade>
      {/* The Windy flow */}
      <div style={{ opacity: tMap * fadeOut, position: 'absolute', inset: 0 }}>
        <WindyCanvas
          width={W} height={H}
          count={1600}
          field={field}
          spawn={spawnFn}
          palette={[C.orange, C.amber, C.coral, C.cyan, C.purple, C.rose]}
          fade={0.045}
          speed={1.4}
          life={140}
          lineWidth={1}
          opacity={1}
          background={C.navy}
        />
      </div>

      {/* Source-region halos — soft tinted zones (replaces continent outlines) */}
      <svg width="1280" height="720" style={{ position: 'absolute', inset: 0, opacity: tMap * fadeOut, pointerEvents: 'none' }}>
        <defs>
          {sources.map((s, i) => (
            <radialGradient key={'rg' + i} id={'srcZone' + i} cx="50%" cy="50%" r="50%">
              <stop offset="0%" stopColor={s.color} stopOpacity="0.22" />
              <stop offset="55%" stopColor={s.color} stopOpacity="0.06" />
              <stop offset="100%" stopColor={s.color} stopOpacity="0" />
            </radialGradient>
          ))}
        </defs>
        {sources.map((s, i) => (
          <ellipse key={'sz' + i}
            cx={s.x} cy={s.y}
            rx={s.spread * 1.9} ry={s.spread * 1.4}
            fill={'url(#srcZone' + i + ')'} />
        ))}
      </svg>

      {/* Reinforced graticule with degree labels */}
      <svg width="1280" height="720" style={{ position: 'absolute', inset: 0, opacity: tMap * fadeOut, pointerEvents: 'none' }}>
        {/* Latitude lines */}
        {[
          { lat: 30, label: '30°N' },
          { lat: 15, label: '15°N' },
          { lat: 0, label: '0°', equator: true },
          { lat: -15, label: '15°S' },
          { lat: -30, label: '30°S' },
        ].map((row, i) => {
          // Map lat → y: equator at y=468, scale 7.1 px/deg
          const y = 468 - row.lat * 7.1;
          if (y < 0 || y > 720) return null;
          return (
            <React.Fragment key={'lat' + i}>
              <line x1={0} y1={y} x2={1280} y2={y}
                stroke={C.cyan}
                strokeOpacity={row.equator ? 0.32 : 0.1}
                strokeWidth={row.equator ? 1 : 0.7}
                strokeDasharray={row.equator ? '0' : '3 7'} />
              <text x={20} y={y - 4} fill={C.cyan}
                fillOpacity={row.equator ? 0.6 : 0.32}
                fontFamily={MONO} fontSize={10} letterSpacing="0.12em">{row.label}</text>
            </React.Fragment>
          );
        })}
        {/* Longitude lines */}
        {[
          { lon: -90, label: '90°W' },
          { lon: -60, label: '60°W' },
          { lon: -30, label: '30°W' },
          { lon: 0, label: '0°' },
          { lon: 30, label: '30°E' },
        ].map((col, i) => {
          // Map lon → x: Valle at lon -75 → x=700, scale 4.13 px/deg
          const x = 700 + (col.lon + 75) * 4.13;
          if (x < 0 || x > 1280) return null;
          return (
            <React.Fragment key={'lon' + i}>
              <line x1={x} y1={0} x2={x} y2={720}
                stroke={C.cyan} strokeOpacity={0.08}
                strokeWidth={0.7} strokeDasharray="3 7" />
              <text x={x + 4} y={714} fill={C.cyan}
                fillOpacity={0.32}
                fontFamily={MONO} fontSize={10} letterSpacing="0.12em">{col.label}</text>
            </React.Fragment>
          );
        })}
      </svg>

      {/* Wind direction compass — top-left corner of "instrument" feel */}
      <div style={{
        position: 'absolute', right: 30, top: 110,
        opacity: tMap * fadeOut, pointerEvents: 'none',
      }}>
        <div style={{
          padding: '10px 14px',
          background: 'rgba(5,10,30,0.55)',
          border: '1px solid ' + C.faint,
          borderRadius: 6,
          backdropFilter: 'blur(4px)',
        }}>
          <div style={{
            fontFamily: MONO, fontSize: 9, color: C.gray,
            letterSpacing: '0.18em', textTransform: 'uppercase',
            marginBottom: 6,
          }}>Flujo dominante</div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <svg width={36} height={20}>
              <line x1={32} y1={10} x2={6} y2={10} stroke={C.cyan} strokeWidth={1.6} />
              <polygon points="6,10 14,5 14,15" fill={C.cyan} />
            </svg>
            <div style={{
              fontFamily: FONT, fontSize: 12, fontWeight: 600, color: C.cyan,
            }}>E → O</div>
          </div>
          <div style={{
            fontFamily: MONO, fontSize: 9, color: C.gray,
            marginTop: 4, letterSpacing: '0.04em',
          }}>vientos alisios tropicales</div>
        </div>
      </div>

      {/* Header strip — top */}
      <div style={{
        position: 'absolute', left: 24, top: 24, right: 24,
        display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start',
        opacity: fadeOut, pointerEvents: 'none',
      }}>
        <div>
          <div style={{
            fontFamily: MONO, fontSize: 11, fontWeight: 600,
            letterSpacing: '0.26em', textTransform: 'uppercase',
            color: C.cyan,
            opacity: tKick, transform: 'translateY(' + ((1 - tKick) * 8) + 'px)',
          }}>
            Capítulo III · Transporte de aerosoles
          </div>
          <div style={{
            marginTop: 10,
            fontFamily: FONT, fontSize: 28, fontWeight: 700,
            color: C.white, lineHeight: 1.1, letterSpacing: '-0.015em', maxWidth: 540,
            opacity: tHead, transform: 'translateY(' + ((1 - tHead) * 14) + 'px)',
            textShadow: '0 2px 16px rgba(5,10,30,0.9)',
          }}>
            Lo que ven los satélites,<br />
            CAMS lo transporta hora a hora.
          </div>
        </div>
        <div style={{
          textAlign: 'right',
          fontFamily: MONO, fontSize: 11, letterSpacing: '0.16em',
          textTransform: 'uppercase', color: C.gray,
          opacity: tHead,
        }}>
          <div style={{ color: C.cyan }}>CAMS Global / aerosol optical depth</div>
          <div style={{ color: C.dim, marginTop: 6 }}>level 550 nm · superficie · UTC {String(Math.floor((time * 6) % 24)).padStart(2, '0')}:00</div>
        </div>
      </div>

      {/* Source pins */}
      {sources.map((s, i) => {
        const ap = Easing.easeOutBack(clamp((localTime - 2.3 - i * 0.2) / 0.5, 0, 1));
        return (
          <div key={i} style={{
            position: 'absolute', left: s.x, top: s.y,
            transform: 'translate(-50%,-50%) scale(' + ap + ')',
            opacity: ap * fadeOut, pointerEvents: 'none',
          }}>
            {/* Halo */}
            <div style={{
              position: 'absolute', left: '50%', top: '50%',
              width: 60, height: 60, borderRadius: '50%',
              transform: 'translate(-50%,-50%)',
              background: 'radial-gradient(circle, ' + hexWithAlpha(s.color, 0.35) + ' 0%, ' + hexWithAlpha(s.color, 0) + ' 70%)',
              animation: 'none',
            }} />
            {/* Dot */}
            <div style={{
              position: 'absolute', left: '50%', top: '50%',
              transform: 'translate(-50%,-50%)',
              width: 10, height: 10, borderRadius: '50%',
              background: s.color, boxShadow: '0 0 14px ' + s.color,
            }} />
            {/* Label card */}
            <div style={{
              position: 'absolute', left: 18, top: -28,
              padding: '6px 10px',
              background: 'rgba(5,10,30,0.78)',
              border: '1px solid ' + hexWithAlpha(s.color, 0.4),
              borderRadius: 6, backdropFilter: 'blur(4px)',
              whiteSpace: 'nowrap',
            }}>
              <div style={{ fontFamily: FONT, fontSize: 12, fontWeight: 700, color: s.color }}>{s.name}</div>
              <div style={{ fontFamily: MONO, fontSize: 9.5, color: C.gray, marginTop: 2, letterSpacing: '0.08em' }}>{s.tag}</div>
            </div>
          </div>
        );
      })}

      {/* Valle de Aburrá target */}
      <div style={{
        position: 'absolute', left: valle.x, top: valle.y,
        transform: 'translate(-50%,-50%) scale(' + tPin + ')',
        opacity: tPin * fadeOut, pointerEvents: 'none',
      }}>
        {/* Concentric pulse rings */}
        {[0, 1, 2].map(i => {
          const ph = ((time * 0.6 + i / 3) % 1);
          return (
            <div key={i} style={{
              position: 'absolute', left: '50%', top: '50%',
              width: 20 + ph * 80, height: 20 + ph * 80,
              borderRadius: '50%',
              transform: 'translate(-50%,-50%)',
              border: '1.5px solid ' + C.white,
              opacity: (1 - ph) * 0.8,
            }} />
          );
        })}
        <div style={{
          position: 'absolute', left: '50%', top: '50%',
          transform: 'translate(-50%,-50%)',
          width: 16, height: 16, borderRadius: '50%',
          background: C.white,
          boxShadow: '0 0 20px ' + C.white + ', 0 0 40px ' + C.cyan,
        }} />
        <div style={{
          position: 'absolute', left: 24, top: -10,
          padding: '8px 12px',
          background: 'rgba(255,255,255,0.95)',
          borderRadius: 6,
          whiteSpace: 'nowrap',
          boxShadow: '0 4px 24px rgba(0,0,0,0.5)',
        }}>
          <div style={{ fontFamily: FONT, fontSize: 13, fontWeight: 800, color: C.navy }}>Valle de Aburrá</div>
          <div style={{ fontFamily: MONO, fontSize: 9.5, color: C.navy, opacity: 0.65, marginTop: 2, letterSpacing: '0.08em' }}>06°15′N · 75°34′W</div>
        </div>
      </div>

      {/* Bottom fact */}
      <div style={{
        position: 'absolute', left: 24, bottom: 60, right: 24,
        opacity: tFact * fadeOut,
        display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end',
        pointerEvents: 'none',
      }}>
        <div style={{
          maxWidth: 460,
          padding: '14px 18px',
          background: 'rgba(5,10,30,0.78)',
          border: '1px solid ' + C.faint,
          borderRadius: 8, backdropFilter: 'blur(6px)',
        }}>
          <div style={{ fontFamily: MONO, fontSize: 10, color: C.cyan, letterSpacing: '0.16em', textTransform: 'uppercase' }}>Por qué importa para el Valle</div>
          <div style={{ fontFamily: FONT, fontSize: 14, color: C.white, lineHeight: 1.5, marginTop: 6 }}>
            Polvo del Sahara, humo de quemas amazónicas y aerosoles marinos
            pueden cruzar el continente en <span style={{ color: C.orange, fontWeight: 700 }}>3 a 5 días</span> y
            modular nuestra calidad del aire local.
          </div>
        </div>
        <div style={{
          fontFamily: MONO, fontSize: 10, color: C.dim,
          letterSpacing: '0.18em', textTransform: 'uppercase',
          textAlign: 'right',
        }}>
          fuente · CAMS global forecast<br />
          <span style={{ color: C.gray }}>aerosol_550nm · superficie</span>
        </div>
      </div>
    </SceneFade>
  );
}

Object.assign(window, { S1Intro, S2What, S3Satellites, S4Aerosols });
