// Nick Connelly — "Soul Winning" gospel presentation feed.
// Unlisted page (#nickconnelly). Same TikTok feed design as the main feed,
// with an EN/ES toggle that swaps the "Watch the full message" target.
// 20 short 9:16 clips; full message in EN/ES, 16:9 + 9:16.

const NICK_BASE = "https://pub-28a96ef5d275416ea27ae14f6e84c132.r2.dev/official/Personalshare/nickconnelly";

const NICK_CLIPS = [
  ["01 intro-question-believe-100-trust", "Intro · Believe 100%, Trust", "Intro · Cree 100%, Confía"],
  ["02 trust-alone-99-vs-1-percent", "Trust Alone · 99 vs 1 Percent", "Solo Confianza · 99 vs 1 Por Ciento"],
  ["03 dead-in-vain-god-loves-you-not-works", "Dead In Vain · God Loves You, Not Works", "Muerto en Vano · Dios Te Ama, No las Obras"],
  ["04 all-sinned-wages-death-there-is-a-hell", "All Sinned · The Wages of Death", "Todos Pecaron · La Paga de la Muerte"],
  ["05 lake-of-fire-one-lie-christ-died", "Lake of Fire · Christ Died", "Lago de Fuego · Cristo Murió"],
  ["06 all-sins-forgiven-jesus-is-god", "All Sins Forgiven · Jesus Is God", "Pecados Perdonados · Jesús es Dios"],
  ["07 trinity-the-word-was-god", "Trinity · The Word Was God", "Trinidad · El Verbo Era Dios"],
  ["08 sinless-life-the-crucifixion", "Sinless Life · The Crucifixion", "Vida Sin Pecado · La Crucifixión"],
  ["09 resurrection-handle-me", "Resurrection · Handle Me", "Resurrección · Tócame"],
  ["10 100-percent-trust-john-3-16", "100% Trust · John 3:16", "100% Confianza · Juan 3:16"],
  ["11 free-gift-grace-not-works", "Free Gift · Grace, Not Works", "Regalo Gratuito · Gracia, No Obras"],
  ["12 god-cannot-lie-water-of-life", "God Cannot Lie · Water of Life", "Dios No Puede Mentir · Agua de Vida"],
  ["13 eternal-life-forever-cant-lose-it", "Eternal Life · Can't Lose It", "Vida Eterna · No Se Puede Perder"],
  ["14 blood-cleanses-once-a-son", "Blood Cleanses · Once a Son", "La Sangre Limpia · Una Vez Hijo"],
  ["15 always-a-son-nothing-separates", "Always a Son · Nothing Separates", "Siempre un Hijo · Nada Separa"],
  ["16 psalm-89-rod-never-break-covenant", "Psalm 89 · Never Break Covenant", "Salmo 89 · Nunca Romper el Pacto"],
  ["17 recap-one-thing-100-percent-trust", "Recap · 100% Trust", "Resumen · 100% Confianza"],
  ["18 never-perish-none-pluck-them-out", "Never Perish · None Pluck Them Out", "Nunca Perecerán · Nadie los Arrebata"],
  ["19 the-decision-romans-10-9", "The Decision · Romans 10:9", "La Decisión · Romanos 10:9"],
  ["20 the-prayer-and-assurance", "The Prayer and Assurance", "La Oración y la Seguridad"],
];

// UI strings per language.
const NICK_T = {
  EN: { title: "Soul Winning", shortClip: "Short Clip", watchFull: "Watch full message", subtitle: "Gospel Presentation · Nick Connelly",
    fullHeading: "Watch the full message", fullNote: "The complete talk — plays right here. Rotate to widescreen, or pick a format.",
    widescreen: "Widescreen 16:9", vertical: "Vertical 9:16", fullscreen: "Fullscreen",
    dlHeading: "Download — clean versions", dlNote: "Unwatermarked masters (no end-card). HD masters are large — use Web for quick sharing.",
    english: "English", spanish: "Spanish", hdMaster: "HD master", web: "Web", preparing: "Preparing" },
  ES: { title: "Ganando Almas", shortClip: "Clip Corto", watchFull: "Ver el mensaje completo", subtitle: "Presentación del Evangelio · Nick Connelly",
    fullHeading: "Ver el mensaje completo", fullNote: "La charla completa — se reproduce aquí. Gira a horizontal o elige un formato.",
    widescreen: "Panorámico 16:9", vertical: "Vertical 9:16", fullscreen: "Pantalla completa",
    dlHeading: "Descargar — versiones limpias", dlNote: "Originales sin marca de agua (sin tarjeta final). Los originales HD son grandes — usa Web para compartir rápido.",
    english: "Inglés", spanish: "Español", hdMaster: "Original HD", web: "Web", preparing: "Preparando" },
};

function nickFull(lang, orient) {
  // e.g. full/Soul Winning EN 9x16 (compressed).mp4
  return encodeURI(`${NICK_BASE}/full/Soul Winning ${lang} ${orient} (compressed).mp4`);
}
function nickMaster(lang, orient) {
  return encodeURI(`${NICK_BASE}/full/Soul Winning ${lang} ${orient} (master H264 CRF16).mp4`);
}

// Full-message + downloads sheet
function NickFilesSheet({ onClose, landscape }) {
  const [lang, setLang] = useState("EN");
  const [watch, setWatch] = useState(null); // {orient}
  const [dl, setDl] = useState(null); // { href, pct }
  const tt = NICK_T[lang] || NICK_T.EN;
  const fullVidRef = useRef(null);
  // Cross-origin force-download. The bucket has CORS, so we can stream the file
  // into a blob and save it with the right filename (the `download` attribute
  // alone is ignored cross-origin, which is why links were opening inline).
  async function downloadFile(href) {
    const filename = decodeURIComponent(href.split("/").pop());
    if (dl) return; // one at a time
    try {
      setDl({ href, pct: 0 });
      const res = await fetch(href);
      if (!res.ok) throw new Error("HTTP " + res.status);
      const total = +(res.headers.get("content-length") || 0);
      const reader = res.body.getReader();
      const chunks = [];
      let received = 0;
      for (;;) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(value);
        received += value.length;
        if (total) setDl({ href, pct: Math.round((received / total) * 100) });
      }
      const url = URL.createObjectURL(new Blob(chunks, { type: "video/mp4" }));
      const a = document.createElement("a");
      a.href = url; a.download = filename;
      document.body.appendChild(a); a.click(); a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 15000);
      setDl(null);
    } catch (e) {
      setDl(null);
      window.open(href, "_blank", "noopener"); // fallback: open so user can save manually
    }
  }
  // Opened in landscape → auto-load + play the widescreen (16:9) full message.
  useEffect(() => {
    if (!landscape) return;
    setWatch("16x9");
    const v = fullVidRef.current;
    if (!v) return;
    document.querySelectorAll("video").forEach((other) => {
      if (other !== v) { try { other.pause(); other.muted = true; } catch {} }
    });
    v.src = nickFull(lang, "16x9");
    v.muted = false;
    const pr = v.play();
    if (pr && pr.catch) pr.catch(() => {});
  }, []);
  // When language changes while a format is open, reload that format in the new language.
  useEffect(() => {
    const v = fullVidRef.current;
    if (!v || !watch) return;
    const t = v.currentTime;
    v.src = nickFull(lang, watch);
    v.muted = false;
    const pr = v.play();
    if (pr && pr.catch) pr.catch(() => {});
  }, [lang]);
  const goFs = () => {
    const v = fullVidRef.current; if (!v) return;
    const el = v.closest('[data-fsroot]') || v;
    if (v.webkitEnterFullscreen) { try { v.webkitEnterFullscreen(); return; } catch {} }
    if (el.requestFullscreen) { el.requestFullscreen().catch(() => {}); }
    else if (v.requestFullscreen) { v.requestFullscreen().catch(() => {}); }
  };
  // Set src + play + fullscreen ALL synchronously inside the click gesture —
  // browsers block requestFullscreen if it's deferred outside the gesture.
  const pickFormat = (orient) => {
    setWatch(orient);
    const v = fullVidRef.current;
    if (!v) return;
    // Stop every other video (the feed clips) so only the full message plays.
    document.querySelectorAll("video").forEach((other) => {
      if (other !== v) { try { other.pause(); other.muted = true; } catch {} }
    });
    v.src = nickFull(lang, orient);
    v.muted = false;
    // iOS: webkitEnterFullscreen only works once metadata is loaded. Try now,
    // and if the video isn't ready yet, retry on loadedmetadata.
    const tryFs = () => {
      if (v.webkitEnterFullscreen) {
        if (v.readyState >= 1) { try { v.webkitEnterFullscreen(); return true; } catch {} }
        return false;
      }
      const el = v.closest('[data-fsroot]') || v;
      if (el.requestFullscreen) { el.requestFullscreen().catch(() => {}); return true; }
      if (v.requestFullscreen) { v.requestFullscreen().catch(() => {}); return true; }
      return false;
    };
    const pr = v.play();
    if (pr && pr.catch) pr.catch(() => {});
    if (!tryFs()) {
      const onMeta = () => { v.removeEventListener("loadedmetadata", onMeta); try { v.webkitEnterFullscreen && v.webkitEnterFullscreen(); } catch {} };
      v.addEventListener("loadedmetadata", onMeta);
    }
  };
  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 110, background: "rgba(10,9,7,0.88)", backdropFilter: "blur(10px)", display: "flex", alignItems: "center", justifyContent: "center", padding: 18, overflowY: "auto" }}>
      <div onClick={(e) => e.stopPropagation()} style={{ position: "relative", width: "min(720px, 100%)", maxHeight: "90vh", overflowY: "auto", background: T.bgRise, border: `1px solid ${T.gold}44`, padding: "32px 28px 26px" }}>
        <button onClick={onClose} aria-label="Close" style={{ position: "absolute", top: 14, right: 14, width: 36, height: 36, background: "transparent", border: `1px solid ${T.gold}33`, color: T.gold, cursor: "pointer" }}>✕</button>
        <SmallCaps color={T.gold} letterSpacing="0.3em">{tt.title} · {tt.fullHeading}</SmallCaps>

        {/* EN/ES toggle */}
        <div style={{ marginTop: 18, display: "flex", gap: 0, width: 140, border: `1px solid ${T.gold}44` }}>
          {["EN", "ES"].map(l => (
            <button key={l} onClick={() => setLang(l)} style={{ flex: 1, padding: "10px 0", background: lang === l ? T.gold : "transparent", color: lang === l ? T.ink : T.paper, border: "none", cursor: "pointer", fontFamily: "'Cinzel', serif", fontSize: 12, letterSpacing: "0.16em", fontWeight: lang === l ? 700 : 500 }}>{l}</button>
          ))}
        </div>

        {/* Watch full */}
        <div style={{ marginTop: 24 }}>
          <h3 style={{ fontFamily: "'Cinzel', serif", fontSize: 22, fontWeight: 500, color: T.paper, margin: "0 0 6px", letterSpacing: "0.02em" }}>{tt.fullHeading}</h3>
          <p style={{ fontSize: 15, color: T.paperDim, fontStyle: "italic", fontFamily: "'Cormorant Garamond', serif", margin: "0 0 14px" }}>{tt.fullNote}</p>
          <div data-fsroot style={{ position: "relative", marginBottom: 14, display: watch ? "block" : "none" }}>
            <video ref={fullVidRef} controls playsInline
              style={{ width: "100%", maxHeight: "50vh", background: "#000", border: `1px solid ${T.gold}33`, aspectRatio: watch === "16x9" ? "16/9" : "9/16", objectFit: "contain", display: "block" }} />
            <button onClick={goFs}
              aria-label="Fullscreen"
              style={{ position: "absolute", top: 10, right: 10, padding: "8px 12px", background: "rgba(0,0,0,0.6)", color: T.paper, border: `1px solid ${T.gold}55`, cursor: "pointer", fontFamily: "'Cinzel', serif", fontSize: 10, letterSpacing: "0.16em", textTransform: "uppercase", backdropFilter: "blur(8px)" }}>
              ⛶ Fullscreen
            </button>
          </div>
          <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
            <button onClick={() => pickFormat("16x9")} style={btnGold(watch === "16x9")}>▶ {tt.widescreen}</button>
            <button onClick={() => pickFormat("9x16")} style={btnGold(watch === "9x16")}>▶ {tt.vertical}</button>
          </div>
        </div>

        {/* Downloads */}
        <div style={{ marginTop: 28, paddingTop: 22, borderTop: `1px solid ${T.gold}22` }}>
          <h3 style={{ fontFamily: "'Cinzel', serif", fontSize: 20, fontWeight: 500, color: T.paper, margin: "0 0 4px", letterSpacing: "0.02em" }}>{tt.dlHeading}</h3>
          <p style={{ fontSize: 14, color: T.paperFaint, fontStyle: "italic", fontFamily: "'Cormorant Garamond', serif", margin: "0 0 16px" }}>{tt.dlNote}</p>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
            {[
              { label: `${tt.widescreen} · ${tt.hdMaster}`, size: "2.4 GB", href: nickMaster(lang, "16x9") },
              { label: `${tt.widescreen} · ${tt.web}`, size: lang === "EN" ? "560 MB" : "563 MB", href: nickFull(lang, "16x9") },
              { label: `${tt.vertical} · ${tt.hdMaster}`, size: "1.7 GB", href: nickMaster(lang, "9x16") },
              { label: `${tt.vertical} · ${tt.web}`, size: lang === "EN" ? "559 MB" : "563 MB", href: nickFull(lang, "9x16") },
            ].map((d) => {
              const active = dl && dl.href === d.href;
              const busy = !!dl;
              return (
              <button key={d.label} onClick={() => downloadFile(d.href)} disabled={busy}
                style={{ position: "relative", overflow: "hidden", padding: "14px 16px", background: T.bg, border: `1px solid ${T.gold}33`, textAlign: "left", cursor: busy ? "default" : "pointer", opacity: busy && !active ? 0.5 : 1, display: "flex", flexDirection: "column", gap: 4 }}>
                {active && <span aria-hidden style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: dl.pct + "%", background: `${T.gold}22`, transition: "width 0.2s", pointerEvents: "none" }} />}
                <span style={{ position: "relative", fontFamily: "'Cinzel', serif", fontSize: 13, color: T.paper, letterSpacing: "0.02em" }}>{d.label}</span>
                <span style={{ position: "relative", fontFamily: "ui-monospace, monospace", fontSize: 11, color: T.gold, letterSpacing: "0.08em" }}>
                  {active ? `${tt.preparing || "Preparing"}… ${dl.pct}%` : `${d.size} ↓`}
                </span>
              </button>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
}
function btnGold(active) {
  return { padding: "12px 18px", background: active ? T.gold : "transparent", color: active ? T.ink : T.gold, border: `1px solid ${T.gold}66`, cursor: "pointer", fontFamily: "'Cinzel', serif", fontSize: 11, letterSpacing: "0.18em", textTransform: "uppercase", fontWeight: active ? 700 : 500 };
}

function NickConnellyFeed({ nav, params }) {
  const [lang, setLang] = useState("EN");
  const [muted, setMuted] = useState(true);
  const [activeIdx, setActiveIdx] = useState(0);
  const [filesOpen, setFilesOpen] = useState(false);
  const [landscape, setLandscape] = useState(false);
  const [autoplay, setAutoplay] = useState(() => {
    try { return localStorage.getItem("nickAutoplay") !== "off"; } catch { return true; }
  });
  useEffect(() => { try { localStorage.setItem("nickAutoplay", autoplay ? "on" : "off"); } catch {} }, [autoplay]);
  const containerRef = useRef(null);
  const p = usePlayer();
  // Track phone/tablet landscape so we can give the 16:9 video a clean takeover.
  useEffect(() => {
    const forced = new URLSearchParams(location.search).get("land") === "1";
    const isTouch = (navigator.maxTouchPoints > 0) || ("ontouchstart" in window);
    const mq = window.matchMedia("(orientation: landscape)");
    const on = () => setLandscape(forced || (mq.matches && isTouch));
    on();
    if (mq.addEventListener) mq.addEventListener("change", on); else mq.addListener(on);
    window.addEventListener("resize", on);
    window.addEventListener("orientationchange", on);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener("change", on); else mq.removeListener(on);
      window.removeEventListener("resize", on);
      window.removeEventListener("orientationchange", on);
    };
  }, []);

  // Pause sticky audio on mount
  useEffect(() => { if (p.playing) p.toggle(); }, []);

  const list = useMemo(() => NICK_CLIPS.map(([slug, titleEN, titleES], i) => ({
    id: "nick-" + (i + 1),
    n: i + 1,
    shortsEN: encodeURI(`${NICK_BASE}/clips/SoulWinning 9x16 ${slug}.mp4`),
    shortsES: encodeURI(`${NICK_BASE}/clips/SoulWinning ES 9x16 ${slug}.mp4`),
    shorts: encodeURI(`${NICK_BASE}/clips/SoulWinning ${lang === "ES" ? "ES " : ""}9x16 ${slug}.mp4`),
    wide: encodeURI(`${NICK_BASE}/clips/SoulWinning ${lang === "ES" ? "ES " : ""}16x9 ${slug}.mp4`),
    full: nickFull(lang, "9x16"),
    fullWide: nickFull(lang, "16x9"),
    title: lang === "ES" ? titleES : titleEN,
    collectionTitle: "Soul Winning · Nick Connelly",
  })), [lang]);
  const tt = NICK_T[lang] || NICK_T.EN;

  // Detect visible slide
  useEffect(() => {
    const c = containerRef.current;
    if (!c) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting && entry.intersectionRatio > 0.6) {
          setActiveIdx(Number(entry.target.dataset.idx));
        }
      });
    }, { root: c, threshold: [0.6] });
    Array.from(c.children).forEach((child) => io.observe(child));
    return () => io.disconnect();
  }, [list.length]);

  // Keyboard nav
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "ArrowDown" || e.key === "j") { e.preventDefault(); jump(1); }
      else if (e.key === "ArrowUp" || e.key === "k") { e.preventDefault(); jump(-1); }
      else if (e.key === "m") setMuted(m => !m);
      else if (e.key === "Escape") window.history.back();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [activeIdx, list.length]);

  function jump(delta) {
    const c = containerRef.current;
    if (!c) return;
    const ni = Math.max(0, Math.min(list.length - 1, activeIdx + delta));
    if (ni === activeIdx) return;
    // Set the active index directly (the IntersectionObserver is unreliable while
    // the active video is position:fixed in landscape) and scroll to match.
    setActiveIdx(ni);
    c.scrollTo({ top: ni * c.clientHeight, behavior: "smooth" });
  }

  // Autoplay advance — wraps to the first clip after the last for a continuous cycle.
  function advanceAuto() {
    const c = containerRef.current;
    const ni = activeIdx + 1 >= list.length ? 0 : activeIdx + 1;
    setActiveIdx(ni);
    if (c) c.scrollTo({ top: ni * c.clientHeight, behavior: "smooth" });
  }

  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 100, background: "#000", overflow: "hidden" }}>
      <div ref={containerRef} style={{ height: "100dvh", overflowY: "scroll", scrollSnapType: "y mandatory", WebkitOverflowScrolling: "touch", scrollbarWidth: "none" }}>
        {list.map((v, i) => (
          <NickSlide key={v.id} video={v} idx={i} active={i === activeIdx} near={Math.abs(i - activeIdx) <= 1} muted={muted} lang={lang} tt={tt} landscape={landscape} onFlip={jump} autoplay={autoplay} onAdvance={advanceAuto} />
        ))}
      </div>

      {/* Top bar */}
      <div style={{ position: landscape ? "fixed" : "absolute", top: 0, left: 0, right: 0, zIndex: 70, padding: landscape ? "8px 14px" : "calc(26px + env(safe-area-inset-top)) 14px 14px", display: "flex", justifyContent: "space-between", alignItems: "center", background: "linear-gradient(180deg, rgba(0,0,0,0.6), transparent)", pointerEvents: "none" }}>
        <button onClick={() => window.history.back()} aria-label="Close" style={{ width: 38, height: 38, background: "rgba(0,0,0,0.4)", border: `1px solid ${T.gold}44`, color: T.paper, cursor: "pointer", backdropFilter: "blur(8px)", pointerEvents: "auto", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
          <svg width="14" height="14" viewBox="0 0 14 14" stroke="currentColor" strokeWidth="2" fill="none"><line x1="3" y1="3" x2="11" y2="11" /><line x1="11" y1="3" x2="3" y2="11" /></svg>
        </button>
        <div style={{ display: "flex", gap: 8, alignItems: "center", pointerEvents: "auto" }}>
          {/* In landscape the EN/ES toggle + counter live up here so the captions stay clear at the bottom. */}
          {landscape && (
            <>
              <button onClick={() => setAutoplay(a => !a)} aria-label="Autoplay" title="Autoplay next" style={{ display: "flex", alignItems: "center", gap: 5, padding: "5px 10px", height: 38, background: autoplay ? T.gold : "rgba(0,0,0,0.4)", color: autoplay ? T.ink : T.paper, border: `1px solid ${T.gold}55`, cursor: "pointer", backdropFilter: "blur(8px)", fontFamily: "'Cinzel', serif", fontSize: 9, letterSpacing: "0.16em", fontWeight: autoplay ? 700 : 500 }}>
                <svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor"><path d="M3 2l11 6-11 6V2z" /></svg>AUTO
              </button>
              <SmallCaps color={T.paperDim} letterSpacing="0.22em" size={9}>{activeIdx + 1}/{list.length}</SmallCaps>
              <div style={{ display: "flex", border: `1px solid ${T.gold}55`, background: "rgba(0,0,0,0.5)", backdropFilter: "blur(8px)" }}>
                {["EN", "ES"].map(l => (
                  <button key={l} onClick={() => setLang(l)} style={{ padding: "5px 11px", background: lang === l ? T.gold : "transparent", color: lang === l ? T.ink : T.paper, border: "none", cursor: "pointer", fontFamily: "'Cinzel', serif", fontSize: 10, letterSpacing: "0.14em", fontWeight: lang === l ? 700 : 500 }}>{l}</button>
                ))}
              </div>
            </>
          )}
          <button onClick={() => setMuted(m => !m)} aria-label="Mute" style={{ width: 38, height: 38, background: "rgba(0,0,0,0.4)", border: `1px solid ${T.gold}44`, color: T.paper, cursor: "pointer", backdropFilter: "blur(8px)", display: "flex", alignItems: "center", justifyContent: "center" }}>
            {muted ? (
              <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M3 6h2l3-3v10l-3-3H3V6z" /><path d="M11 6l3 3M14 6l-3 3" stroke="currentColor" strokeWidth="1.4" fill="none" /></svg>
            ) : (
              <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M3 6h2l3-3v10l-3-3H3V6z" /><path d="M11 5a3 3 0 010 6" stroke="currentColor" strokeWidth="1.4" fill="none" /></svg>
            )}
          </button>
          <button onClick={() => setFilesOpen(true)} aria-label="Full message and files" style={{ width: 38, height: 38, background: "rgba(0,0,0,0.4)", border: `1px solid ${T.gold}44`, color: T.gold, cursor: "pointer", backdropFilter: "blur(8px)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 16 }}>↓</button>
        </div>
      </div>

      {/* Footer — portrait only (brand + EN/ES toggle + counter). Hidden in landscape so it never covers the burned-in captions. */}
      {!landscape && (
      <div style={{ position: "absolute", bottom: 0, left: 0, right: 0, zIndex: 70, padding: "10px 14px calc(10px + env(safe-area-inset-bottom))", display: "flex", alignItems: "center", justifyContent: "center", gap: 10, background: "linear-gradient(0deg, rgba(0,0,0,0.6), transparent)", pointerEvents: "none" }}>
        <button onClick={() => setAutoplay(a => !a)} aria-label="Autoplay" title="Autoplay next" style={{ display: "flex", alignItems: "center", gap: 5, padding: "5px 11px", background: autoplay ? T.gold : "transparent", color: autoplay ? T.ink : T.paper, border: `1px solid ${T.gold}55`, cursor: "pointer", backdropFilter: "blur(8px)", pointerEvents: "auto", fontFamily: "'Cinzel', serif", fontSize: 9, letterSpacing: "0.16em", fontWeight: autoplay ? 700 : 500 }}>
          <svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor"><path d="M3 2l11 6-11 6V2z" /></svg>AUTO
        </button>
        <div style={{ display: "flex", border: `1px solid ${T.gold}55`, background: "rgba(0,0,0,0.5)", backdropFilter: "blur(8px)", pointerEvents: "auto" }}>
          {["EN", "ES"].map(l => (
            <button key={l} onClick={() => setLang(l)} style={{ padding: "5px 13px", background: lang === l ? T.gold : "transparent", color: lang === l ? T.ink : T.paper, border: "none", cursor: "pointer", fontFamily: "'Cinzel', serif", fontSize: 10, letterSpacing: "0.14em", fontWeight: lang === l ? 700 : 500 }}>{l}</button>
          ))}
        </div>
        <SmallCaps color={T.paperDim} letterSpacing="0.22em" size={9}>{activeIdx + 1}/{list.length}</SmallCaps>
      </div>
      )}
      {/* Landscape prev/next — the fullscreen fixed video swallows swipes, so
          give explicit chevrons (vertically centered on each edge). */}
      {landscape && (
        <>
          <button onClick={() => jump(-1)} disabled={activeIdx === 0} aria-label="Previous clip"
            style={{ position: "fixed", left: 8, top: "50%", transform: "translateY(-50%)", width: 44, height: 44, zIndex: 71, background: "rgba(0,0,0,0.45)", border: `1px solid ${T.gold}55`, color: activeIdx === 0 ? `${T.paper}44` : T.paper, cursor: activeIdx === 0 ? "default" : "pointer", backdropFilter: "blur(8px)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 20, lineHeight: 1 }}>‹</button>
          <button onClick={() => jump(1)} disabled={activeIdx === list.length - 1} aria-label="Next clip"
            style={{ position: "fixed", right: 8, top: "50%", transform: "translateY(-50%)", width: 44, height: 44, zIndex: 71, background: "rgba(0,0,0,0.45)", border: `1px solid ${T.gold}55`, color: activeIdx === list.length - 1 ? `${T.paper}44` : T.paper, cursor: activeIdx === list.length - 1 ? "default" : "pointer", backdropFilter: "blur(8px)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 20, lineHeight: 1 }}>›</button>
        </>
      )}
      {filesOpen && <NickFilesSheet onClose={() => setFilesOpen(false)} landscape={landscape} />}
    </div>
  );
}

function NickSlide({ video, idx, active, near, muted, tt, landscape, onFlip, autoplay, onAdvance }) {
  const videoRef = useRef(null);
  const seekRef = useRef(0);   // timestamp to restore after a src swap
  // Rotate to landscape (phone/tablet) → swap to the 16:9 clip at the same
  // timestamp; rotate back → resume the 9:16 clip where it left off.
  const [wide, setWide] = useState(false);
  useEffect(() => {
    const forced = new URLSearchParams(location.search).get("land") === "1";
    const isTouch = (navigator.maxTouchPoints > 0) || ("ontouchstart" in window);
    const mq = window.matchMedia("(orientation: landscape)");
    const onChange = () => {
      const v = videoRef.current;
      seekRef.current = v ? v.currentTime : 0;  // restored on loadedmetadata
      setWide(forced || (mq.matches && isTouch));
    };
    onChange();
    if (mq.addEventListener) mq.addEventListener("change", onChange); else mq.addListener(onChange);
    window.addEventListener("orientationchange", onChange);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener("change", onChange); else mq.removeListener(onChange);
      window.removeEventListener("orientationchange", onChange);
    };
  }, [active]);
  const src = wide ? video.wide : video.shorts;
  // Apply the pending seek once the (possibly newly-swapped) source is ready.
  const onMeta = () => {
    const v = videoRef.current;
    if (!v) return;
    const t = seekRef.current;
    if (t > 0 && t < (v.duration || Infinity)) { try { v.currentTime = t; } catch {} }
    if (active) v.play().catch(() => {});
  };
  useEffect(() => {
    const v = videoRef.current;
    if (!v) return;
    v.muted = muted;
    if (active) { v.play().catch(() => {}); }
    else v.pause();
  }, [active, muted, near, src]);

  const onTap = () => {
    const v = videoRef.current;
    if (!v) return;
    if (v.paused) v.play().catch(() => {}); else v.pause();
  };

  // Swipe to flip clips in landscape (the fixed fullscreen video eats scroll).
  const touch = useRef(null);
  const onTouchStart = (e) => { const t = e.touches[0]; touch.current = { x: t.clientX, y: t.clientY, moved: false }; };
  const onTouchMove = (e) => { if (touch.current) touch.current.moved = true; };
  const onTouchEnd = (e) => {
    if (!landscape || !touch.current) { touch.current = null; return; }
    const t = e.changedTouches[0];
    const dx = t.clientX - touch.current.x, dy = t.clientY - touch.current.y;
    touch.current = null;
    // Horizontal OR vertical swipe past threshold flips; otherwise it's a tap.
    if (Math.max(Math.abs(dx), Math.abs(dy)) < 45) return;
    const primary = Math.abs(dx) >= Math.abs(dy) ? -Math.sign(dx) : -Math.sign(dy); // swipe left/up → next
    if (onFlip) onFlip(primary);
  };

  return (
    <div data-idx={idx} style={{ height: "100dvh", scrollSnapAlign: "start", scrollSnapStop: "always", position: "relative", background: "#000", overflow: "hidden", display: "flex", alignItems: "center", justifyContent: "center" }}>
      {near && (
        <>
          {active && <div aria-hidden style={{ position: "absolute", inset: 0, background: `url(${src}) center/cover`, filter: "blur(40px) brightness(0.35)", opacity: 0.6 }} />}
          <video ref={videoRef} src={src} loop={!autoplay} playsInline autoPlay muted={muted} preload={active ? "auto" : "metadata"} onClick={onTap} onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd} onLoadedMetadata={onMeta} onEnded={() => { if (autoplay && active && onAdvance) onAdvance(); }}
            style={(landscape && active)
              ? { position: "fixed", inset: 0, width: "100%", height: "100%", objectFit: "contain", background: "#000", zIndex: 2 }
              : { position: "relative", height: "100%", maxWidth: "min(100%, calc(100dvh * 9 / 16))", width: "auto", objectFit: "contain", background: "#000", zIndex: 1 }} />
        </>
      )}

      {/* Info overlay — only the active slide shows it. In landscape it sits
          at TOP-left (just under the bar) so the bottom captions stay clear. */}
      {(active || !landscape) && (
      <div style={landscape
        ? { position: "fixed", left: 60, right: "auto", top: 10, bottom: "auto", maxWidth: "55%", padding: 0, zIndex: 60, textAlign: "left", pointerEvents: "none" }
        : { position: "absolute", left: 0, right: 0, top: "calc(70px + env(safe-area-inset-top))", padding: "0 24px", zIndex: 4, textAlign: "center", pointerEvents: "none", display: "block" }}>
        <SmallCaps color={T.gold} size={9} style={{ textShadow: "0 1px 8px rgba(0,0,0,0.95)" }}>{String(video.n).padStart(2, "0")} · {tt.shortClip}</SmallCaps>
        <h2 style={{ fontFamily: "'Cinzel', serif", fontSize: landscape ? 16 : 22, fontWeight: 500, margin: landscape ? "4px 0 3px" : "8px 0 6px", color: T.paper, letterSpacing: "0.02em", lineHeight: 1.2, textShadow: "0 1px 8px rgba(0,0,0,0.95), 0 0 16px rgba(0,0,0,0.7)" }}>{video.title}</h2>
        <div style={{ fontSize: landscape ? 12 : 14, color: T.paperDim, fontStyle: "italic", fontFamily: "'Cormorant Garamond', serif", textShadow: "0 1px 6px rgba(0,0,0,0.9)", pointerEvents: "auto" }}>
          <a href={landscape ? video.fullWide : video.full} target="_blank" rel="noopener noreferrer" style={{ color: T.gold, fontStyle: "normal", textDecoration: "underline", textUnderlineOffset: "3px" }}>{tt.watchFull} ›</a>
        </div>
      </div>
      )}
    </div>
  );
}

Object.assign(window, { NickConnellyFeed });
