/* VARIANTE E · CELLULAR — formas orgánicas animadas, interactivas con el mouse */

const { useState: useStateE, useEffect: useEffectE, useRef: useRefE,
        useCallback: useCallbackE, useMemo: useMemoE } = React;

const FOAM_MARACUYA  = 'images/espuma-maracuya.png';
const FOAM_FRAMBUESA = 'images/espuma-frambuesa.png';

/* Espuma en movimiento (video → secuencia de frames) controlada por scroll.
   Mismo enfoque que el logo: scrubbing sobre frames para evitar el jank de
   seek de <video> en iOS Safari. 55 frames extraídos de "espuma frames .mp4". */
const FOAM_FRAME_COUNT = 133;
/* Video original con su fondo oscuro — así estaba bien.
   Móvil usa los MISMOS frames de 832px que desktop: los de 416px se
   veían pixelados al estirarse a pantalla completa. */
const FOAM_FRAME_PATH_DESKTOP = (i) => `images/espuma-frames/f${String(i).padStart(3, '0')}.jpg?v=4`;
const FOAM_FRAME_PATH_MOBILE  = (i) => `images/espuma-frames/f${String(i).padStart(3, '0')}.jpg?v=4`;

/* Registro global de precarga: el age-gate lo lee para mostrar el progreso
   y dejar que el usuario entre a la página con todo ya cargado. */
window.__sourPreload = window.__sourPreload || { loaded: 0, total: 0 };

/* ── Organic blob paths ── */
const BP = {
  A: "M195,18 C228,4 268,8 292,32 C318,58 310,100 322,132 C336,168 358,192 348,226 C338,260 304,272 278,292 C250,314 242,348 210,354 C178,360 150,336 122,322 C92,308 54,312 34,288 C12,264 16,230 14,198 C12,166 -4,134 4,100 C12,66 42,44 72,28 C102,12 162,32 195,18 Z",
  B: "M180,14 C212,2 252,6 278,28 C306,52 298,94 312,126 C328,160 352,180 344,216 C334,252 298,266 270,288 C240,312 230,350 196,358 C162,366 132,340 102,324 C70,306 28,314 8,288 C-14,260 -8,224 -10,190 C-12,156 -28,122 -18,86 C-8,50 24,28 56,12 C88,-4 148,26 180,14 Z",
  C: "M165,12 C200,-2 242,4 268,28 C296,54 286,98 300,132 C316,168 342,190 332,228 C320,266 282,280 252,304 C220,330 208,366 174,374 C138,382 106,356 74,338 C40,318 -2,328 -22,300 C-44,270 -38,232 -40,196 C-42,158 -60,120 -48,82 C-36,42 0,18 34,2 C70,-16 128,28 165,12 Z",
  D: "M188,10 C222,-2 264,4 290,28 C318,54 308,98 322,132 C338,168 364,188 354,226 C342,264 304,278 274,302 C242,328 230,366 196,374 C160,382 128,354 96,336 C62,316 18,326 -4,296 C-28,264 -22,226 -24,190 C-26,152 -44,114 -32,76 C-20,36 16,12 50,-4 C86,-22 152,24 188,10 Z",
  E: "M172,20 C208,4 248,12 274,36 C302,62 294,106 308,140 C324,178 350,196 340,236 C328,276 288,288 258,312 C226,338 214,376 178,382 C140,388 108,360 76,342 C42,322 -2,330 -20,300 C-40,268 -32,228 -34,192 C-36,154 -56,116 -42,78 C-28,38 8,14 42,-2 C78,-18 134,36 172,20 Z",
};

const SPLASH_CTA = "M52,4 C68,-1 82,6 92,18 C103,31 108,50 106,68 C104,86 94,100 82,110 C69,121 54,128 40,126 C25,124 12,114 6,100 C-1,86 -2,66 2,50 C6,34 16,20 28,12 C38,5 42,7 52,4 Z";

/* Centro óptico de los paths (aprox) */
const CX = 160, CY = 185;

/* Configuración de células por sección */
const CELL_CONFIGS = [
  { path: BP.A, ox: -20, oy: -15, freq: 0.36, phase: 0,   amp: 22, rot: 0,   sc: 1.28, op: 0.13, pull:  1 },
  { path: BP.B, ox:  25, oy: -10, freq: 0.26, phase: 2.1, amp: 28, rot: 12,  sc: 1.08, op: 0.10, pull: -1 },
  { path: BP.C, ox: -10, oy:  20, freq: 0.42, phase: 4.0, amp: 18, rot: -8,  sc: 1.33, op: 0.09, pull:  1 },
  { path: BP.D, ox:  15, oy:  18, freq: 0.22, phase: 1.5, amp: 32, rot: 22,  sc: 1.15, op: 0.08, pull: -1 },
  { path: BP.E, ox:  -5, oy:  -5, freq: 0.48, phase: 3.4, amp: 16, rot: -18, sc: 1.24, op: 0.07, pull:  1 },
  { path: BP.A, ox:  30, oy:  -8, freq: 0.30, phase: 5.2, amp: 25, rot:  5,  sc: 1.00, op: 0.06, pull: -1 },
];

/* Tono neutro de la caja — gris cálido del fondo de la imagen */
const CAJA_TONE = '#8c847a';

/* ─── CellularField: tejido celular animado con influencia del mouse ─── */
function CellularField({ tone, configs = CELL_CONFIGS, className = '' }) {
  const svgRef   = useRefE(null);
  const mouseT   = useRefE({ x: 0.5, y: 0.5 });
  const mouseS   = useRefE({ x: 0.5, y: 0.5 });

  useEffectE(() => {
    const gs = svgRef.current ? [...svgRef.current.querySelectorAll('.cg')] : [];

    /* MÓVIL: sin bucle de animación. Varios campos celulares animando
       SVGs a 60fps eran una fuente importante de saltos en el scroll
       táctil (y sin mouse la interactividad no aporta). Se pintan
       estáticos en una pose orgánica y listo. */
    if (matchMedia('(max-width: 760px)').matches || matchMedia('(pointer: coarse)').matches) {
      configs.forEach((c, i) => {
        const el = gs[i];
        if (!el) return;
        el.setAttribute('transform',
          `translate(${c.ox + Math.sin(c.phase) * c.amp * 0.5}, ${c.oy + Math.cos(c.phase) * c.amp * 0.4}) scale(${c.sc}) rotate(${c.rot}, ${CX}, ${CY})`
        );
      });
      return;
    }

    const onMove = e => {
      mouseT.current.x = e.clientX / innerWidth;
      mouseT.current.y = e.clientY / innerHeight;
    };
    addEventListener('mousemove', onMove);

    let raf;

    const tick = () => {
      const t  = Date.now() * 0.001;
      const sm = mouseS.current, tm = mouseT.current;
      /* suavizado muy lento del mouse */
      sm.x += (tm.x - sm.x) * 0.018;
      sm.y += (tm.y - sm.y) * 0.018;

      /* desplazamiento del mouse en unidades SVG: ±20 */
      const mox = (sm.x - 0.5) * 40;
      const moy = (sm.y - 0.5) * 40;

      configs.forEach((c, i) => {
        const el = gs[i];
        if (!el) return;

        /* movimiento sinusoidal en dos frecuencias → trayectoria lissajous */
        const ox = c.ox + Math.sin(t * c.freq + c.phase) * c.amp
                        + Math.sin(t * c.freq * 0.53 + c.phase + 1.3) * c.amp * 0.4;
        const oy = c.oy + Math.cos(t * c.freq * 0.79 + c.phase + 0.6) * c.amp
                        + Math.cos(t * c.freq * 0.41 + c.phase + 2.1) * c.amp * 0.35;

        /* respiración de escala */
        const sc = c.sc * (1 + Math.sin(t * c.freq * 0.38 + c.phase) * 0.045);

        /* rotación sinusoidal lenta */
        const ro = c.rot + Math.sin(t * c.freq * 0.25 + c.phase + 0.8) * 8;

        /* influencia del mouse — células alternas se acercan / alejan */
        const mx = mox * c.pull * 0.18;
        const my = moy * c.pull * 0.18;

        el.setAttribute('transform',
          `translate(${ox + mx}, ${oy + my}) scale(${sc}) rotate(${ro}, ${CX}, ${CY})`
        );
      });
      raf = requestAnimationFrame(tick);
    };

    raf = requestAnimationFrame(tick);
    return () => {
      cancelAnimationFrame(raf);
      removeEventListener('mousemove', onMove);
    };
  }, [configs]);

  return (
    <div className={`cellular-field ${className}`}>
      <svg ref={svgRef} viewBox="-80 -30 470 420" preserveAspectRatio="xMidYMid meet">
        {configs.map((c, i) => (
          <g key={i} className="cg" style={{ opacity: c.op }}>
            <path d={c.path} fill={tone} />
          </g>
        ))}
      </svg>
    </div>
  );
}

/* ─── SplashBlob estático (hero / footer) ─── */
function SplashBlobE({ path, className, style, children }) {
  return (
    <div className={`splash-blob ${className || ''}`} style={style}>
      <svg viewBox="-60 -10 440 390" preserveAspectRatio="none">
        <path d={path} />
      </svg>
      {children}
    </div>
  );
}

function RevealSectionE({ children, className, style, delay = 0 }) {
  const [ref, shown] = useReveal();
  return (
    <div ref={ref}
         className={`rv ${shown ? 'rv-in' : ''} ${className || ''}`}
         style={{ ...style, transitionDelay: `${delay}ms` }}>
      {children}
    </div>
  );
}

function ProductSplashE({ product, onClose, cart, add, total }) {
  if (!product) return null;
  const cocktailImg = product.id === 'maracuya' ? 'images/pisco-sour-maracuya-169.png'
    : product.id === 'frambuesa' ? 'images/pisco-sour-frambuesa-169.png'
    : 'images/pina-colada-169.png';
  const qty = (cart && cart[product.id]) || 0;
  const canAdd = total < PACK_QTY;
  return (
    <div className="prod-splash-overlay" onClick={onClose}>
      <div className="prod-splash-bg" style={{ '--p-tone': product.tone }} onClick={e => e.stopPropagation()}>
        <svg viewBox="-60 -10 440 390" preserveAspectRatio="none">
          <path d={BP.A} />
        </svg>
      </div>
      <img src={cocktailImg} alt={product.name} className="prod-splash-img" onClick={e => e.stopPropagation()} />
      <div className="prod-splash-info" onClick={e => e.stopPropagation()}>
        <span className="prod-splash-n">{product.n}</span>
        <h3>{product.name}</h3>
        <p>{product.mood}</p>
        <div className="prod-splash-buy">
          <div className="prod-splash-price">S/{product.price}<small>125 ml</small></div>
          <div className="vD-qty prod-splash-qty">
            <button onClick={() => add && add(product.id, -1)} disabled={qty <= 0} data-cursor>−</button>
            <span>{qty}</span>
            <button onClick={() => add && add(product.id, +1)} disabled={!canAdd} data-cursor>+</button>
          </div>
        </div>
      </div>
    </div>
  );
}

function FloatingCTAE({ total, final, onCheckout, warning }) {
  const ref    = useRefE(null);
  const pos    = useRefE({ x: window.innerWidth - 100, y: window.innerHeight / 2 });
  const target = useRefE({ x: window.innerWidth - 100, y: window.innerHeight / 2 });
  const [vis, setVis] = useStateE(false);

  useEffectE(() => {
    if ('ontouchstart' in window) return;
    let raf;
    const onMove = e => { target.current.x = e.clientX + 55; target.current.y = e.clientY + 55; };
    const loop = () => {
      const p = pos.current, t = target.current;
      p.x += (t.x - p.x) * 0.1;
      p.y += (t.y - p.y) * 0.1;
      const breathe = Math.sin(Date.now() * 0.001) * 4;
      if (ref.current) ref.current.style.transform = `translate(${p.x}px,${p.y + breathe}px) translate(-50%,-50%)`;
      raf = requestAnimationFrame(loop);
    };
    const timer = setTimeout(() => setVis(true), 1200);
    raf = requestAnimationFrame(loop);
    addEventListener('mousemove', onMove);
    return () => { cancelAnimationFrame(raf); removeEventListener('mousemove', onMove); clearTimeout(timer); };
  }, []);

  if (!vis) return null;
  const hasItems = total > 0;
  return (
    <button ref={ref}
            className={`floating-cta ${hasItems ? 'has-items' : ''} ${warning ? 'is-warn' : ''}`}
            onClick={onCheckout} data-cursor>
      <svg className="floating-cta-shape" viewBox="-10 -10 130 150" preserveAspectRatio="none">
        <path d={SPLASH_CTA} />
      </svg>
      {hasItems && <span className="floating-cta-label">{warning ? '!' : `S/${final}`}</span>}
      {hasItems && <span className="floating-cta-count">{total}</span>}
    </button>
  );
}

/* ─── RitualSteps: tres blobs orgánicos en fila, lenguaje visual del producto ─── */
const RITUAL_STEP_DATA = [
  { n: '01', t: 'vierte el contenido', b: '125 ml en la licuadora.',         path: 'A', tone: '#E8842B' },
  { n: '02', t: 'añade agua al envase', b: '125 ml de agua a la licuadora.', path: 'B', tone: '#C8385C' },
  { n: '03', t: 'licúa y sirve',        b: 'licúa por 10 segundos.',          path: 'C', tone: '#D9B33A' },
];

function RitualSteps() {
  const refs = [useRefE(null), useRefE(null), useRefE(null)];
  const phases = [0, 1.8, 3.5];
  const freqs  = [0.18, 0.14, 0.22];

  useEffectE(() => {
    /* en móvil los blobs SVG están ocultos (círculos CSS): no animar */
    if (matchMedia('(max-width: 760px)').matches) return;
    let raf;
    const tick = () => {
      const t = Date.now() * 0.001;
      refs.forEach((ref, i) => {
        if (!ref.current) return;
        const sc = 1 + Math.sin(t * freqs[i] + phases[i]) * 0.032;
        const ro = Math.sin(t * freqs[i] * 0.6 + phases[i] + 1) * 2.5;
        ref.current.style.transform = `scale(${sc}) rotate(${ro}deg)`;
      });
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  return (
    <div className="ritual-steps">
      {RITUAL_STEP_DATA.map((s, i) => (
        <div key={i} className="ritual-step">
          <div className="ritual-step-blob-wrap">
            <svg ref={refs[i]} className="ritual-step-blob"
                 viewBox="-60 -10 440 390" preserveAspectRatio="none">
              <path d={BP[s.path]} fill={s.tone} fillOpacity="0.15" />
            </svg>
            <span className="ritual-step-n">{s.n}</span>
            <strong className="ritual-step-title">{s.t}</strong>
          </div>
          <p className="ritual-step-body">{s.b}</p>
        </div>
      ))}
    </div>
  );
}

/* ─── Config cellular WA button — blobs verdes lentos para círculo pequeño ─── */
const CELL_CONFIGS_WA = [
  { path: BP.A, ox:  0,  oy:  0,  freq: 0.08, phase: 0,   amp: 8,  rot: 0,   sc: 1.60, op: 0.55, pull:  1 },
  { path: BP.C, ox:  6,  oy: -4,  freq: 0.06, phase: 1.5, amp: 6,  rot: 15,  sc: 1.40, op: 0.40, pull: -1 },
  { path: BP.E, ox: -5,  oy:  6,  freq: 0.10, phase: 3.0, amp: 5,  rot: -10, sc: 1.30, op: 0.30, pull:  1 },
];

/* ─── Config cellular B2B — lento, amplio, 3D ─── */
const CELL_CONFIGS_B2B = [
  { path: BP.A, ox: -10, oy: -8,  freq: 0.07, phase: 0,   amp: 20, rot: 0,   sc: 2.10, op: 0.18, pull:  1 },
  { path: BP.C, ox:  18, oy:  8,  freq: 0.05, phase: 1.8, amp: 26, rot: 12,  sc: 1.80, op: 0.14, pull: -1 },
  { path: BP.E, ox: -8,  oy:  14, freq: 0.09, phase: 3.5, amp: 16, rot: -8,  sc: 1.95, op: 0.11, pull:  1 },
  { path: BP.B, ox:  12, oy: -10, freq: 0.06, phase: 2.2, amp: 22, rot: 18,  sc: 1.70, op: 0.09, pull: -1 },
];

/* ─── RitualBubble: gran burbuja con tres burbujas internas flotantes ─── */
const RITUAL_STEPS = [
  { n: '01', t: 'Vierte',     b: '125 ml en la licuadora.' },
  { n: '02', t: 'Añade agua', b: 'Llena el envase y echa.'  },
  { n: '03', t: 'Licúa',      b: '10 s a potencia alta.'   },
];

function RitualBubble() {
  const wrapRef  = useRefE(null);
  const outerRef = useRefE(null);
  const timeRef  = useRefE(null);
  const i0 = useRefE(null), i1 = useRefE(null), i2 = useRefE(null);
  const innerRefs = [i0, i1, i2];
  const INNER_SIZES = ['42%', '38%', '44%'];
  const INNER_PCT   = [0.42, 0.38, 0.44];

  /* cuenta regresiva 40 → 0 → 40 */
  useEffectE(() => {
    let c = 40;
    const fmt = n => `0:${String(n).padStart(2,'0')}`;
    if (timeRef.current) timeRef.current.textContent = fmt(c);
    const id = setInterval(() => {
      c = c <= 0 ? 40 : c - 1;
      if (timeRef.current) timeRef.current.textContent = fmt(c);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  /* motor de física */
  useEffectE(() => {
    if (!outerRef.current || !wrapRef.current) return;

    const oSize = outerRef.current.offsetWidth;
    const oR    = oSize / 2;
    const pad   = 5;

    /* burbuja exterior: posición absoluta dentro del wrap */
    const ow = wrapRef.current.offsetWidth;
    const oh = wrapRef.current.offsetHeight;
    const outer = {
      x: (ow - oSize) / 2, y: (oh - oSize) / 2,
      vx: 0.32, vy: 0.22,
    };

    /* burbujas internas: coordenadas relativas al centro de la exterior */
    const inners = INNER_PCT.map((pct, i) => ({
      x: [oR * -0.30, oR * 0.28, 0][i],
      y: [oR * -0.26, oR * -0.22, oR * 0.28][i],
      vx: [0.55, -0.48, 0.30][i],
      vy: [0.38,  0.44, -0.52][i],
      r:  oR * pct / 2,
      sc: 1,
    }));

    let raf;
    const tick = () => {
      const t  = Date.now() * 0.001;
      const ww = wrapRef.current?.clientWidth  || ow;
      const wh = wrapRef.current?.clientHeight || oh;
      const mg = 18;

      /* — burbuja exterior flota — */
      outer.x += outer.vx;
      outer.y += outer.vy;
      if (outer.x < mg)            { outer.x = mg;           outer.vx =  Math.abs(outer.vx) * 0.88; }
      if (outer.x+oSize > ww-mg)   { outer.x = ww-mg-oSize;  outer.vx = -Math.abs(outer.vx) * 0.88; }
      if (outer.y < mg)            { outer.y = mg;           outer.vy =  Math.abs(outer.vy) * 0.88; }
      if (outer.y+oSize > wh-mg)   { outer.y = wh-mg-oSize;  outer.vy = -Math.abs(outer.vy) * 0.88; }
      outer.vx += (Math.random()-0.5)*0.012;
      outer.vy += (Math.random()-0.5)*0.012;
      const cap = 0.85;
      outer.vx = Math.max(-cap, Math.min(cap, outer.vx));
      outer.vy = Math.max(-cap, Math.min(cap, outer.vy));

      if (outerRef.current) {
        outerRef.current.style.left = `${outer.x}px`;
        outerRef.current.style.top  = `${outer.y}px`;
        /* aura pulsante */
        const p = Math.sin(t * 0.26) * 0.5 + 0.5;
        outerRef.current.style.boxShadow =
          `inset 0 2px ${20+p*28}px rgba(255,255,255,${0.12+p*0.10}),`+
          `0 24px 60px rgba(0,0,0,0.07),`+
          `0 0 ${36+p*38}px ${5+p*9}px rgba(255,255,255,${p*0.14})`;
      }

      /* — colisiones interior-interior — */
      for (let a = 0; a < inners.length; a++) {
        for (let b = a+1; b < inners.length; b++) {
          const ba = inners[a], bb = inners[b];
          const dx = ba.x-bb.x, dy = ba.y-bb.y;
          const d  = Math.sqrt(dx*dx+dy*dy) || 0.001;
          const minD = ba.r+bb.r+3;
          if (d < minD) {
            const nx=dx/d, ny=dy/d, ov=(minD-d)*0.5;
            ba.x+=nx*ov; ba.y+=ny*ov;
            bb.x-=nx*ov; bb.y-=ny*ov;
            const rv=(ba.vx-bb.vx)*nx+(ba.vy-bb.vy)*ny;
            if (rv<0){
              ba.vx-=rv*nx*0.78; ba.vy-=rv*ny*0.78;
              bb.vx+=rv*nx*0.78; bb.vy+=rv*ny*0.78;
            }
            ba.sc=1.07; bb.sc=1.07;
          }
        }
      }

      /* — burbujas internas se mueven y rebotan en la pared — */
      inners.forEach((b, i) => {
        b.x += b.vx; b.y += b.vy;
        const dist = Math.sqrt(b.x*b.x+b.y*b.y) || 0.001;
        const wall = oR - b.r - pad;
        if (dist > wall) {
          const nx=b.x/dist, ny=b.y/dist;
          const dv=b.vx*nx+b.vy*ny;
          b.vx=(b.vx-2*dv*nx)*0.82; b.vy=(b.vy-2*dv*ny)*0.82;
          b.x=nx*wall; b.y=ny*wall;
          b.sc=1.08;
        }
        b.vx*=0.997; b.vy*=0.997;
        const spd=Math.sqrt(b.vx*b.vx+b.vy*b.vy);
        if (spd<0.20){ const a=Math.random()*Math.PI*2; b.vx+=Math.cos(a)*0.14; b.vy+=Math.sin(a)*0.14; }
        b.sc += (1-b.sc)*0.07;
        const breathe = 1+Math.sin(t*(0.65+i*0.22)+i*2.1)*0.045;
        if (innerRefs[i].current) {
          innerRefs[i].current.style.transform =
            `translate(calc(-50% + ${b.x}px), calc(-50% + ${b.y}px)) scale(${b.sc*breathe})`;
        }
      });

      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  return (
    <div ref={wrapRef} className="ritual-bubble-wrap">
      <div ref={outerRef} className="ritual-bubble-outer">
        <span ref={timeRef} className="ritual-time-mark">0:40</span>
        {RITUAL_STEPS.map((s, i) => (
          <div key={i} ref={innerRefs[i]} className="ritual-bubble-inner"
               style={{ width: INNER_SIZES[i] }}>
            <div className="ritual-ripple" />
            <div className="ritual-ripple ritual-ripple-b" />
            <span className="ritual-inner-n">{s.n}</span>
            <strong>{s.t}</strong>
            <p>{s.b}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ─── Parallax sin re-renders ─── */
function useParallaxRefsE() {
  const foamA   = useRefE(null);
  const foamB   = useRefE(null);
  const heroTxt = useRefE(null);
  const blobA   = useRefE(null);
  const blobB   = useRefE(null);
  const bridge  = useRefE(null);
  const nav     = useRefE(null);

  useEffectE(() => {
    let raf = null;
    const tick = () => {
      raf = null;
      const y = window.scrollY;
      if (foamA.current)   foamA.current.style.transform   = `translateY(${y * 0.22}px)`;
      if (foamB.current)   foamB.current.style.transform   = `translateY(${y * 0.32}px)`;
      if (heroTxt.current) heroTxt.current.style.transform = `translateY(${-y * 0.28}px)`;
      if (blobA.current)   blobA.current.style.transform   = `translateY(${y * 0.14}px)`;
      if (blobB.current)   blobB.current.style.transform   = `translateY(${y * 0.09}px)`;
      if (bridge.current)  bridge.current.style.transform  = `translateY(${Math.max(0, (y - 300)) * 0.08}px)`;
      if (nav.current)     nav.current.classList.toggle('is-stick', y > 40);
    };
    const onScroll = () => { if (!raf) raf = requestAnimationFrame(tick); };
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => { window.removeEventListener('scroll', onScroll); if (raf) cancelAnimationFrame(raf); };
  }, []);

  return { foamA, foamB, heroTxt, blobA, blobB, bridge, nav };
}

/* ─── VariantE ─── */
function VariantE({ accent }) {
  const { cart, total, final, add, reset, checkout, warning } = useCart();
  const [activeFoam, setActiveFoam]       = useStateE(0);
  const [menuOpen, setMenuOpen]           = useStateE(false);
  const [splashProduct, setSplashProduct] = useStateE(null);
  const [scrolled, setScrolled]           = useStateE(false);
  const px = useParallaxRefsE();

  useEffectE(() => {
    const id = setInterval(() => setActiveFoam(v => (v + 1) % 2), 8000);
    return () => clearInterval(id);
  }, []);

  // Al primer scroll: ocultar el hint "søur ↓" y el subtítulo flotante.
  useEffectE(() => {
    const onS = () => {
      if (window.scrollY > 6) {
        setScrolled(true);
        window.removeEventListener('scroll', onS);
      }
    };
    window.addEventListener('scroll', onS, { passive: true });
    onS();
    return () => window.removeEventListener('scroll', onS);
  }, []);

  useEffectE(() => {
    document.body.style.overflow = (menuOpen || splashProduct) ? 'hidden' : '';
    return () => { document.body.style.overflow = ''; };
  }, [menuOpen, splashProduct]);

  const handleProductClick = useCallbackE((p) => {
    setSplashProduct(p);
    setTimeout(() => setSplashProduct(null), 4800);
  }, []);

  return (
    <div className={`vD ${scrolled ? 'is-scrolled' : ''}`} style={{ '--accent': accent }}>
      {/* Logo + subtítulo fijos en viewport, hijos directos del .vD.
          Las secciones con z-index 5 + bg fade los tapan al subir. */}
      <HeroLogoCanvas />
      <p className="vD-hero-sub-floating">Coctelería congelada</p>

      {/* Aviso de scroll — SOLO móvil (en desktop lo hace el cursor-flecha).
          "desliza" + gota que cae, abajo al centro; se desvanece al primer
          scroll vía .is-scrolled */}
      <div className="vD-swipe-hint" aria-hidden="true">
        <span>desliza</span>
        <i />
      </div>

      {/* ── PRODUCT SPLASH ── */}
      <ProductSplashE product={splashProduct} onClose={() => setSplashProduct(null)} cart={cart} add={add} total={total} />

      {/* ── HERO ── */}
      <section className="vD-hero">
        <div className="vD-hero-foam">
          {/* Espuma en movimiento sincronizada con el scroll, detrás del logo */}
          <HeroFoamCanvas />
        </div>
        <div className="vD-hero-veil" />
        {/* splash-hero-a eliminado: dejaba una mancha translúcida en la
            parte inferior izquierda al cargar la página */}
        <div ref={px.blobB}><SplashBlobE path={BP.B} className="splash-hero-b" /></div>
        {/* hero-text vacío — el logo y subtítulo viven flotando como hijos
            directos del .vD para escapar del stacking aislado del hero */}
      </section>


      {/* ── COLECCIÓN ── */}
      <section className="vD-coleccion">
        <div className="vD-prod-list">
          {PRODUCTS.map((p, idx) => {
            const cocktailImg = p.id === 'maracuya' ? 'images/pisco-sour-maracuya-169.png'
              : p.id === 'frambuesa' ? 'images/pisco-sour-frambuesa-169.png'
              : 'images/pina-colada-169.png';
            return (
              <RevealSectionE key={p.id} delay={idx * 120}>
                <div className={`vD-prod ${idx % 2 === 1 ? 'is-flip' : ''}`} style={{ '--p-tone': p.tone }}>
                  <div className="vD-prod-visual" onClick={() => handleProductClick(p)}
                       data-cursor data-cursor-label="Ver">
                    {/* CAMPO CELULAR animado por producto */}
                    <CellularField tone={p.tone} />
                    <img src={cocktailImg} alt={p.name} className="vD-prod-img" />
                  </div>
                  <div className="vD-prod-info">
                    <span className="vD-prod-n">{p.n}</span>
                    <h3 className="vD-prod-name">{p.name}</h3>
                    <p className="vD-prod-mood">{p.mood}</p>
                    <button className="vD-prod-open" onClick={() => handleProductClick(p)} data-cursor data-cursor-label="Ver">
                      Ver producto →
                    </button>
                  </div>
                </div>
              </RevealSectionE>
            );
          })}
        </div>
      </section>

      {/* ── RITUAL STEPS ── (movido ANTES de CAJA) */}
      <section className="vD-ritual" id="vD-ritual">
        <RevealSectionE>
          <RitualSteps />
        </RevealSectionE>
      </section>

      {/* ── CAJA ── */}
      <section className="vD-caja" id="vD-caja">
        <div className="vD-caja-grid vD-caja-grid-solo">
          <RevealSectionE className="vD-caja-art" delay={200}>
            <CellularField tone={CAJA_TONE} />
            <div className="vD-caja-builder-wrap">
              <div className="vD-builder">
                <div className="vD-builder-head">
                  <span>Tu selección</span>
                  <strong>{total} / {PACK_QTY}</strong>
                </div>
                <div className="vD-builder-bar">
                  <div className="vD-builder-fill" style={{ width: `${(total / PACK_QTY) * 100}%` }} />
                </div>
                {PRODUCTS.map(p => (
                  <div key={p.id} className={`vD-builder-row ${cart[p.id] ? 'is-on' : ''}`}>
                    <div className="vD-builder-name">
                      <span className="vD-dot" style={{ background: p.tone }} />{p.name}
                    </div>
                    <div className="vD-qty">
                      <button onClick={() => add(p.id, -1)} disabled={cart[p.id] <= 0} data-cursor>−</button>
                      <span>{cart[p.id]}</span>
                      <button onClick={() => add(p.id, +1)} disabled={total >= PACK_QTY} data-cursor>+</button>
                    </div>
                  </div>
                ))}
              </div>
              <div className="vD-price-row">
                <div>
                  <div className="vD-price-now">S/{final}</div>
                  {total === PACK_QTY && <div className="vD-price-was"><s>S/90</s> <span>Ahorras S/30</span></div>}
                </div>
                <div className="vD-price-cta">
                  <button className={`vD-cta ${warning ? 'is-warn' : ''}`} onClick={checkout}
                          data-cursor>
                    <span>{warning ? warning : (total === 0 ? 'Componer' : `Pedir · S/${final}`)}</span>
                    <svg viewBox="0 0 24 24" width="14" height="14"><path d="M4 12h16M14 6l6 6-6 6" stroke="currentColor" strokeWidth="1.5" fill="none" /></svg>
                  </button>
                  <button className="vD-cta-link" onClick={reset} data-cursor>Vaciar</button>
                </div>
              </div>
              <div className="vD-caja-note">
                Por la compra de 6 unidades ahorras S/30
              </div>
            </div>
          </RevealSectionE>
        </div>
      </section>

      {/* ── B2B ── */}
      <section className="vD-b2b">
        <RevealSectionE className="vD-b2b-inner">
          <CellularField tone={accent} configs={CELL_CONFIGS_B2B} className="is-b2b" />
          <h2 className="vD-h2">¿Restaurante o evento?</h2>
          <p className="vD-p">Programa B2B con precios mayoristas, soporte de carta y entrega programada.</p>
          <a href="restaurantes.html" className="vD-cta" data-cursor data-cursor-label="Ver">
            <span>Propuesta empresas</span>
            <svg viewBox="0 0 24 24" width="14" height="14"><path d="M4 12h16M14 6l6 6-6 6" stroke="currentColor" strokeWidth="1.5" fill="none" /></svg>
          </a>
        </RevealSectionE>
      </section>

      {/* ── FOOTER ── */}
      <footer className="vD-foot">
        <div className="vD-foot-foam"><img src={FOAM_FRAMBUESA} alt="" /></div>
        <div className="vD-foot-mark"><img src="images/sour-logo.png?v=3" alt="Søur" className="vD-foot-logo" /></div>
        <div className="vD-foot-tag">Coctelería congelada · Hecha en Lima</div>
        <div className="vD-foot-social">
          <a className="vD-foot-ico" href="https://wa.me/51997331112" target="_blank" rel="noopener noreferrer" aria-label="WhatsApp" data-cursor>
            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
              <path d="M21 11.5a8.5 8.5 0 0 1-12.83 7.33L3 20.5l1.7-5.15A8.5 8.5 0 1 1 21 11.5Z"/>
              <path d="M8.6 9.3c.2-.5.6-.55.85-.55h.45c.2 0 .42.02.6.45.2.5.7 1.7.76 1.82.06.13.1.27.02.42-.08.16-.13.26-.25.4-.13.16-.27.34-.39.46-.13.13-.26.26-.12.51.13.26.6 1 1.3 1.62.9.8 1.66 1.05 1.9 1.18.24.13.38.1.52-.07.13-.16.6-.7.76-.95.16-.25.31-.21.53-.13.22.07 1.42.67 1.66.79.25.13.4.19.46.3.07.1.07.58-.13 1.14-.2.56-1.13 1.07-1.6 1.13-.45.07-1.02.1-1.65-.1-.38-.13-.86-.28-1.48-.55-2.6-1.13-4.3-3.78-4.43-3.96-.13-.18-1.05-1.42-1.05-2.7 0-1.3.68-1.93.92-2.2Z"/>
            </svg>
          </a>
          <a className="vD-foot-ico" href="https://instagram.com/sour.pe" target="_blank" rel="noopener noreferrer" aria-label="Instagram" data-cursor>
            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
              <rect x="3" y="3" width="18" height="18" rx="5"/>
              <circle cx="12" cy="12" r="4"/>
              <circle cx="17.5" cy="6.5" r="0.6" fill="currentColor"/>
            </svg>
          </a>
          <a className="vD-foot-ico" href="mailto:cordialperu@gmail.com" aria-label="Correo" data-cursor>
            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
              <rect x="3" y="5" width="18" height="14" rx="2"/>
              <path d="m3.5 6.5 8.5 6.5 8.5-6.5"/>
            </svg>
          </a>
        </div>
      </footer>

      {/* FloatingCTAE eliminado: la mancha flotante "Pedir" que seguía
          al cursor distraía — el pedido vive en la sección Caja */}
    </div>
  );
}

/* ----------------------------------------------------------------
   HeroFoamCanvas — espuma en movimiento (secuencia de frames JPEG)
   controlada por el scroll, exactamente como el logo. Sirve de fondo
   animado detrás del logo: al hacer scroll, la espuma avanza/retrocede
   sincronizada, de modo que el logo parece flotar sobre ella e
   interactuar con la reacción del "hielo" que cae.
   Fondo full-bleed con encuadre "cover". Los frames tienen fondo negro,
   por eso el canvas se compone con mix-blend-mode: screen (CSS) y el
   negro desaparece sobre el fondo crema.
---------------------------------------------------------------- */
function HeroFoamCanvas() {
  const canvasRef = useRef(null);
  const framesRef = useRef([]);
  const lastFrameRef = useRef(-1);
  const rafRef = useRef(null);
  const readyRef = useRef(false);
  const heroHRef = useRef(0);
  const [isMobile, setIsMobile] = useState(() => {
    if (typeof window === 'undefined') return false;
    return window.matchMedia('(max-width: 760px)').matches;
  });

  useEffect(() => {
    if (typeof window === 'undefined') return;
    const mql = window.matchMedia('(max-width: 760px)');
    const handler = (e) => setIsMobile(e.matches);
    mql.addEventListener('change', handler);
    return () => mql.removeEventListener('change', handler);
  }, []);

  useEffect(() => {
    readyRef.current = false;
    lastFrameRef.current = -1;

    const FRAME_PATH = isMobile ? FOAM_FRAME_PATH_MOBILE : FOAM_FRAME_PATH_DESKTOP;
    /* Desktop: los frames fuente miden 832x464 — DPR 1 basta (más DPR no
       añade detalle y duplica el costo). Móvil: DPR 2 — la pantalla es
       chica y el recorte "cover" amplía mucho; el buffer extra mejora
       visiblemente la nitidez del reescalado. */
    const DPR_CAP    = isMobile ? 2 : 1;

    let cancelled = false;
    let loadedCount = 0;
    const imgs = new Array(FOAM_FRAME_COUNT);

    /* La animación usa solo la SEGUNDA MITAD del video (de la mitad en
       adelante): más corta, va directo a la acción de la espuma. */
    const FOAM_START = Math.floor(FOAM_FRAME_COUNT / 2);

    const measure = () => {
      const heroEl = document.querySelector('.vD-hero');
      heroHRef.current = heroEl ? heroEl.offsetHeight : (window.innerHeight * 1.75);
    };
    measure();

    /* Orden de carga intercalado: barrido grueso del rango usado
       (FOAM_START → final) primero, relleno después — el scrub temprano
       tiene cobertura completa con el fallback al frame más cercano.
       La primera mitad del video no se descarga: no se usa. */
    const loadOrder = [];
    const seen = new Set();
    for (const step of [8, 4, 2, 1]) {
      for (let i = FOAM_START; i < FOAM_FRAME_COUNT; i += step) {
        if (!seen.has(i)) { seen.add(i); loadOrder.push(i); }
      }
    }
    window.__sourPreload.total += (FOAM_FRAME_COUNT - FOAM_START);
    for (const i of loadOrder) {
      const img = new Image();
      img.decoding = 'async';
      img.onload = () => {
        if (cancelled) return;
        loadedCount++;
        window.__sourPreload.loaded++;
        readyRef.current = true;
        /* invalidar el último frame dibujado: el bucle continuo redibuja
           apenas llega cada imagen (la espuma aparece al cargar, sin
           esperar al primer scroll) */
        lastFrameRef.current = -1;
        /* PRE-DECODIFICAR: en móvil a ImageBitmap (elimina micro-tirones
           del scrub). En desktop solo img.decode() — mantener cientos de
           MB de bitmaps fijados colgaba el scroll rápido; decode() deja
           la caché de decodificación en manos del navegador. */
        if (isMobile && window.createImageBitmap) {
          createImageBitmap(img).then(b => { if (!cancelled) imgs[i] = b; }).catch(() => {});
        } else if (img.decode) {
          img.decode().catch(() => {});
        }
      };
      img.src = FRAME_PATH(i + 1);
      imgs[i] = img;
    }
    framesRef.current = imgs;

    /* Image: exigir COMPLETE (un JPEG progresivo reporta dimensiones a
       medio descargar y dibujarlo produce flashes). ImageBitmap (sin
       .complete) siempre está decodificado entero. */
    const okFrame = (f) => !!f && (f.complete === undefined
      ? (f.width || 0) > 0
      : (f.complete && f.naturalWidth > 0));
    const pickFrame = (idx) => {
      let f = framesRef.current[idx];
      if (okFrame(f)) return f;
      for (let i = idx; i >= 0; i--) {
        const c = framesRef.current[i];
        if (okFrame(c)) return c;
      }
      return null;
    };

    /* Dibujo con fundido entre frames adyacentes: la posición fraccional
       (p.ej. 71.6) pinta el frame 71 y encima el 72 al 60% de opacidad.
       El paso de frame a frame deja de ser un corte seco (el "temblor")
       y se convierte en un fundido continuo, tipo motion blur. */
    const drawAt = (pos) => {
      const canvas = canvasRef.current;
      if (!canvas) return;
      const i0 = Math.max(0, Math.min(FOAM_FRAME_COUNT - 1, Math.floor(pos)));
      const i1 = Math.min(FOAM_FRAME_COUNT - 1, i0 + 1);
      const frac = Math.max(0, Math.min(1, pos - i0));
      const f0 = pickFrame(i0);
      if (!f0) return;
      const f1 = (frac > 0.02 && i1 !== i0) ? pickFrame(i1) : null;
      lastFrameRef.current = pos;

      const ctx = canvas.getContext('2d');
      const dpr = Math.min(window.devicePixelRatio || 1, DPR_CAP);
      const cw = canvas.clientWidth, ch = canvas.clientHeight;
      const tw = Math.max(1, Math.round(cw * dpr));
      const th = Math.max(1, Math.round(ch * dpr));
      if (canvas.width !== tw || canvas.height !== th) { canvas.width = tw; canvas.height = th; }

      /* encuadre "cover" clásico: la espuma llena toda la pantalla y se
         dibuja TAL CUAL (Image o ImageBitmap, ya decodificado) */
      const cover = (frame) => {
        const fw = frame.width, fh = frame.height;
        const scale = Math.max(canvas.width / fw, canvas.height / fh);
        const dw = fw * scale, dh = fh * scale;
        ctx.drawImage(frame, (canvas.width - dw) / 2, (canvas.height - dh) / 2, dw, dh);
      };
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.globalAlpha = 1;
      cover(f0);
      if (f1 && f1 !== f0) {
        ctx.globalAlpha = frac;
        cover(f1);
        ctx.globalAlpha = 1;
      }

      /* MÓVIL: banda de dilución espacial + remate sinuoso, anclados al
         scroll. Diluyen la parte baja del video (donde está el borde
         claro del vaso) y la integran con el crema — la versión que ya
         estaba casi perfecta, ahora sobre frames pre-decodificados. */
      if (isMobile) {
        /* Sello permanente: el borde INFERIOR del canvas se diluye
           siempre un poco — al comenzar el scroll (antes de que la banda
           se active) el corte del video dejaba una línea blanca fina en
           la base de la pantalla. */
        const Hc = canvas.height;
        const gb = ctx.createLinearGradient(0, Hc * 0.90, 0, Hc);
        gb.addColorStop(0, 'rgba(0,0,0,0)');
        gb.addColorStop(0.6, 'rgba(0,0,0,0.5)');
        gb.addColorStop(1, 'rgba(0,0,0,0.95)');
        ctx.globalCompositeOperation = 'destination-out';
        ctx.fillStyle = gb;
        ctx.fillRect(0, Hc * 0.90, canvas.width, Hc * 0.10);
        ctx.globalCompositeOperation = 'source-over';

        const range = Math.max(1, heroHRef.current);
        const heroBottom = range - window.scrollY; /* px desde el top del viewport */
        const ih = window.innerHeight;
        if (heroBottom < ih * 2.1) {
          const yEdge = heroBottom - ih * 0.08; /* css px, puede ser negativo */
          const y1 = (heroBottom - ih * 0.95) * dpr;
          const y2 = (yEdge - ih * 0.05) * dpr;
          const g = ctx.createLinearGradient(0, y1, 0, Math.max(y1 + 1, y2));
          g.addColorStop(0,    'rgba(0,0,0,0)');
          g.addColorStop(0.22, 'rgba(0,0,0,0.06)');
          g.addColorStop(0.45, 'rgba(0,0,0,0.28)');
          g.addColorStop(0.68, 'rgba(0,0,0,0.60)');
          g.addColorStop(0.86, 'rgba(0,0,0,0.84)');
          g.addColorStop(1,    'rgba(0,0,0,0.94)');
          ctx.globalCompositeOperation = 'destination-out';
          ctx.fillStyle = g;
          const yFill = Math.max(0, y1);
          ctx.fillRect(0, yFill, canvas.width, canvas.height - yFill);

          /* remate sinuoso animado */
          const t = Date.now() * 0.0006;
          const amp = ih * 0.085;
          const W = canvas.width;
          ctx.beginPath();
          ctx.moveTo(0, canvas.height);
          ctx.lineTo(0, (yEdge + amp * Math.sin(t * 1.3)) * dpr);
          const STEPS = 22;
          for (let s = 1; s <= STEPS; s++) {
            const x = s / STEPS;
            const yw = yEdge
              + amp * Math.sin(x * Math.PI * 2.3 + t * 1.7)
              + amp * 0.5 * Math.sin(x * Math.PI * 5.1 - t * 1.1);
            ctx.lineTo(x * W, yw * dpr);
          }
          ctx.lineTo(W, canvas.height);
          ctx.closePath();
          ctx.fillStyle = 'rgba(0,0,0,1)';
          ctx.fill();
          ctx.globalCompositeOperation = 'source-over';
        }
      }
    };

    /* ── Bucle continuo de suavizado ──
       En vez de saltar al frame que dicta el scroll (se siente áspero),
       la posición actual PERSIGUE al objetivo con interpolación
       exponencial y se pinta con fundido sub-frame: fluido incluso con
       scrolls bruscos. El bucle solo corre mientras el hero es visible. */
    let cur = FOAM_START;
    let running = false;
    /* misma persecución cremosa que desktop — 0.15 se sentía seco */
    const CHASE = isMobile ? 0.10 : 0.07;

    let lastFoamOp = -1;
    let lastDrawY = -1;
    const tick = () => {
      if (!running) return;
      /* el canvas es fixed (cero lag): solo hay que scrubear los frames */
      const range = Math.max(1, heroHRef.current);
      const y = Math.max(0, Math.min(window.scrollY, range));
      /* Disolvencia de salida: UNA sola mecánica, opacidad con curva
         smoothstep (arranque y final imperceptibles). En móvil termina
         al 68% del hero — la espuma ya no existe cuando llega cualquier
         contenido, así no hay borde posible, a ninguna velocidad. */
      const canvas = canvasRef.current;
      if (canvas) {
        const FA = isMobile ? 0.42 : 0.74;
        const FB = isMobile ? 0.26 : 0.20;
        let k = 1 - Math.max(0, Math.min(1, (window.scrollY - range * FA) / (range * FB)));
        k = k * k * (3 - 2 * k); /* smoothstep */
        if (Math.abs(k - lastFoamOp) > 0.004) {
          canvas.style.opacity = k;
          lastFoamOp = k;
        }
      }
      const target = FOAM_START + (y / range) * (FOAM_FRAME_COUNT - 1 - FOAM_START);
      cur += (target - cur) * CHASE;
      if (Math.abs(target - cur) < 0.02) cur = target;
      const frameMoved = Math.abs(cur - lastFrameRef.current) > 0.03;
      const waveVisible = isMobile
        && (range - window.scrollY) < window.innerHeight * 2.1
        && window.scrollY < range;
      if (readyRef.current && (frameMoved || waveVisible)) {
        drawAt(cur);
        lastDrawY = window.scrollY;
      }
      rafRef.current = requestAnimationFrame(tick);
    };

    const startLoop = () => {
      if (running) return;
      running = true;
      if (canvasRef.current) canvasRef.current.style.visibility = '';
      rafRef.current = requestAnimationFrame(tick);
    };
    const stopLoop = () => {
      running = false;
      /* el canvas fixed no se va con el scroll: ocultarlo al salir del hero */
      if (canvasRef.current) canvasRef.current.style.visibility = 'hidden';
      if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; }
    };

    const heroEl = document.querySelector('.vD-hero');
    let io = null;
    if (heroEl && typeof IntersectionObserver !== 'undefined') {
      io = new IntersectionObserver(([entry]) => {
        if (entry.isIntersecting) startLoop();
        else stopLoop();
      }, { rootMargin: '200px' });
      io.observe(heroEl);
      startLoop();
    } else {
      startLoop();
    }

    let resizeTimer = null;
    let lastW = window.innerWidth;
    const onResize = () => {
      /* en móvil el colapso de la barra de URL dispara resize SOLO de
         altura: ignorarlo (re-medir ahí provocaba saltos en el scroll) */
      if (window.innerWidth === lastW) return;
      lastW = window.innerWidth;
      if (resizeTimer) return;
      resizeTimer = setTimeout(() => {
        resizeTimer = null;
        measure();
        lastFrameRef.current = -1;
      }, 200);
    };
    window.addEventListener('resize', onResize);

    return () => {
      cancelled = true;
      if (resizeTimer) clearTimeout(resizeTimer);
      stopLoop();
      window.removeEventListener('resize', onResize);
      if (io) io.disconnect();
      /* si el efecto se re-ejecuta (rotación), retirar nuestra cuenta */
      window.__sourPreload.total -= (FOAM_FRAME_COUNT - FOAM_START);
      window.__sourPreload.loaded -= loadedCount;
    };
  }, [isMobile]);

  return <canvas ref={canvasRef} className="vD-foam-canvas" aria-hidden="true" />;
}

/* ----------------------------------------------------------------
   HeroLogoCanvas — PNG sequence con alpha real, controlada por scroll
   Precarga 125 frames y dibuja el que corresponde según scrollY.
   Mientras carga, muestra un PNG estático del logo como fallback.
---------------------------------------------------------------- */
// Desktop: secuencia full (136 frames a 720x720, ~16 MB)
// Móvil: la MISMA secuencia completa en WebP 560px (~2.7 MB) — con 45
//        frames el fundido mezclaba poses muy separadas y el giro se
//        veía borroso; con los 136 el paso entre frames es mínimo.
const LOGO_FRAME_COUNT_DESKTOP = 136;
const LOGO_FRAME_COUNT_MOBILE = 136;
const LOGO_FRAME_PATH_DESKTOP = (i) => `images/logo-frames/f${String(i).padStart(3, '0')}.png?v=2`;
const LOGO_FRAME_PATH_MOBILE  = (i) => `images/logo-frames-mobile-hd/f${String(i).padStart(3, '0')}.webp?v=2`;

function HeroLogoCanvas() {
  const canvasRef = useRef(null);
  const framesRef = useRef([]);
  const lastFrameRef = useRef(-1);
  const rafRef = useRef(null);
  const readyRef = useRef(false);
  const heroHRef = useRef(0);
  const hasDrawnRef = useRef(false);
  const [loaded, setLoaded] = useState(false);
  const [isMobile, setIsMobile] = useState(() => {
    if (typeof window === 'undefined') return false;
    return window.matchMedia('(max-width: 760px)').matches
        || window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  });

  // Listener para cambios de orientación / viewport
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const mql = window.matchMedia('(max-width: 760px)');
    const handler = (e) => setIsMobile(e.matches);
    mql.addEventListener('change', handler);
    return () => mql.removeEventListener('change', handler);
  }, []);

  useEffect(() => {
    // Reset state cuando cambia desktop ↔ móvil (rotación)
    setLoaded(false);
    readyRef.current = false;
    lastFrameRef.current = -1;
    hasDrawnRef.current = false;

    const FRAME_COUNT = isMobile ? LOGO_FRAME_COUNT_MOBILE : LOGO_FRAME_COUNT_DESKTOP;
    const FRAME_PATH  = isMobile ? LOGO_FRAME_PATH_MOBILE  : LOGO_FRAME_PATH_DESKTOP;
    /* DPR 2 también en móvil: los frames HD (560px) dan de sobra para
       un canvas de ~330px CSS en pantalla retina → logo nítido */
    const DPR_CAP     = 2;

    let cancelled = false;
    let loadedCount = 0;
    const imgs = new Array(FRAME_COUNT);

    const measure = () => {
      const heroEl = document.querySelector('.vD-hero');
      heroHRef.current = heroEl ? heroEl.offsetHeight : (window.innerHeight * 1.75);
    };
    measure();

    /* Orden de carga intercalado: primero un barrido grueso de TODA la
       secuencia (cada 8º frame), luego se rellena. Así un scroll temprano
       encuentra cobertura en todo el rango (con el fallback al frame
       cargado más cercano) en vez de solo el principio. */
    const loadOrder = [];
    const seen = new Set();
    for (const step of [8, 4, 2, 1]) {
      for (let i = 0; i < FRAME_COUNT; i += step) {
        if (!seen.has(i)) { seen.add(i); loadOrder.push(i); }
      }
    }
    window.__sourPreload.total += FRAME_COUNT;
    for (const i of loadOrder) {
      const img = new Image();
      img.decoding = 'async';
      img.onload = () => {
        if (cancelled) return;
        loadedCount++;
        window.__sourPreload.loaded++;
        if (loadedCount === 1) readyRef.current = true;
        /* PRE-DECODIFICAR: en móvil a ImageBitmap (el WebP se decodificaba
           en pleno scrub y el giro se veía entrecortado). En desktop solo
           img.decode() — los 136 PNG de 720px como bitmaps fijados eran
           cientos de MB y colgaban el scroll rápido. */
        if (isMobile && window.createImageBitmap) {
          createImageBitmap(img).then(b => { if (!cancelled) imgs[i] = b; }).catch(() => {});
        } else if (img.decode) {
          img.decode().catch(() => {});
        }
      };
      img.src = FRAME_PATH(i + 1);
      imgs[i] = img;
    }
    framesRef.current = imgs;

    /* Dibujo con fundido entre frames adyacentes: una posición fraccional
       (p.ej. 41.4) pinta el frame 41 y encima el 42 al 40% de opacidad.
       El giro se percibe continuo, sin saltos de frame visibles. */
    /* Image: exigir COMPLETE (a medio descargar produce flashes).
       ImageBitmap (sin .complete) siempre está decodificado entero. */
    const okFrame = (f) => !!f && (f.complete === undefined
      ? (f.width || 0) > 0
      : (f.complete && f.naturalWidth > 0));
    const pickFrame = (idx) => {
      let f = framesRef.current[idx];
      if (okFrame(f)) return f;
      for (let i = idx; i >= 0; i--) {
        const c = framesRef.current[i];
        if (okFrame(c)) return c;
      }
      return null;
    };

    const drawAt = (pos) => {
      const canvas = canvasRef.current;
      if (!canvas || !readyRef.current) return;
      const i0 = Math.max(0, Math.min(FRAME_COUNT - 1, Math.floor(pos)));
      const i1 = Math.min(FRAME_COUNT - 1, i0 + 1);
      const frac = Math.max(0, Math.min(1, pos - i0));

      const f0 = pickFrame(i0);
      if (!f0) return;
      const f1 = (frac > 0.02 && i1 !== i0) ? pickFrame(i1) : null;

      const ctx = canvas.getContext('2d');
      const cssSize = canvas.clientWidth;
      const dpr = Math.min(window.devicePixelRatio || 1, DPR_CAP);
      const target = Math.round(cssSize * dpr);
      if (canvas.width !== target) {
        canvas.width = target;
        canvas.height = target;
      }
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.globalAlpha = 1;
      ctx.drawImage(f0, 0, 0, canvas.width, canvas.height);
      if (f1 && f1 !== f0) {
        ctx.globalAlpha = frac;
        ctx.drawImage(f1, 0, 0, canvas.width, canvas.height);
        ctx.globalAlpha = 1;
      }
      // Primer paint exitoso → ocultar el fallback img
      if (!hasDrawnRef.current) {
        hasDrawnRef.current = true;
        setLoaded(true);
      }
    };

    const safetyTimer = setTimeout(() => {
      if (!cancelled && !hasDrawnRef.current) {
        hasDrawnRef.current = true;
        setLoaded(true);
      }
    }, 3000);

    /* ── Bucle de persecución exponencial (igual que la espuma) ──
       En vez de saltar al frame que dicta el scroll (áspero con rueda de
       pasos grandes), la posición actual PERSIGUE al objetivo con
       interpolación exponencial. El giro queda fluido y elegante incluso
       con scrolls bruscos. Corre solo mientras el hero es visible. */
    let cur = 0;
    let lastPaint = -10;
    let lastOpacity = -1;
    let running = false;
    /* misma suavidad que desktop también en el giro móvil */
    const CHASE = isMobile ? 0.10 : 0.085;

    const tick = () => {
      if (!running) return;
      const range = Math.max(1, heroHRef.current);
      const yRaw = Math.max(0, Math.min(window.scrollY, range));

      /* Primero corre sola la animación de espuma; el símbolo aparece
         RECIÉN después de varios cuadros, con un fade in lento y pausado
         (12%→34% del hero), gira del 26% al 86% y termina en su frame
         transparente — nunca se trasluce por detrás de la web. */
      const canvas = canvasRef.current;
      if (canvas) {
        const fadeIn = Math.max(0, Math.min(1, (yRaw - range * 0.12) / (range * 0.22)));
        if (Math.abs(fadeIn - lastOpacity) > 0.004) {
          canvas.style.opacity = fadeIn;
          lastOpacity = fadeIn;
        }
      }

      const startY = range * 0.26;
      const endY   = range * 0.86; /* el giro completa antes del fin del hero */
      const p = Math.max(0, Math.min(1, (yRaw - startY) / (endY - startY)));
      const target = p * (FRAME_COUNT - 1);
      cur += (target - cur) * CHASE;
      if (Math.abs(target - cur) < 0.02) cur = target;

      if (readyRef.current && Math.abs(cur - lastPaint) > 0.04) {
        drawAt(cur);
        lastPaint = cur;
      }
      rafRef.current = requestAnimationFrame(tick);
    };

    const startLoop = () => {
      if (running) return;
      running = true;
      lastOpacity = -1; /* re-aplicar opacidad al volver al hero */
      rafRef.current = requestAnimationFrame(tick);
    };
    const stopLoop = () => {
      running = false;
      if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; }
      /* Si salimos del hero hacia abajo, FORZAR el logo invisible: las
         secciones tienen degradados transparentes en su borde superior y
         el logo fijo se traslucía por ahí en la parte baja de la web. */
      if (canvasRef.current && window.scrollY > heroHRef.current * 0.5) {
        canvasRef.current.style.opacity = 0;
        lastOpacity = 0;
      }
    };

    // IntersectionObserver: cuando el hero NO está visible, suspendemos
    // el bucle completamente. Ahorra trabajo pasado el hero.
    const heroEl = document.querySelector('.vD-hero');
    let io = null;
    if (heroEl && typeof IntersectionObserver !== 'undefined') {
      io = new IntersectionObserver(([entry]) => {
        if (entry.isIntersecting) startLoop();
        else stopLoop();
      }, { rootMargin: '200px' });
      io.observe(heroEl);
      startLoop(); // arrancar visible
    } else {
      startLoop();
    }

    // Resize con debounce — ignora cambios solo de altura (barra de URL
    // móvil): re-medir ahí producía saltos durante el scroll
    let resizeTimer = null;
    let lastW = window.innerWidth;
    const onResize = () => {
      if (window.innerWidth === lastW) return;
      lastW = window.innerWidth;
      if (resizeTimer) return;
      resizeTimer = setTimeout(() => {
        resizeTimer = null;
        measure();
        lastPaint = -10; /* forzar repintado con el nuevo tamaño */
      }, 200);
    };
    window.addEventListener('resize', onResize);

    return () => {
      cancelled = true;
      clearTimeout(safetyTimer);
      if (resizeTimer) clearTimeout(resizeTimer);
      stopLoop();
      window.removeEventListener('resize', onResize);
      if (io) io.disconnect();
      /* si el efecto se re-ejecuta (rotación), retirar nuestra cuenta */
      window.__sourPreload.total -= FRAME_COUNT;
      window.__sourPreload.loaded -= loadedCount;
    };
  }, [isMobile]);

  return (
    <div className="vD-hero-canvas-wrap">
      <canvas
        ref={canvasRef}
        className="vD-hero-solo-o vD-hero-solo-canvas"
        style={{ opacity: 0 }}
        aria-label="Søur"
      />
    </div>
  );
}

window.VariantE = VariantE;
