/* ============================================================
   AGROTHECA — shared components & hooks  (exported to window)
   ============================================================ */
const { useState, useEffect, useRef, useCallback } = React;

/* ---------------- Inline icon set (stroke) ---------------- */
const ICON_PATHS = {
  leaf: 'M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z|M2 21c0-3 1.85-5.36 5.08-6',
  bug: 'M8 2l1.88 1.88|M14.12 3.88 16 2|M9 7.13v-1a3.003 3.003 0 1 1 6 0v1|M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6Z|M12 20v-9|M6.53 9C4.6 8.8 3 7.1 3 5|M6 13H2|M3 21c0-2.1 1.7-3.9 3.8-4|M20.97 5c0 2.1-1.6 3.8-3.5 4|M22 13h-4|M17.2 17c2.1.1 3.8 1.9 3.8 4',
  sprout: 'M7 20h10|M10 20c5.5-2.5.8-6.4 3-10|M9.5 9.4c1.1.8 1.8 2.2 2.3 3.7-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2 2.8-.5 4.4 0 5.5.8Z|M14.1 6a7 7 0 0 0-1.1 4c1.9-.1 3.3-.6 4.3-1.4 1-1 1.6-2.3 1.7-4.6-2.7.1-4 1-4.9 2Z',
  cloud: 'M17.5 19a4.5 4.5 0 1 0 0-9h-1.8A7 7 0 1 0 4 16.3|M12 12l-3 5h4l-3 5',
  terminal: 'M7 11l3-3-3-3|M13 15h5',
  search: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z|M21 21l-4.3-4.3',
  microscope: 'M6 18h8|M3 22h18|M14 22a7 7 0 0 0 0-14|M9 14h2|M9 12a2 2 0 0 1-2-2V6h4v4a2 2 0 0 1-2 2Z|M12 6V3a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v3',
  database: 'M12 8c4.97 0 9-1.34 9-3s-4.03-3-9-3-9 1.34-9 3 4.03 3 9 3Z|M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5|M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3',
  arrowRight: 'M5 12h14|M13 6l6 6-6 6',
  arrowUpRight: 'M7 17 17 7|M7 7h10v10',
  plus: 'M12 5v14|M5 12h14',
  play: 'M6 4l14 8-14 8V4Z',
  menu: 'M3 6h18|M3 12h18|M3 18h18',
  globe: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z|M3.6 9h16.8|M3.6 15h16.8|M12 3a14 14 0 0 0 0 18|M12 3a14 14 0 0 1 0 18'
};
function Ico({ name, size = 18, sw = 1.6, style }) {
  const d = ICON_PATHS[name] || '';
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
    stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round" style={style}>
      {d.split('|').map((p, i) => <path key={i} d={p} />)}
    </svg>);

}

/* ---------------- useInView (IO + rect fallback) ---------------- */
function useInView(opts) {
  const ref = useRef(null);
  const [inView, setInView] = useState(false);
  useEffect(() => {
    const el = ref.current;if (!el) return;
    let done = false,io = null,t = 0;
    const cleanup = () => {
      if (io) io.disconnect();
      window.removeEventListener('scroll', check, true);
      window.removeEventListener('resize', check);
      clearTimeout(t);
    };
    const reveal = () => {if (done) return;done = true;setInView(true);cleanup();};
    function check() {
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight || document.documentElement.clientHeight;
      if (r.top < vh * 0.92 && r.bottom > 0) reveal();
    }
    if (typeof IntersectionObserver !== 'undefined') {
      io = new IntersectionObserver(([e]) => {if (e.isIntersecting) reveal();},
      { threshold: 0.15, rootMargin: '0px 0px -8% 0px', ...(opts || {}) });
      io.observe(el);
    }
    // Fallback for environments where IO never fires: rect checks on scroll + initial.
    window.addEventListener('scroll', check, true);
    window.addEventListener('resize', check);
    requestAnimationFrame(check);
    t = setTimeout(check, 350);
    return cleanup;
  }, []);
  return [ref, inView];
}

/* ---------------- Reveal wrapper ---------------- */
function Reveal({ as = 'div', delay, className = '', style, children, ...rest }) {
  const [ref, inView] = useInView();
  const Tag = as;
  return (
    <Tag ref={ref} style={style}
    className={`reveal ${delay ? 'reveal-d' + delay : ''} ${inView ? 'in' : ''} ${className}`} {...rest}>
      {children}
    </Tag>);

}

/* ---------------- RiseText (staggered letters) ---------------- */
function RiseText({ text, className = '', style, step = 0.04, start = 0.05, trigger }) {
  const [ref, inView] = useInView({ threshold: 0.25 });
  const on = trigger === undefined ? inView : trigger;
  const words = String(text).split(' ');
  let idx = 0;
  return (
    <span ref={ref} className={className} style={{ ...style }}>
      {words.map((w, wi) =>
      <span key={wi} style={{ display: 'inline-block', whiteSpace: 'nowrap' }}>
          {[...w].map((ch, ci) => {
          const d = start + idx++ * step;
          return (
            <span key={ci} className={`rise ${on ? 'in' : ''}`}>
                <span style={{ transitionDelay: `${d}s` }}>{ch}</span>
              </span>);

        })}
          {wi < words.length - 1 &&
        <span className={`rise ${on ? 'in' : ''}`}><span style={{ transitionDelay: `${start + idx++ * step}s` }}>{'\u00A0'}</span></span>}
        </span>
      )}
    </span>);

}

/* ---------------- OrganicTransitionImage (dissolve) ---------------- */
function OrganicTransitionImage({ src, alt = '', className = '', style }) {
  const base = useRef('org' + Math.random().toString(36).slice(2)).current;
  const fIn = base + '-in',fOut = base + '-out';
  const dispIn = useRef(null),dispOut = useRef(null);
  const raf = useRef(0);
  const [st, setSt] = useState({ curr: src, prev: null, p: 1 });

  useEffect(() => {
    setSt((s) => s.curr === src ? s : { curr: src, prev: s.curr, p: 0 });
  }, [src]);

  useEffect(() => {
    if (st.prev == null) return;
    let start = null;const dur = 1000;
    const tick = (t) => {
      if (start === null) start = t;
      const p = Math.min(1, (t - start) / dur);
      const enter = 1 - Math.pow(1 - p, 4);
      const exit = Math.pow(p, 3);
      if (dispIn.current) dispIn.current.setAttribute('scale', String((1 - enter) * 130));
      if (dispOut.current) dispOut.current.setAttribute('scale', String(exit * 170));
      setSt((s) => ({ ...s, p }));
      if (p < 1) raf.current = requestAnimationFrame(tick);else
      setSt((s) => ({ curr: s.curr, prev: null, p: 1 }));
    };
    raf.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf.current);
  }, [st.prev, st.curr]);

  const enter = 1 - Math.pow(1 - st.p, 4);
  const exit = Math.pow(st.p, 3);
  const animating = st.prev != null;
  const imgBase = { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' };

  return (
    <div className={className} style={{ position: 'relative', overflow: 'hidden', ...style }}>
      <svg width="0" height="0" aria-hidden="true" style={{ position: 'absolute' }}>
        <filter id={fIn} x="-30%" y="-30%" width="160%" height="160%">
          <feTurbulence type="fractalNoise" baseFrequency="0.9 1.5" numOctaves="3" seed="11" result="n" />
          <feDisplacementMap ref={dispIn} in="SourceGraphic" in2="n" scale="0" xChannelSelector="R" yChannelSelector="G" />
        </filter>
        <filter id={fOut} x="-30%" y="-30%" width="160%" height="160%">
          <feTurbulence type="fractalNoise" baseFrequency="0.9 1.5" numOctaves="3" seed="4" result="n" />
          <feDisplacementMap ref={dispOut} in="SourceGraphic" in2="n" scale="0" xChannelSelector="R" yChannelSelector="G" />
        </filter>
      </svg>
      {animating &&
      <img src={st.prev} alt="" referrerPolicy="no-referrer" style={{
        ...imgBase, filter: `url(#${fOut})`, opacity: 1 - exit,
        transform: `scale(${1 + exit * 0.07})`
      }} />
      }
      <img key={st.curr} src={st.curr} alt={alt} referrerPolicy="no-referrer" style={{
        ...imgBase, filter: animating ? `url(#${fIn})` : 'none',
        opacity: animating ? Math.max(0, enter) : 1,
        transform: `scale(${1 + (1 - enter) * 0.05})`
      }} />
    </div>);

}

/* ---------------- Brand corner ---------------- */
function BrandCorner({ dark = false, tagline = true, href = 'index.html' }) {
  return (
    <a href={href} className="brand-corner" aria-label="AGROTECHA — inicio">
      <img src="assets/logo-mark.png" alt="" style={{ height: "48px", width: "35px" }} />
      <span className="wm">
        <b style={dark ? { color: 'var(--cream)' } : null}>
          AGRO<span style={{ fontWeight: 400, color: dark ? 'var(--on-dark-60)' : 'var(--chloro)' }}>TECHA</span>
        </b>
        {tagline && <small style={dark ? { color: 'var(--on-dark-35)' } : null}>CIENCIA AGRÍCOLA PARA MENTES CURIOSAS</small>}
      </span>
    </a>);

}

/* ---------------- Animated counter numeral ---------------- */
function Counter({ value, total = 5 }) {
  const n = String(value + 1).padStart(2, '0');
  return (
    <span className="mono" style={{ display: 'inline-flex', alignItems: 'baseline', gap: 6, fontSize: 12 }}>
      <span style={{ position: 'relative', display: 'inline-block', height: '1em', overflow: 'hidden', width: '1.4em' }}>
        <span key={n} style={{ display: 'inline-block', animation: 'numRise .6s var(--ease)' }}>{n}</span>
      </span>
      <span style={{ color: 'var(--on-dark-35)' }}>/</span>
      <span style={{ color: 'var(--on-dark-35)' }}>{String(total).padStart(2, '0')}</span>
    </span>);

}

/* ---------------- RotatingWord (cycles words in a loop) ---------------- */
function RotatingWord({ words, interval = 2200, className = '', style }) {
  const [i, setI] = useState(0);
  const [phase, setPhase] = useState('in'); // 'in' | 'out'
  useEffect(() => {
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) {
      const id = setInterval(() => setI((p) => (p + 1) % words.length), interval + 600);
      return () => clearInterval(id);
    }
    let outT, swapT;
    const id = setInterval(() => {
      setPhase('out');
      outT = setTimeout(() => {
        setI((p) => (p + 1) % words.length);
        setPhase('in');
      }, 520);
    }, interval);
    return () => { clearInterval(id); clearTimeout(outT); clearTimeout(swapT); };
  }, [words, interval]);

  // width follows the widest word so layout doesn't jump
  const widest = words.reduce((a, b) => (b.length > a.length ? b : a), '');
  return (
    <span className={`rot-word ${className}`} style={{ ...style }}>
      <span className="rot-ghost" aria-hidden="true">{widest}</span>
      <span key={i} className={`rot-cur ${phase === 'out' ? 'rot-out' : 'rot-in'}`}>{words[i]}</span>
    </span>
  );
}

/* ---------------- RootsBackdrop (book + growing roots, looping canvas) ---------------- */
function RootsBackdrop({ accent = '#C27D38' }) {
  const ref = useRef(null);
  useEffect(() => {
    const cv = ref.current; if (!cv) return;
    const ctx = cv.getContext('2d');
    // brighter stroke colour derived from accent (keeps brand hue, more visible)
    function lighten(hex, amt) {
      const n = parseInt(hex.slice(1), 16); let r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
      r = Math.round(r + (255 - r) * amt); g = Math.round(g + (255 - g) * amt); b = Math.round(b + (255 - b) * amt);
      return `rgb(${r},${g},${b})`;
    }
    const stroke = lighten(accent, 0.22);
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    let w = 0, h = 0, dpr = Math.min(2, window.devicePixelRatio || 1), raf = 0;
    const GROW = 6.2, HOLD = 1.6, FADE = 2.0, SPAWN_AT = 0.62; // seconds / fraction

    function bookSpine() { return [w / 2, Math.min(h - 40, h * 0.88)]; }
    function makeTree() {
      const [bx, by] = bookSpine();
      const tree = [];
      const baseLen = Math.min(h * 0.2, 150);
      function branch(x, y, ang, len, wd, depth, t0) {
        if (depth <= 0 || len < 6) return;
        const x2 = x + Math.cos(ang) * len, y2 = y + Math.sin(ang) * len;
        const off = (Math.random() - 0.5) * len * 0.5;
        const cx = (x + x2) / 2 + Math.cos(ang + Math.PI / 2) * off;
        const cy = (y + y2) / 2 + Math.sin(ang + Math.PI / 2) * off;
        const t1 = Math.min(1, t0 + 0.15 + Math.random() * 0.05);
        tree.push({ x, y, cx, cy, x2, y2, w: wd, t0: Math.min(t0, 0.95), t1 });
        const n = Math.random() < 0.65 ? 2 : 3;
        for (let i = 0; i < n; i++) {
          const spread = 0.42 + Math.random() * 0.5;
          const na = ang + (i === 0 ? -spread : i === 1 ? spread : (Math.random() - 0.5) * 0.4);
          branch(x2, y2, na, len * (0.66 + Math.random() * 0.12), wd * 0.72, depth - 1, t1);
        }
      }
      branch(bx, by, -Math.PI / 2 - 0.42, baseLen * 0.82, 4.0, 6, 0.01);
      branch(bx, by, -Math.PI / 2 - 0.2, baseLen, 4.6, 6, 0);
      branch(bx, by, -Math.PI / 2 + 0.2, baseLen, 4.6, 6, 0.02);
      branch(bx, by, -Math.PI / 2 + 0.42, baseLen * 0.82, 4.0, 6, 0.01);
      branch(bx, by, -Math.PI / 2, baseLen * 1.08, 5.0, 6, 0);
      return tree;
    }
    let gens = [];
    function resize() {
      w = cv.clientWidth; h = cv.clientHeight;
      cv.width = w * dpr; cv.height = h * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      gens = [{ birth: performance.now() / 1000, tree: makeTree() }];
    }
    resize();
    const ro = new ResizeObserver(resize); ro.observe(cv);

    function quad(p, x0, y0, cx, cy, x1, y1) {
      const m = 1 - p; return [m * m * x0 + 2 * m * p * cx + p * p * x1, m * m * y0 + 2 * m * p * cy + p * p * y1];
    }
    function drawTree(tree, g, alpha) {
      ctx.save(); ctx.globalAlpha = alpha; ctx.lineCap = 'round'; ctx.strokeStyle = stroke;
      ctx.shadowColor = accent; ctx.shadowBlur = 10;
      for (const s of tree) {
        const local = (g - s.t0) / (s.t1 - s.t0); if (local <= 0) continue;
        const f = Math.min(1, local);
        ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineWidth = s.w;
        for (let i = 1; i <= 8; i++) { const p = (i / 8) * f; const pt = quad(p, s.x, s.y, s.cx, s.cy, s.x2, s.y2); ctx.lineTo(pt[0], pt[1]); }
        ctx.stroke();
        if (f < 1) { const pt = quad(f, s.x, s.y, s.cx, s.cy, s.x2, s.y2); ctx.beginPath(); ctx.arc(pt[0], pt[1], s.w * 0.9 + 1.4, 0, 7); ctx.fillStyle = stroke; ctx.fill(); }
      }
      ctx.restore();
    }
    function drawBook(alpha) {
      const [bx, by] = bookSpine();
      ctx.save(); ctx.globalAlpha = alpha; ctx.strokeStyle = accent; ctx.lineWidth = 2.2;
      ctx.lineJoin = 'round'; ctx.shadowColor = accent; ctx.shadowBlur = 6;
      const pw = Math.min(120, w * 0.12), ph = pw * 0.44;
      ctx.beginPath(); ctx.moveTo(bx, by); ctx.lineTo(bx - pw, by - ph * 0.5); ctx.lineTo(bx - pw, by + ph * 0.32); ctx.lineTo(bx, by + ph * 0.5); ctx.closePath(); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(bx, by); ctx.lineTo(bx + pw, by - ph * 0.5); ctx.lineTo(bx + pw, by + ph * 0.32); ctx.lineTo(bx, by + ph * 0.5); ctx.closePath(); ctx.stroke();
      ctx.globalAlpha = alpha * 0.45; ctx.lineWidth = 1;
      for (let i = 1; i <= 3; i++) { const yy = by - ph * 0.5 + i * ph * 0.2; ctx.beginPath(); ctx.moveTo(bx - pw * 0.8, yy + i * 1.4); ctx.lineTo(bx - pw * 0.12, yy); ctx.stroke(); ctx.beginPath(); ctx.moveTo(bx + pw * 0.12, yy); ctx.lineTo(bx + pw * 0.8, yy + i * 1.4); ctx.stroke(); }
      ctx.restore();
    }
    function frame(nowMs) {
      const now = nowMs / 1000;
      ctx.clearRect(0, 0, w, h);
      drawBook(0.55 + 0.12 * Math.sin(now * 1.1));
      for (const gen of gens) {
        const age = now - gen.birth;
        const g = Math.min(1, age / GROW);
        const inA = Math.min(1, age / 0.5);
        const mature = GROW + HOLD;
        const alpha = age < mature ? inA : Math.max(0, 1 - (age - mature) / FADE);
        gen._g = g; gen._age = age;
        drawTree(gen.tree, g, alpha);
      }
      const newest = gens[gens.length - 1];
      if (newest._g >= SPAWN_AT && gens.length < 2) gens.push({ birth: now, tree: makeTree() });
      gens = gens.filter((gn) => gn._age === undefined || gn._age < GROW + HOLD + FADE);
      if (!gens.length) gens.push({ birth: now, tree: makeTree() });
      raf = requestAnimationFrame(frame);
    }
    if (reduce) { ctx.clearRect(0, 0, w, h); drawBook(0.6); drawTree(gens[0].tree, 1, 0.8); }
    else raf = requestAnimationFrame(frame);
    return () => { cancelAnimationFrame(raf); ro.disconnect(); };
  }, [accent]);
  return <canvas ref={ref} className="roots-canvas" aria-hidden="true" />;
}

Object.assign(window, {
  Ico, useInView, Reveal, RiseText, OrganicTransitionImage, BrandCorner, Counter,
  HeroMedia, useParallax, RotatingWord, RotatingPhrase, RootsBackdrop
});

/* ---------------- RotatingPhrase (cycles whole phrases, wraps + reserves height) ---------------- */
function RotatingPhrase({ phrases, interval = 2800, className = '', style }) {
  const [i, setI] = useState(0);
  const [phase, setPhase] = useState('in');
  useEffect(() => {
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) {
      const id = setInterval(() => setI((p) => (p + 1) % phrases.length), interval + 800);
      return () => clearInterval(id);
    }
    let outT;
    const id = setInterval(() => {
      setPhase('out');
      outT = setTimeout(() => {
        setI((p) => (p + 1) % phrases.length);
        setPhase('in');
      }, 520);
    }, interval);
    return () => { clearInterval(id); clearTimeout(outT); };
  }, [phrases, interval]);

  return (
    <span className={`rot-phrase ${className}`} style={style}>
      {phrases.map((p, idx) => (
        <span key={idx}
          className={`rot-line ${idx === i ? (phase === 'out' ? 'rot-out' : 'rot-in') : 'rot-hidden'}`}>
          {p}
        </span>
      ))}
    </span>
  );
}

/* ---------------- useParallax (mouse) ---------------- */
function useParallax(strength = 18) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;if (!el) return;
    const host = el.closest('header') || el.parentElement || document.body;
    let raf = 0,tx = 0,ty = 0,cx = 0,cy = 0;
    const onMove = (e) => {
      const r = host.getBoundingClientRect();
      tx = ((e.clientX - r.left) / r.width - 0.5) * 2;
      ty = ((e.clientY - r.top) / r.height - 0.5) * 2;
      if (!raf) raf = requestAnimationFrame(loop);
    };
    const loop = () => {
      cx += (tx - cx) * 0.08;cy += (ty - cy) * 0.08;
      el.style.setProperty('--px', (cx * strength).toFixed(2) + 'px');
      el.style.setProperty('--py', (cy * strength).toFixed(2) + 'px');
      if (Math.abs(tx - cx) > 0.001 || Math.abs(ty - cy) > 0.001) raf = requestAnimationFrame(loop);else
      raf = 0;
    };
    host.addEventListener('mousemove', onMove);
    return () => {host.removeEventListener('mousemove', onMove);cancelAnimationFrame(raf);};
  }, [strength]);
  return ref;
}

/* ---------------- HeroMedia (video + animated "invisible science" canvas) ----------------
   - If assets/hero-video.mp4 exists it plays under the canvas; otherwise the still + canvas show.
   - The canvas draws drifting spores, faint links and slow growing tendrils — works with no asset. */
function HeroMedia({ show, poster = 'assets/hero.jpg', video = 'assets/hero-video.mp4' }) {
  const canvasRef = useRef(null);
  const [hasVideo, setHasVideo] = useState(false);

  useEffect(() => {
    const v = document.createElement('video');
    v.muted = true;
    v.addEventListener('loadeddata', () => setHasVideo(true));
    v.addEventListener('error', () => setHasVideo(false));
    v.src = video;
    return () => {v.src = '';};
  }, [video]);

  useEffect(() => {
    const cv = canvasRef.current;if (!cv) return;
    const ctx = cv.getContext('2d');
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    let w = 0,h = 0,dpr = Math.min(2, window.devicePixelRatio || 1),raf = 0,t0 = performance.now();
    const N = 60;
    const dots = Array.from({ length: N }, () => ({
      x: Math.random(), y: Math.random(), r: Math.random() * 1.8 + 0.4,
      vx: (Math.random() - 0.5) * 0.00006, vy: -(Math.random() * 0.00012 + 0.00003),
      ph: Math.random() * 6.28, sp: Math.random() * 0.4 + 0.2
    }));
    const tendrils = Array.from({ length: 5 }, (_, i) => ({
      x: 0.12 + i * 0.19 + (Math.random() - 0.5) * 0.05, len: Math.random() * 0.18 + 0.12,
      sway: Math.random() * 0.02 + 0.01, ph: Math.random() * 6.28, born: Math.random() * 8000
    }));
    function resize() {
      w = cv.clientWidth;h = cv.clientHeight;
      cv.width = w * dpr;cv.height = h * dpr;ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
    resize();
    const ro = new ResizeObserver(resize);ro.observe(cv);

    function frame(now) {
      const t = now - t0;
      ctx.clearRect(0, 0, w, h);
      ctx.lineCap = 'round';
      tendrils.forEach((td) => {
        const prog = Math.max(0, Math.min(1, (t - td.born) / 5000));
        if (prog <= 0) return;
        const baseX = td.x * w,baseY = h + 10;
        const segs = 26;ctx.beginPath();
        for (let s = 0; s <= segs * prog; s++) {
          const u = s / segs;
          const yy = baseY - u * td.len * h * 3.0;
          const xx = baseX + Math.sin(u * 7 + td.ph + t * 0.0004) * td.sway * w * (0.4 + u);
          s === 0 ? ctx.moveTo(xx, yy) : ctx.lineTo(xx, yy);
        }
        ctx.strokeStyle = 'rgba(214,164,94,0.13)';ctx.lineWidth = 1.4;ctx.stroke();
      });
      for (let i = 0; i < N; i++) {
        for (let j = i + 1; j < N; j++) {
          const a = dots[i],b = dots[j];
          const dx = (a.x - b.x) * w,dy = (a.y - b.y) * h;
          const d2 = dx * dx + dy * dy;
          if (d2 < 120 * 120) {
            const al = (1 - Math.sqrt(d2) / 120) * 0.10;
            ctx.strokeStyle = `rgba(157,177,140,${al.toFixed(3)})`;
            ctx.lineWidth = 0.6;ctx.beginPath();
            ctx.moveTo(a.x * w, a.y * h);ctx.lineTo(b.x * w, b.y * h);ctx.stroke();
          }
        }
      }
      dots.forEach((p) => {
        if (!reduce) {
          p.x += p.vx;p.y += p.vy;
          if (p.y < -0.02) {p.y = 1.02;p.x = Math.random();}
          if (p.x < -0.02) p.x = 1.02;if (p.x > 1.02) p.x = -0.02;
        }
        const tw = 0.5 + 0.5 * Math.sin(t * 0.001 * p.sp + p.ph);
        const px = p.x * w,py = p.y * h;
        const g = ctx.createRadialGradient(px, py, 0, px, py, p.r * 6);
        g.addColorStop(0, `rgba(231,200,140,${(0.5 * tw).toFixed(3)})`);
        g.addColorStop(1, 'rgba(231,200,140,0)');
        ctx.fillStyle = g;ctx.beginPath();ctx.arc(px, py, p.r * 6, 0, 7);ctx.fill();
        ctx.fillStyle = `rgba(244,236,210,${(0.7 * tw).toFixed(3)})`;
        ctx.beginPath();ctx.arc(px, py, p.r, 0, 7);ctx.fill();
      });
      if (!reduce) raf = requestAnimationFrame(frame);
    }
    raf = requestAnimationFrame(frame);
    if (reduce) frame(t0 + 3000);
    return () => {cancelAnimationFrame(raf);ro.disconnect();};
  }, []);

  return (
    <div className="hero-media" style={{ opacity: show ? 1 : 0, transition: 'opacity 1.5s cubic-bezier(0.16,1,0.3,1)' }}>
      {hasVideo ?
      <video className="hm-video" src={video} poster={poster} autoPlay muted loop playsInline /> :
      <img className="hm-img" src={poster} alt="" />}
      <canvas ref={canvasRef} className="hm-canvas" aria-hidden="true" />
      <div className="veil" />
    </div>);

}