// ═══════════════════════════════════════════════════════════════
// VAMOS VIENDO — TRAILER DE PAPEL (full-bleed prototype)
// Feed vertical de críticas. La imagen manda.
// Swipe ↑ sobre el cover → entra a la crítica full con fotogramas.
// Swipe ↓ cierra.
// ═══════════════════════════════════════════════════════════════

// ─── Haptics ─────────────────────────────────────────────────────
// Feedback háptico mínimo para gestos deliberados (swipe cover, pull-to-close,
// abrir reader). Dual-path:
//   · Android → navigator.vibrate(ms)
//   · iOS 17.4+ → <input type="checkbox" switch> hack. Si el UA soporta la
//     propiedad `switch`, un .click() programático dentro de un gesture handler
//     dispara un tap háptico del sistema. En iOS < 17.4 es no-op silencioso.
// El helper NO distrae: solo se invoca en 3-4 puntos clave. Si el usuario tiene
// el teléfono en silencio sin vibración, también es no-op.
const Haptics = (function initHaptics() {
  if (typeof document === 'undefined') return { light(){}, medium(){}, soft(){} };
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;

  // Build the iOS switch (only once). Kept off-screen but in the DOM.
  let sw = null;
  try {
    const input = document.createElement('input');
    input.type = 'checkbox';
    input.setAttribute('switch', '');
    // feature-detect: iOS 17.4+ reflects the attribute property. In older UAs
    // it'll still render as a checkbox — click() is harmless either way.
    const label = document.createElement('label');
    Object.assign(label.style, {
      position:'fixed', top:'-9999px', left:'-9999px',
      width:'1px', height:'1px', pointerEvents:'none', opacity:'0',
    });
    label.setAttribute('aria-hidden', 'true');
    label.appendChild(input);
    (document.body || document.documentElement).appendChild(label);
    sw = input;
  } catch (e) { sw = null; }

  const iosTap = () => {
    try { sw && sw.click(); } catch (e) {}
  };
  const vibrate = (ms) => {
    try { navigator.vibrate && navigator.vibrate(ms); } catch (e) {}
  };

  return {
    // Transiciones suaves: swipe horizontal, feedback al cruzar un umbral.
    soft:   () => isIOS ? iosTap() : vibrate(8),
    light:  () => isIOS ? iosTap() : vibrate(12),
    // Acciones de "commit": abrir reader, cerrar reader.
    medium: () => isIOS ? iosTap() : vibrate(22),
  };
})();
window.RintanaHaptics = Haptics;

// Filtro defensivo en runtime: VV no debe incluir notas de Inmenso Bastardo
// (que tienen su sección IB aparte). gen-data.mjs ya filtra en el build, pero
// mantenemos esta capa por si vvFull.jsx quedó con datos viejos. Se cruza
// VV_ORDER con window.CRITICAS[*].series y se reescribe ORDER+FULL in-place.
(function excludeIBFromVamosViendo() {
  if (typeof window === 'undefined') return;
  if (!Array.isArray(window.VV_ORDER) || !window.VV_FULL) return;
  if (!Array.isArray(window.CRITICAS)) return;
  const isIB = new Set(
    window.CRITICAS
      .filter(c => Array.isArray(c.series) && c.series.includes('inmenso-bastardo'))
      .map(c => c.slug)
  );
  if (isIB.size === 0) return;
  const filteredOrder = window.VV_ORDER.filter(slug => !isIB.has(slug));
  if (filteredOrder.length === window.VV_ORDER.length) return;
  const filteredFull = {};
  for (const slug of filteredOrder) {
    if (window.VV_FULL[slug]) filteredFull[slug] = window.VV_FULL[slug];
  }
  window.VV_ORDER = filteredOrder;
  window.VV_FULL = filteredFull;
})();

function TrailerApp({ initialSlug, onSlugChange, onHome } = {}) {
  const ORDER = window.VV_ORDER;
  const DATA = window.VV_FULL;
  const [i, setI] = React.useState(() => {
    if (initialSlug) {
      const k = ORDER.indexOf(initialSlug);
      if (k >= 0) return k;
    }
    const s = localStorage.getItem('vv.i');
    return s ? +s : 0;
  });
  // sync initialSlug → index when prop changes (deep link nav)
  React.useEffect(() => {
    if (initialSlug) {
      const k = ORDER.indexOf(initialSlug);
      if (k >= 0 && k !== i) setI(k);
    }
  }, [initialSlug]);
  // notify parent of current slug
  React.useEffect(() => {
    if (onSlugChange) onSlugChange(ORDER[i]);
  }, [i]);
  const [open, setOpen] = React.useState(false);
  const [transition, setTransition] = React.useState(null); // {fromRect, src}
  const [sharedFlying, setSharedFlying] = React.useState(false); // true while clone is mid-flight
  const [exiting, setExiting] = React.useState(null); // 'up'|'down'|'left'|'right'
  const [dragOffset, setDragOffset] = React.useState({ x: 0, y: 0 });
  const startPt = React.useRef(null);
  const gestureAxis = React.useRef(null); // 'x'|'y'|null (locked after ~10px)

  React.useEffect(() => { localStorage.setItem('vv.i', String(i)); }, [i]);

  const entry = DATA[ORDER[i % ORDER.length]];
  const nextEntry = DATA[ORDER[(i+1) % ORDER.length]];
  const prevEntry = DATA[ORDER[(i-1+ORDER.length) % ORDER.length]];

  // Open the reader with a shared-element transition.
  //
  // Critical insight: the bug users see ("nota tarda, superposiciones, imagen
  // cambia de tamaño raro") comes from the HERO image not being decoded yet
  // when the reader mounts. Sequence that goes wrong:
  //   t=0   user taps → reader mounts → hero <img> starts loading/decoding
  //   t=10  clone flies in — looks great
  //   t=540 clone fades out → hero <img> should be visible but STILL decoding
  //   t=540+ flash: empty hero area; then image pops in at final size → feels
  //          like "resize glitch"
  //
  // Fix: pre-decode the hero src BEFORE starting the animation. If the image
  // is already cached (usually yes — it's the same cover src), decode()
  // resolves in <1 frame. If not, we wait for it. Only then we mount the
  // reader AND start the clone flight — no race.
  const openReader = async () => {
    if (open || exiting || transition) return;
    Haptics.medium(); // "commit": entramos al reader
    const coverImg = document.querySelector('.vv-cover.is-active .vv-cover-img');
    const shell = document.querySelector('.vv-shell');
    if (!coverImg || !shell) { setOpen(true); return; }
    const rect = coverImg.getBoundingClientRect();
    const shellRect = shell.getBoundingClientRect();

    // Pre-decode the hero image so there's no decode flash mid-flight
    try {
      const pre = new Image();
      pre.src = entry.cover;
      await pre.decode().catch(() => {});
    } catch {}

    // ORDER MATTERS — to avoid any frame where the user sees the cover without
    // the clone on top, OR the reader's empty hero slot without the clone:
    //   (a) Set transition FIRST → clone appears at fromRect (over the cover's
    //       own <img>), visually indistinguishable from the cover.
    //   (b) Next frame: mount the reader. Its hero-img is opacity:0 during
    //       transitioning, and the reader slides in instantly (is-transitioning
    //       class skips the slide-up animation). The clone is still on top,
    //       still at the same spot, so no visual jump.
    //   (c) The clone now animates toward destRect.
    setTransition({ fromRect: rect, shellRect, src: entry.cover });
    requestAnimationFrame(() => {
      setSharedFlying(true);
      setOpen(true);
    });
    // Clone animation is 420ms; reveal the real hero-img just before it ends
    setTimeout(() => setSharedFlying(false), 400);
    setTimeout(() => setTransition(null), 700);
  };

  const go = (dir, axis='y') => {
    if (open) return;
    Haptics.light(); // swipe entre covers
    const exitMap = { 'y': dir > 0 ? 'up' : 'down', 'x': dir > 0 ? 'left' : 'right' };
    setExiting(exitMap[axis]);
    setTimeout(() => {
      setI((i + dir + ORDER.length) % ORDER.length);
      setExiting(null);
      setDragOffset({ x: 0, y: 0 });
    }, 340);
  };

  const onStart = (e) => {
    if (open || exiting) return;
    const pt = e.touches ? e.touches[0] : e;
    startPt.current = { x: pt.clientX, y: pt.clientY };
    gestureAxis.current = null;
  };
  const onMove = (e) => {
    if (!startPt.current || open || exiting) return;
    const pt = e.touches ? e.touches[0] : e;
    const dx = pt.clientX - startPt.current.x;
    const dy = pt.clientY - startPt.current.y;

    // Lock axis after 10px of motion
    if (!gestureAxis.current) {
      const adx = Math.abs(dx), ady = Math.abs(dy);
      if (adx > 10 || ady > 10) gestureAxis.current = adx > ady ? 'x' : 'y';
      else return;
    }

    if (gestureAxis.current === 'x') {
      setDragOffset({ x: dx * 0.7, y: 0 });
    } else {
      // Vertical up drag lifts the cover slightly (only up — ↓ stays put)
      setDragOffset({ x: 0, y: dy < 0 ? dy * 0.4 : 0 });
    }
  };
  const onEnd = (e) => {
    if (!startPt.current || open || exiting) { startPt.current = null; return; }
    const pt = (e.changedTouches ? e.changedTouches[0] : e);
    const dx = pt.clientX - startPt.current.x;
    const dy = pt.clientY - startPt.current.y;
    const axis = gestureAxis.current;
    startPt.current = null;
    gestureAxis.current = null;

    // Horizontal: navigate between films
    if (axis === 'x') {
      if (dx < -60) { go(1, 'x'); return; }   // left → next
      if (dx >  60) { go(-1, 'x'); return; }  // right → prev
      setDragOffset({ x: 0, y: 0 });
      return;
    }
    // Vertical UP → open reader (single clear gesture)
    if (axis === 'y') {
      if (dy < -60) { openReader(); setDragOffset({ x: 0, y: 0 }); return; }
      // ignore swipe-down on cover (no "prev" vertically)
      setDragOffset({ x: 0, y: 0 });
      return;
    }
    setDragOffset({ x: 0, y: 0 });
  };

  return (
    <div className="vv-shell">
      {/* Chrome top
          RINTANA + Vamos Viendo como link al home si onHome está disponible.
          Si no, queda como texto plano (defensivo por si TrailerApp se monta fuera
          de RintanaApp). */}
      <header className="vv-top">
        {onHome ? (
          <button
            type="button"
            className="vv-brand vv-brand--link"
            onClick={onHome}
            aria-label="Volver al inicio">
            RINTANA<span className="vv-dot"/>
            <span className="vv-brand-sep">·</span>
            <span className="vv-brand-serie">Vamos Viendo</span>
          </button>
        ) : (
          <div className="vv-brand">
            RINTANA<span className="vv-dot"/>
            <span className="vv-brand-sep">·</span>
            <span className="vv-brand-serie">Vamos Viendo</span>
          </div>
        )}
        <div className="vv-progress">
          {ORDER.map((_, j) => (
            <span key={j} className={`vv-pdot ${j===i?'is-on':''}`}/>
          ))}
        </div>
      </header>

      {/* COVER STACK */}
      <div className="vv-stack"
        onMouseDown={onStart} onMouseMove={onMove} onMouseUp={onEnd} onMouseLeave={onEnd}
        onTouchStart={onStart} onTouchMove={onMove} onTouchEnd={onEnd}>

        <CoverCard entry={entry} exiting={exiting} dragOffset={dragOffset} active onTap={openReader}/>

        {/* Peek of next cover underneath (only when not dragging horizontally) */}
        {!exiting && !dragOffset.x && (
          <div className="vv-peek">
            <div className="vv-peek-label">
              <span className="vv-peek-title">Siguiente · {nextEntry.filmTitle}</span>
            </div>
          </div>
        )}

        {/* CTA bottom */}
        <button className="vv-cta" onClick={openReader}>
          <span className="vv-cta-arrow">↑</span>
          <span>Deslizá para leer</span>
        </button>

        {/* Counter */}
        <div className="vv-counter">
          {String(i+1).padStart(2,'0')}<span className="vv-counter-sep">/</span>{String(ORDER.length).padStart(2,'0')}
        </div>
      </div>

      {/* ARTICLE overlay */}
      {open && (
        <ArticleReader
          entry={entry}
          transitioning={sharedFlying}
          onClose={() => setOpen(false)}
          onPickCritic={(slug) => {
            const k = ORDER.indexOf(slug);
            if (k >= 0) setI(k);
          }}
        />
      )}

      {/* Shared element transition layer (image expands from cover to hero) */}
      {transition && <SharedElementLayer transition={transition} headerH={520}/>}
</div>
  );
}

// Shared-element transition. Uses the FLIP-like approach:
//   - Clone starts at EXACTLY the cover's rect (no scaling at start → no
//     distortion, no "resize weirdness")
//   - Clone animates position + size to the hero's rect
//   - Because cover and hero may have different aspect ratios, we animate
//     width/height directly; object-fit:cover on the <img> handles the
//     crop interpolation smoothly (browser-native).
//   - Once at destination, fade out over 200ms; the real hero-img fades in
//     under it at the same time.
// Imperative so React never re-renders over the inline styles.
function SharedElementLayer({ transition, headerH }) {
  const containerRef = React.useRef(null);
  React.useEffect(() => {
    const container = containerRef.current;
    if (!container || !transition) return;
    const { fromRect, shellRect, src } = transition;

    const destW = shellRect.width;
    const destH = headerH;
    const destLeft = shellRect.left;
    const destTop = shellRect.top;

    const img = document.createElement('img');
    img.src = src;
    img.alt = '';
    Object.assign(img.style, {
      position: 'fixed',
      // Start at the cover's exact rect — no transform, no clip, nothing
      // that could distort the image at frame 0.
      top: fromRect.top + 'px',
      left: fromRect.left + 'px',
      width: fromRect.width + 'px',
      height: fromRect.height + 'px',
      objectFit: 'cover',
      objectPosition: 'center',
      zIndex: '200',
      pointerEvents: 'none',
      opacity: '1',
      willChange: 'top, left, width, height, opacity',
      boxShadow: '0 20px 60px rgba(0,0,0,0.55)',
    });
    container.appendChild(img);

    // Commit starting state
    void img.offsetHeight;

    const DURATION = 420;
    // Wait TWO frames before starting the animation. Frame 1 lets the reader
    // mount below us; frame 2 lets the browser paint that mount. Only then we
    // start moving — the reader's hero slot is always covered by the clone
    // until the clone fades out near the end.
    let rafInner = 0;
    const rafId = requestAnimationFrame(() => {
      rafInner = requestAnimationFrame(() => {
        img.style.transition = [
          `top ${DURATION}ms cubic-bezier(.22,.9,.3,1)`,
          `left ${DURATION}ms cubic-bezier(.22,.9,.3,1)`,
          `width ${DURATION}ms cubic-bezier(.22,.9,.3,1)`,
          `height ${DURATION}ms cubic-bezier(.22,.9,.3,1)`,
          `opacity 200ms ease-out ${DURATION - 40}ms`,
          `box-shadow ${DURATION}ms ease-out`,
        ].join(', ');
        img.style.top = destTop + 'px';
        img.style.left = destLeft + 'px';
        img.style.width = destW + 'px';
        img.style.height = destH + 'px';
        img.style.opacity = '0';
        img.style.boxShadow = '0 0 0 rgba(0,0,0,0)';
      });
    });

    return () => {
      cancelAnimationFrame(rafId);
      cancelAnimationFrame(rafInner);
      if (img.parentElement === container) container.removeChild(img);
    };
  }, [transition, headerH]);

  if (!transition) return null;
  return <div ref={containerRef} aria-hidden="true"/>;
}

function CoverCard({ entry, exiting, active, dragOffset, onTap }) {
  const dx = dragOffset?.x || 0;
  const dy = dragOffset?.y || 0;
  const isDragging = dx !== 0 || dy !== 0;
  // Fade/rotate during horizontal drag for nicer feel
  const rot = isDragging ? dx * 0.015 : 0;
  const op  = isDragging ? Math.max(0.5, 1 - Math.abs(dx)/400 - Math.abs(dy)/500) : 1;
  const style = isDragging ? {
    transform: `translate3d(${dx}px, ${dy}px, 0) rotate(${rot}deg)`,
    opacity: op,
    transition: 'none',
  } : {};
  return (
    <div
      className={`vv-cover ${exiting ? 'exit-'+exiting : ''} ${active?'is-active':''}`}
      style={style}>
      <img src={entry.cover} alt="" className="vv-cover-img" data-shared={`cover-${entry.slug}`}/>
      <div className="vv-cover-grad"/>
      <div className="vv-cover-grain"/>

      {/* Solid black lower panel — text lives here */}
      <div className="vv-cover-panel"/>

      {/* Big year mark — sits in the panel as graphic element */}
      <div className="vv-year-mark">'{String(entry.year).slice(-2)}</div>

      {/* Bottom content */}
      <div className="vv-cover-bottom">
        <div className="vv-meta-line">
          <span>{entry.year}</span>
          <span className="vv-meta-dot">·</span>
          <span>{entry.runtime}MIN</span>
          <span className="vv-meta-dot">·</span>
          <span>{entry.country}</span>
          <span className="vv-rating">★ {entry.rating}/10</span>
        </div>

        <div className="vv-director">DIR. {entry.director.toUpperCase()}</div>

        <h1 className="vv-film-title">{entry.filmTitle}</h1>

        <div className="vv-crit-title">«{entry.title}»</div>

        <p className="vv-excerpt">{entry.excerpt}</p>
      </div>
    </div>
  );
}

function ArticleReader({ entry, onClose, onPickCritic, transitioning }) {
  const scrollRef = React.useRef(null);
  const [scrollY, setScrollY] = React.useState(0);
  const [indexOpen, setIndexOpen] = React.useState(false);
  const [pullY, setPullY] = React.useState(0); // drag distance when pulling down from top
  const dragStart = React.useRef(null);
  const pullYRef = React.useRef(0);
  pullYRef.current = pullY;

  React.useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    const onScroll = () => setScrollY(el.scrollTop);
    el.addEventListener('scroll', onScroll);
    return () => el.removeEventListener('scroll', onScroll);
  }, []);

  // When the user picks another critic from "Seguí leyendo", reset the reader
  // scroll position back to the top — otherwise the reader stays at whatever
  // scrollTop the previous article had.
  React.useEffect(() => {
    const el = scrollRef.current;
    if (el) el.scrollTop = 0;
    setScrollY(0);
    setPullY(0);
  }, [entry.slug]);

  // Pull-to-close: attach manually so we can use {passive:false}
  React.useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;

    const getY = (e) => (e.touches ? e.touches[0].clientY : e.clientY);

    const start = (e) => {
      if (el.scrollTop > 0) return;
      dragStart.current = getY(e);
    };
    // Haptics: un único tick "soft" al cruzar el umbral de cierre (100px).
    // Evita vibrar en cada frame del drag; solo avisa que "soltar = cerrar".
    const crossedRef = { current: false };
    const move = (e) => {
      if (dragStart.current == null) return;
      const dy = getY(e) - dragStart.current;
      if (dy > 0) {
        setPullY(Math.min(dy, 260));
        if (dy > 100 && !crossedRef.current) {
          crossedRef.current = true;
          Haptics.soft();
        } else if (dy <= 100 && crossedRef.current) {
          crossedRef.current = false;
        }
        if (e.cancelable && dy > 6) e.preventDefault();
      } else {
        // user scrolled up past start — cancel pull
        setPullY(0);
        crossedRef.current = false;
        dragStart.current = null;
      }
    };
    const end = () => {
      if (dragStart.current == null) return;
      if (pullYRef.current > 100) {
        Haptics.medium(); // "commit": cerrar reader
        setPullY(320);
        setTimeout(onClose, 180);
      } else {
        setPullY(0);
      }
      crossedRef.current = false;
      dragStart.current = null;
    };

    el.addEventListener('touchstart', start, { passive: true });
    el.addEventListener('touchmove', move, { passive: false });
    el.addEventListener('touchend', end);
    el.addEventListener('touchcancel', end);
    // Desktop testing via mouse drag
    el.addEventListener('mousedown', start);
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', end);

    return () => {
      el.removeEventListener('touchstart', start);
      el.removeEventListener('touchmove', move);
      el.removeEventListener('touchend', end);
      el.removeEventListener('touchcancel', end);
      el.removeEventListener('mousedown', start);
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', end);
    };
  }, [onClose]);

  // header image parallax fade
  const headerH = 520;
  const heroOp = Math.max(0, 1 - scrollY / 400);
  const heroScale = 1 + scrollY * 0.0004;

  const pullProgress = Math.min(pullY / 100, 1);

  return (
    <div
      className={`vv-reader ${transitioning ? 'is-transitioning' : ''}`}
      ref={scrollRef}
      style={{
        transform: pullY ? `translateY(${pullY * 0.55}px)` : undefined,
        transition: pullY === 0 || pullY >= 320 ? 'transform 220ms cubic-bezier(.2,.7,.3,1)' : 'none',
        opacity: pullY >= 320 ? 0 : 1 - pullProgress * 0.2,
        cursor: pullY > 0 ? 'grabbing' : undefined,
      }}
    >
      {/* Pull indicator */}
      {pullY > 8 && (
        <div className="vv-pull-hint" style={{ opacity: pullProgress }}>
          <span>{pullProgress >= 1 ? 'Soltá para cerrar' : '↓ Tirá para cerrar'}</span>
        </div>
      )}

      {/* Top chrome: Index toggle + Close */}
      <div className="vv-reader-chrome">
        <button className="vv-chrome-btn vv-chrome-index" onClick={() => setIndexOpen(true)}>
          <span className="vv-chrome-btn-icon">☰</span>
          <span className="vv-chrome-btn-label">Índice VV</span>
          <span className="vv-chrome-btn-num">{String(window.VV_ORDER.indexOf(entry.slug)+1).padStart(2,'0')}/{String(window.VV_ORDER.length).padStart(2,'0')}</span>
        </button>
        <button className="vv-chrome-btn vv-chrome-close" onClick={onClose}>
          <span>↓</span>
          <span className="vv-chrome-btn-label">Cerrar</span>
        </button>
      </div>

      {/* Index drawer */}
      {indexOpen && <IndexDrawer currentSlug={entry.slug} onPick={(slug) => { setIndexOpen(false); onPickCritic && onPickCritic(slug); }} onClose={() => setIndexOpen(false)}/>}

      {/* Hero */}
      <div className="vv-hero" style={{ height: headerH }}>
        <img src={entry.cover} alt="" className="vv-hero-img"
          style={{
            opacity: transitioning ? 0 : heroOp,
            transform: `scale(${heroScale})`,
            transition: transitioning ? 'none' : 'opacity 220ms ease-out',
          }}/>
        <div className="vv-hero-grad"/>
        <div className="vv-hero-bottom">
          <div className="vv-hero-kicker">VAMOS VIENDO · N° {String(window.VV_ORDER.indexOf(entry.slug)+1).padStart(3,'0')}</div>
          <h1 className="vv-hero-title">{entry.filmTitle}</h1>
          <div className="vv-hero-meta">
            {entry.year} · Dir. {entry.director} · {entry.runtime} min · {entry.country}
          </div>
        </div>
      </div>

      {/* Article body */}
      <article className="vv-article">
        {/* Title of critic */}
        <div className="vv-art-kicker">Sobre {entry.filmTitle}</div>
        <h2 className="vv-art-title">{entry.title}</h2>

        {/* Byline row */}
        <div className="vv-byline">
          <div className="vv-byline-left">
            <div className="vv-byline-name">Nicolás Barak</div>
            <div className="vv-byline-date">{formatPubDate(entry.publishedAt)}<em>{entry.readMin || 8} min lectura</em></div>
          </div>
          <div className="vv-rating-chip">★ {entry.rating}<small>/10</small></div>
        </div>

        {/* Camera spec sheet */}
        <CameraSpec camera={entry.camera}/>

        {/* Blocks */}
        {entry.blocks.map((b, k) => {
          if (b.type === 'p') {
            return <p key={k} className={`vv-p ${k===0?'vv-p-first':''}`}>{b.text}</p>;
          }
          if (b.type === 'pull') {
            return (
              <blockquote key={k} className="vv-pull">
                <span className="vv-pull-quote">"</span>
                {b.text}
              </blockquote>
            );
          }
          if (b.type === 'img') {
            return (
              <figure key={k} className="vv-fig">
                <img src={b.src} alt={b.alt || ''} loading="lazy"/>
              </figure>
            );
          }
          return null;
        })}

        {/* Separador — línea roja, sin VV ni texto
            (antes decía "VV · Seguís el rollo · más Vamos Viendo abajo" — sacado por pedido). */}
        <div className="vv-foot vv-foot--line-only">
          <div className="vv-foot-line"/>
        </div>

        {/* More critics grid */}
        <MoreCritics entry={entry} onPick={onPickCritic}/>
      </article>
    </div>
  );
}

function MoreCritics({ entry, onPick }) {
  const DATA = window.VV_FULL;
  const ORDER = window.VV_ORDER;
  const IB_CAPS = window.IB_CAPITULOS || [];
  const IB_BY_SLUG = {};
  IB_CAPS.forEach(c => { IB_BY_SLUG[c.slug] = c; });
  const CRIT = {};
  (window.CRITICAS || []).forEach(c => { CRIT[c.slug] = c; });

  // Use the precomputed related-notes list (scored by director / tag / series
  // overlap in gen-data.mjs). Fall back to chronological neighbours only if
  // the entry has no related field yet (older data).
  // NOTE: related slugs may belong either to VV_FULL or to IB_CAPITULOS.
  // Keep both — we render them with a cross-section link.
  let relatedSlugs = Array.isArray(entry.related)
    ? entry.related.filter(s => s !== entry.slug && (DATA[s] || IB_BY_SLUG[s]))
    : [];
  if (relatedSlugs.length === 0) {
    const idx = ORDER.indexOf(entry.slug);
    relatedSlugs = [
      ORDER[(idx + 1) % ORDER.length],
      ORDER[(idx - 1 + ORDER.length) % ORDER.length],
      ORDER[(idx + 2) % ORDER.length],
      ORDER[(idx - 2 + ORDER.length) % ORDER.length],
    ].filter(s => s && s !== entry.slug && DATA[s]);
  }
  const shown = relatedSlugs.slice(0, 4);
  if (shown.length === 0) return null;
  return (
    <section className="vv-more">
      <div className="vv-more-head">
        <div className="vv-more-kick">SEGUÍ LEYENDO</div>
        <h3 className="vv-more-title">Notas relacionadas</h3>
        <div className="vv-more-rule"/>
      </div>
      <div className="vv-more-grid">
        {shown.map(slug => {
          // Prefer VV_FULL data; if not there, resolve via IB_CAPITULOS.
          const vv = DATA[slug];
          const ib = IB_BY_SLUG[slug];
          const c = CRIT[slug] || {};
          const film = c.film || {};
          const e = vv ? vv : (ib ? {
            poster: ib.poster || ib.cover,
            cover: ib.cover,
            year: ib.year,
            filmTitle: ib.filmTitle,
            title: ib.title,
            director: Array.isArray(ib.director) ? ib.director[0] : ib.director,
            rating: ib.rating,
          } : null);
          if (!e) return null;
          const isIB = !!ib;
          const href = isIB ? `#/ib/${slug}` : `#/vv/${slug}`;
          const handleClick = (ev) => {
            if (isIB) {
              // Let the hash change — RintanaApp listens and switches section.
              return;
            }
            ev.preventDefault();
            onPick && onPick(slug);
          };
          // Prefer the vertical poster; fall back to the runtime poster map
          // (seeded by IBSection) and finally to the featured image/cover.
          const posterMap = window.__RT_POSTER_MAP__ || {};
          const imgSrc = e.poster || (c && c.poster) || posterMap[slug] || e.cover;
          return (
            <a key={slug}
               className="vv-more-cell"
               href={href}
               onClick={handleClick}
               style={{ textDecoration:'none', color:'inherit' }}>
              <div className="vv-more-cover vv-more-cover--poster">
                <img src={imgSrc} alt="" loading="lazy"/>
                <div className="vv-more-year">{e.year}</div>
              </div>
              <div className="vv-more-film">{e.filmTitle}</div>
              <div className="vv-more-crit">«{e.title}»</div>
              <div className="vv-more-meta">
                <span>Dir. {e.director}</span>
                <span className="vv-more-rating">{isIB ? 'IB' : `★ ${e.rating}`}</span>
              </div>
            </a>
          );
        })}
      </div>
    </section>
  );
}

function IndexDrawer({ currentSlug, onPick, onClose }) {
  const ORDER = window.VV_ORDER;
  const DATA = window.VV_FULL;
  // Sort reverse-chrono by publishedAt for fanzine TOC feel
  const sorted = [...ORDER].sort((a,b) => {
    const da = DATA[a].publishedAt || '';
    const db = DATA[b].publishedAt || '';
    return db.localeCompare(da);
  });

  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  return (
    <div className="vv-drawer-scrim" onClick={onClose}>
      <div className="vv-drawer" onClick={(e) => e.stopPropagation()}>
        <div className="vv-drawer-head">
          <div>
            <div className="vv-drawer-kick">ÍNDICE · VAMOS VIENDO</div>
            <h3 className="vv-drawer-title">Todas las críticas</h3>
          </div>
          <button className="vv-drawer-x" onClick={onClose} aria-label="Cerrar índice">✕</button>
        </div>
        <div className="vv-drawer-count">{ORDER.length} críticas · ordenadas por fecha de publicación</div>
        <ol className="vv-drawer-list">
          {sorted.map((slug, k) => {
            const e = DATA[slug];
            const isCurrent = slug === currentSlug;
            const orderNum = ORDER.indexOf(slug) + 1;
            return (
              <li key={slug} className={`vv-drawer-item ${isCurrent?'is-current':''}`}>
                <button onClick={() => !isCurrent && onPick(slug)} disabled={isCurrent}>
                  <span className="vv-drawer-num">N°{String(orderNum).padStart(3,'0')}</span>
                  <span className="vv-drawer-body">
                    <span className="vv-drawer-film">{e.filmTitle} <em>· {e.year}</em></span>
                    <span className="vv-drawer-crit">«{e.title}»</span>
                    <span className="vv-drawer-meta">{formatPubDate(e.publishedAt)} · {e.readMin} min</span>
                  </span>
                  <span className="vv-drawer-chev">{isCurrent ? 'LEYENDO' : '→'}</span>
                </button>
              </li>
            );
          })}
        </ol>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════
// CAMERA SPEC — "SHOT ON" stamp, shown in the expanded reader
// ═══════════════════════════════════════════════════════════════
function CameraSpec({ camera }) {
  if (!camera) return null;
  return (
    <div className="vv-cam vv-cam-stamp">
      <div className="vv-cam-stamp-inner">
        <div className="vv-cam-stamp-kick">SHOT ON</div>
        <div className="vv-cam-stamp-main">{camera.format}</div>
        <div className="vv-cam-stamp-sub">{camera.camera}</div>
      </div>
    </div>
  );
}

window.TrailerApp = TrailerApp;

// ═══════════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════════
function formatPubDate(iso) {
  if (!iso) return 'Publicado';
  const months = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'];
  const [y,m,d] = iso.split('-').map(n => parseInt(n,10));
  if (!y || !m || !d) return 'Publicado';
  return `Publicado el ${d} de ${months[m-1]} de ${y}`;
}
window.formatPubDate = formatPubDate;
