/* Koolorama — shared components */

const { useState, useEffect, useRef, useMemo } = React;

/* ------------------------------------------------------------------ */
/* Iridescent blob — the only ornamental element on the page          */
/* ------------------------------------------------------------------ */
function IridescentBlob({ size = 520, intensity = 1, style = {} }) {
  return (
    <div
      style={{
        position: "relative",
        width: size,
        height: size,
        pointerEvents: "none",
        ...style
      }}>
      
      {/* soft floor shadow — sits BELOW the prism, breathes inversely */}
      <div
        style={{
          position: "absolute",
          left: "5%",
          right: "5%",
          bottom: -size * 0.02,
          height: size * 0.13,
          borderRadius: "50%",
          background:
          "radial-gradient(ellipse at center, rgba(10,10,10,0.55), rgba(10,10,10,0.25) 35%, rgba(10,10,10,0.08) 60%, rgba(10,10,10,0) 78%)",
          animation: "breathe-shadow 7s ease-in-out infinite",
          transformOrigin: "center center",
          willChange: "transform, opacity, filter",
          zIndex: 0,
        }} />
      
      {/* breathing prism */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          animation: "breathe 7s ease-in-out infinite",
          willChange: "transform",
          filter: `saturate(${1.0 * intensity})`,
          zIndex: 1,
        }}>
        
        <img
          src="assets/prism.png"
          alt=""
          draggable="false"
          style={{
            width: "100%",
            height: "100%",
            objectFit: "contain",
            userSelect: "none"
          }} />
        
      </div>
    </div>);

}

/* ------------------------------------------------------------------ */
/* Video placeholder — looks like a premium 9:16 film still           */
/* ------------------------------------------------------------------ */
const FILM_PALETTES = {
  aether: ["#9ad7ff", "#ffd2ea", "#fff1cf", "#cbb6ff"],
  form: ["#ffd1b8", "#ffb5d8", "#e8c8ff", "#fce8c2"],
  loop: ["#a8c5ff", "#c7b4ff", "#ffd0f0", "#84a8ff"],
  solstice: ["#ffd9b3", "#ffba99", "#ffe1c2", "#ffb09c"],
  mira: ["#b9f0e3", "#cfe7ff", "#e0d6ff", "#aae3d2"],
  glass: ["#dfe7ff", "#ffd6e7", "#fff0d9", "#cfe2ff"],
  monolith: ["#1a1a1a", "#2b2b2b", "#3a3a3a", "#0a0a0a"],
  bloom: ["#ffe2e8", "#ffd0b8", "#ffb8c8", "#fff1d6"]
};

/* ------------------------------------------------------------------ */
/* FilmGridShimmer — dot grid loading state, bright clusters drift     */
/* across the grid (ChatGPT image-gen feel).                           */
/* ------------------------------------------------------------------ */
function FilmGridShimmer({ dark = false, index = 0 }) {
  // Dot grid colors. On light panels we go DARKER for legibility; on dark
  // (monolith) we go LIGHTER. Bright clusters always push toward white,
  // mixed with screen blending so the existing palette glows through.
  const baseDot   = dark ? "rgba(255,255,255,0.16)" : "rgba(10,10,10,0.22)";
  const brightDot = dark ? "rgba(255,255,255,0.95)" : "rgba(255,255,255,0.95)";
  const blend     = dark ? "screen" : "screen";
  const gridSize  = 13; // dot pitch in px
  const dotR      = 1.0; // dot radius in px

  const dotPattern = (color) =>
    `radial-gradient(circle at center, ${color} ${dotR}px, transparent ${dotR + 0.4}px)`;

  // Single soft blob mask — translated across the panel via mask-position.
  const blobMask = "radial-gradient(ellipse 38% 32% at center, rgba(0,0,0,1) 0%, rgba(0,0,0,0.6) 35%, rgba(0,0,0,0) 65%)";
  const blobMaskTight = "radial-gradient(ellipse 26% 22% at center, rgba(0,0,0,1) 0%, rgba(0,0,0,0.55) 40%, rgba(0,0,0,0) 70%)";
  const blobMaskWide = "radial-gradient(ellipse 50% 30% at center, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.45) 40%, rgba(0,0,0,0) 70%)";

  const clusterBase = {
    position: "absolute",
    inset: 0,
    pointerEvents: "none",
    backgroundImage: dotPattern(brightDot),
    backgroundSize: `${gridSize}px ${gridSize}px`,
    mixBlendMode: blend,
    WebkitMaskRepeat: "no-repeat",
    maskRepeat: "no-repeat",
    WebkitMaskSize: "180% 180%",
    maskSize: "180% 180%",
    willChange: "mask-position",
  };

  // Stagger per-panel so the gallery doesn't pulse in lock-step
  const phase = (index % 8) * 0.6;

  return (
    <React.Fragment>
      {/* Base dim dot grid — anchored, always visible */}
      <div
        aria-hidden="true"
        style={{
          position: "absolute",
          inset: 0,
          pointerEvents: "none",
          backgroundImage: dotPattern(baseDot),
          backgroundSize: `${gridSize}px ${gridSize}px`,
          mixBlendMode: dark ? "screen" : "multiply",
          opacity: 0.7,
        }} />

      {/* Bright cluster A — slow, long diagonal sweep */}
      <div
        aria-hidden="true"
        style={{
          ...clusterBase,
          WebkitMaskImage: blobMask,
          maskImage: blobMask,
          animation: `cluster-a 2.8s ease-in-out infinite`,
          animationDelay: `${-phase}s`,
        }} />

      {/* Bright cluster B — opposite direction, slightly faster */}
      <div
        aria-hidden="true"
        style={{
          ...clusterBase,
          WebkitMaskImage: blobMaskTight,
          maskImage: blobMaskTight,
          WebkitMaskSize: "160% 160%",
          maskSize: "160% 160%",
          opacity: 0.9,
          animation: `cluster-b 3.6s ease-in-out infinite`,
          animationDelay: `${-phase - 1.4}s`,
        }} />

      {/* Bright cluster C — top-down wandering blob */}
      <div
        aria-hidden="true"
        style={{
          ...clusterBase,
          WebkitMaskImage: blobMaskWide,
          maskImage: blobMaskWide,
          WebkitMaskSize: "220% 220%",
          maskSize: "220% 220%",
          opacity: 0.7,
          animation: `cluster-c 4.5s ease-in-out infinite`,
          animationDelay: `${-phase - 3}s`,
        }} />
    </React.Fragment>
  );
}

function FilmStill({
  paletteKey = "aether",
  title,
  client,
  meta,
  ratio = "9 / 16",
  active = false,
  onClick,
  index = 0,
  duration = "0:15",
  category = "Brand Film",
  videoSrc,
  posterSrc,
  playing = false,
  muted = true,
  onSoundToggle,
  showControls = false
}) {
  const p = FILM_PALETTES[paletteKey] || FILM_PALETTES.aether;
  const dark = paletteKey === "monolith";
  const wrapperRef = useRef(null);
  const videoRef = useRef(null);
  const [ready, setReady] = useState(false);
  const [isFullscreen, setIsFullscreen] = useState(false);
  // Pseudo-fullscreen — used when the real Fullscreen API isn't available
  // (e.g. inside an iframe without `allow="fullscreen"`). We render the
  // panel as a fixed overlay covering the viewport.
  const [pseudoFs, setPseudoFs] = useState(false);
  const fullscreenActive = isFullscreen || pseudoFs;
  // User-driven pause (only meaningful when showControls is true).
  // Distinct from `playing`, which is the viewport-driven "is it allowed to play".
  const [userPaused, setUserPaused] = useState(false);
  // Scrub state
  const [progress, setProgress] = useState(0); // 0..1
  const [vDuration, setVDuration] = useState(0); // seconds
  const [scrubbing, setScrubbing] = useState(false);
  const [hover, setHover] = useState(false);
  // Controls auto-hide. We track an "idle" flag that goes true 1s after
  // the cursor leaves the panel. All overlay chrome (duration, category,
  // fullscreen, sound buttons) fades out together. Hovering or scrubbing
  // brings them back instantly.
  const [idle, setIdle] = useState(false);
  useEffect(() => {
    if (!showControls) return;
    if (hover || scrubbing) {
      setIdle(false);
      return;
    }
    const t = setTimeout(() => setIdle(true), 1000);
    return () => clearTimeout(t);
  }, [hover, scrubbing, showControls]);

  // Escape exits pseudo-fullscreen (real fullscreen handles this natively)
  useEffect(() => {
    if (!pseudoFs) return;
    const onKey = (e) => { if (e.key === "Escape") setPseudoFs(false); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [pseudoFs]);

  // Track fullscreen changes (handles ESC exit too).
  // Listen on both standard and webkit-prefixed events for Safari support.
  useEffect(() => {
    const onChange = () => {
      const fsEl =
        document.fullscreenElement ||
        document.webkitFullscreenElement;
      setIsFullscreen(fsEl === wrapperRef.current);
    };
    document.addEventListener("fullscreenchange", onChange);
    document.addEventListener("webkitfullscreenchange", onChange);
    return () => {
      document.removeEventListener("fullscreenchange", onChange);
      document.removeEventListener("webkitfullscreenchange", onChange);
    };
  }, []);

  // Resolve `videoSrc` to a fully-seekable URL. Some servers (incl. the
  // local dev preview) don't return HTTP 206 Range responses, which makes
  // the browser refuse `currentTime` writes even after buffering the
  // whole file. Loading the bytes via fetch + createObjectURL sidesteps
  // that: blob URLs are local and always seekable.
  //
  // Only the featured panel needs this (it's the one with a scrub bar
  // and the one that actually plays), so we gate on `showControls`.
  // Thumbs get nothing — they show posters and don't mount a <video>.
  const [resolvedSrc, setResolvedSrc] = useState(null);
  useEffect(() => {
    if (!videoSrc || !showControls) {
      // For non-featured panels with a video source, fall back to direct
      // playback — we never seek into them.
      setResolvedSrc(showControls ? null : videoSrc || null);
      return;
    }
    let cancelled = false;
    let objectUrl = null;
    setResolvedSrc(null); // trigger shimmer while we fetch
    fetch(videoSrc)
      .then((r) => r.blob())
      .then((blob) => {
        if (cancelled) return;
        objectUrl = URL.createObjectURL(blob);
        setResolvedSrc(objectUrl);
      })
      .catch(() => {
        // Fall back to direct src — at worst seeking won't work but the
        // film will still play.
        if (!cancelled) setResolvedSrc(videoSrc);
      });
    return () => {
      cancelled = true;
      if (objectUrl) URL.revokeObjectURL(objectUrl);
    };
  }, [videoSrc, showControls]);

  // Reset the "ready" gate whenever the source changes so the shimmer
  // panel re-appears under the next clip while it loads.
  useEffect(() => {
    setReady(false);
    setUserPaused(false);
    setProgress(0);
    setVDuration(0);
  }, [videoSrc]);

  // Drive play/pause from `playing` AND user pause intent. The element
  // itself respects `muted` via the DOM property (React's `muted`
  // attribute alone is unreliable).
  // Depends on `resolvedSrc` (not `videoSrc`) so playback kicks in
  // immediately after a blob URL is wired up — otherwise switching to a
  // new film leaves the video stuck on the first frame.
  useEffect(() => {
    const v = videoRef.current;
    if (!v) return;
    v.muted = muted;
    const shouldPlay = playing && resolvedSrc && !userPaused;
    if (shouldPlay) {
      const tryPlay = () => {
        const pr = v.play();
        if (pr && pr.catch) pr.catch(() => {});
      };
      tryPlay();
      // Some browsers reject .play() before the new src is loaded — retry
      // once the element signals it can play.
      const onCanPlay = () => tryPlay();
      v.addEventListener("canplay", onCanPlay, { once: true });
      return () => v.removeEventListener("canplay", onCanPlay);
    } else {
      v.pause();
    }
  }, [playing, muted, resolvedSrc, userPaused]);

  // Track playback position for the scrub bar
  useEffect(() => {
    const v = videoRef.current;
    if (!v || !videoSrc) return;
    const onTime = () => {
      if (scrubbing) return; // don't fight the user while dragging
      if (v.duration > 0) setProgress(v.currentTime / v.duration);
    };
    const onMeta = () => setVDuration(v.duration || 0);
    v.addEventListener("timeupdate", onTime);
    v.addEventListener("loadedmetadata", onMeta);
    v.addEventListener("durationchange", onMeta);
    return () => {
      v.removeEventListener("timeupdate", onTime);
      v.removeEventListener("loadedmetadata", onMeta);
      v.removeEventListener("durationchange", onMeta);
    };
  }, [videoSrc, scrubbing]);

  // Seek by clicking / dragging the scrub bar
  const scrubRef = useRef(null);
  const seekToClientX = (clientX) => {
    const v = videoRef.current;
    const bar = scrubRef.current;
    if (!v || !bar) return;
    if (!Number.isFinite(v.duration) || v.duration <= 0) return;
    const rect = bar.getBoundingClientRect();
    if (rect.width <= 0) return;
    const ratio = Math.max(0, Math.min(0.999, (clientX - rect.left) / rect.width));
    v.currentTime = ratio * v.duration;
    setProgress(ratio);
  };
  const handleScrubDown = (e) => {
    if (!videoSrc) return;
    e.stopPropagation();
    setScrubbing(true);
    seekToClientX(e.clientX);
    const onMove = (ev) => seekToClientX(ev.clientX);
    const onUp   = () => {
      setScrubbing(false);
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup",   onUp);
    };
    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup",   onUp);
  };

  const togglePlay = (e) => {
    if (!showControls || !videoSrc) return;
    e.stopPropagation();
    setUserPaused((p) => !p);
  };

  // What handler fires on a click on the panel itself?
  // - Thumb (no showControls): onClick from parent (sets as featured)
  // - Featured (showControls): toggle play/pause
  const panelClick = showControls ? togglePlay : onClick;

  // Shared style for the small round overlay control buttons.
  const ctlBtnStyle = {
    width: 32,
    height: 32,
    borderRadius: "50%",
    border: "none",
    background: "rgba(10,10,10,0.55)",
    backdropFilter: "blur(8px)",
    WebkitBackdropFilter: "blur(8px)",
    color: "#fafafa",
    display: "grid",
    placeItems: "center",
    cursor: "pointer",
    transition: "background .25s ease",
  };

  return (
    <div
      ref={wrapperRef}
      className="film-still"
      onClick={panelClick}
      onMouseEnter={showControls ? () => setHover(true) : undefined}
      onMouseLeave={showControls ? () => setHover(false) : undefined}
      style={{
        position: pseudoFs ? "fixed" : "relative",
        ...(pseudoFs && {
          inset: 0,
          zIndex: 9999,
        }),
        // In any fullscreen mode (real or pseudo) drop the 9:16 box and
        // let the wrapper fill the screen on a black background.
        aspectRatio: fullscreenActive ? "auto" : ratio,
        width: pseudoFs ? "100vw" : "100%",
        height: pseudoFs ? "100vh" : undefined,
        borderRadius: fullscreenActive ? 0 : 4,
        overflow: "hidden",
        cursor: panelClick ? "pointer" : "default",
        background: fullscreenActive ? "#000" : (dark ? "#0a0a0a" : "#eee"),
        transition: "transform .6s cubic-bezier(.2,.7,.2,1), box-shadow .6s ease",
        transform: active ? "scale(1.0)" : "scale(1.0)",
        boxShadow: fullscreenActive ? "none" : (active ?
        "0 30px 80px -40px rgba(10,10,10,0.35), 0 8px 24px -8px rgba(10,10,10,0.10)" :
        "0 12px 36px -24px rgba(10,10,10,0.30)")
      }}>
      
      {/* Decoration layers — hidden in fullscreen so only the video shows
          on a black background, like a real player. */}
      {!fullscreenActive && (
        <React.Fragment>
          {/* base color */}
          <div
            style={{
              position: "absolute",
              inset: 0,
              background: `linear-gradient(${135 + index * 23}deg, ${p[0]}, ${p[1]} 35%, ${p[2]} 65%, ${p[3]})`
            }} />

          {/* iridescent sheen — slow asymmetric drift gives the panel a
              liquid feel. Per-panel direction (a vs b) and delay desyncs them. */}
          <div
            style={{
              position: "absolute",
              inset: "-10%",
              background: `conic-gradient(from ${index * 47}deg at 60% 40%, ${p[0]}, ${p[1]}, ${p[2]}, ${p[3]}, ${p[0]})`,
              mixBlendMode: dark ? "screen" : "soft-light",
              opacity: dark ? 0.5 : 0.85,
              filter: "blur(20px)",
              animation: `${index % 2 ? "panel-drift-b" : "panel-drift-a"} ${22 + (index % 4) * 3}s ease-in-out infinite`,
              animationDelay: `${-(index * 1.7)}s`,
              willChange: "transform"
            }} />

          {/* Poster — real first-frame thumbnail. Sits above the iridescent
              panel so each tile actually previews its film. The panel below
              remains the loading fallback if the image hasn't loaded yet. */}
          {posterSrc && (
            <img
              src={posterSrc}
              alt=""
              loading="lazy"
              decoding="async"
              style={{
                position: "absolute",
                inset: 0,
                width: "100%",
                height: "100%",
                objectFit: "cover",
                userSelect: "none",
                pointerEvents: "none"
              }} />
          )}

          {/* film grain */}
          <div
            style={{
              position: "absolute",
              inset: 0,
              backgroundImage:
              "radial-gradient(rgba(255,255,255,0.08) 1px, transparent 1px), radial-gradient(rgba(0,0,0,0.05) 1px, transparent 1px)",
              backgroundSize: "3px 3px, 5px 5px",
              backgroundPosition: "0 0, 1px 2px",
              mixBlendMode: dark ? "screen" : "multiply",
              opacity: 0.5
            }} />

          {/* subtle vignette */}
          <div
            style={{
              position: "absolute",
              inset: 0,
              background: dark ?
              "radial-gradient(ellipse at 50% 100%, rgba(0,0,0,0), rgba(0,0,0,0.5))" :
              "radial-gradient(ellipse at 50% 100%, rgba(255,255,255,0), rgba(10,10,10,0.18))"
            }} />

          {/* Loading shimmer — only visible on a featured panel while its
              video is loading. */}
          <div
            style={{
              position: "absolute",
              inset: 0,
              opacity: active && playing && !ready ? 1 : 0,
              transition: "opacity .6s ease",
              pointerEvents: "none"
            }}>
            <FilmGridShimmer dark={dark} index={index} />
          </div>
        </React.Fragment>
      )}

      {/* Video — always mounted while a source is set so seeking and
          buffered position survive across viewport / pause transitions.
          Playback is driven by the useEffect above (`playing` + `userPaused`).
          `resolvedSrc` may be a blob URL (featured) or the original URL
          (thumbs / fallback). */}
      {resolvedSrc && (
        <video
          ref={videoRef}
          src={resolvedSrc}
          playsInline
          loop
          preload="auto"
          onCanPlay={() => setReady(true)}
          onPlaying={() => setReady(true)}
          style={{
            position: "absolute",
            inset: 0,
            width: "100%",
            height: "100%",
            objectFit: fullscreenActive ? "contain" : "cover",
            opacity: ready ? 1 : 0,
            transition: "opacity 0.5s ease",
            pointerEvents: "none"
          }} />
      )}

      {/* duration tag */}
      <div
        style={{
          position: "absolute",
          top: 14,
          left: 14,
          fontFamily: "var(--mono)",
          fontSize: 10,
          letterSpacing: "0.14em",
          color: "#ffffff",
          background: "rgba(10,10,10,0.42)",
          backdropFilter: "blur(8px)",
          WebkitBackdropFilter: "blur(8px)",
          padding: "4px 8px",
          borderRadius: 2,
          textTransform: "uppercase",
          opacity: showControls && idle ? 0 : 1,
          transition: "opacity .35s ease",
          pointerEvents: showControls && idle ? "none" : "auto"
        }}>
        
        {duration}
      </div>
      {/* category */}
      <div
        style={{
          position: "absolute",
          top: 14,
          right: 14,
          fontFamily: "var(--mono)",
          fontSize: 10,
          letterSpacing: "0.14em",
          color: "#ffffff",
          textShadow: "0 1px 2px rgba(0,0,0,0.45)",
          textTransform: "uppercase",
          opacity: showControls && idle ? 0 : 1,
          transition: "opacity .35s ease"
        }}>
        
        {category}
      </div>
      {/* play affordance — center. Visible on the featured panel when
          paused, either by the user or because it isn't in view yet. */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          opacity: active && showControls && (userPaused || !playing) ? 1 : 0.0,
          transition: "opacity .35s ease",
          pointerEvents: "none"
        }}>
        
        <div
          style={{
            width: 64,
            height: 64,
            borderRadius: "50%",
            background: "rgba(255,255,255,0.85)",
            backdropFilter: "blur(8px)",
            display: "grid",
            placeItems: "center",
            color: "#0a0a0a"
          }}>
          
          <svg width="16" height="18" viewBox="0 0 16 18" fill="none">
            <path d="M1 1L15 9L1 17V1Z" fill="currentColor" />
          </svg>
        </div>
      </div>

      {/* caption — keeps a small fixed offset from the bottom in all
          modes so it never drifts into the subtitle band of the video. */}
      <div
        style={{
          position: "absolute",
          left: 14,
          right: 14,
          bottom: 14,
          display: "flex",
          alignItems: "flex-end",
          justifyContent: "space-between",
          gap: 10,
          color: "#ffffff",
          textShadow: "0 1px 3px rgba(0,0,0,0.55)"
        }}>
        
        <div style={{ minWidth: 0, flex: 1 }}>
          <div
            style={{
              fontFamily: "var(--serif)",
              fontSize: showControls ? 24 : 19,
              lineHeight: 1.0,
              letterSpacing: "-0.01em",
              whiteSpace: "nowrap",
              overflow: "hidden",
              textOverflow: "ellipsis"
            }}>
            
            {title}
          </div>
          <div
            style={{
              fontFamily: "var(--mono)",
              fontSize: 10,
              letterSpacing: "0.14em",
              textTransform: "uppercase",
              marginTop: 6,
              opacity: 0.7,
              whiteSpace: "nowrap",
              overflow: "hidden",
              textOverflow: "ellipsis"
            }}>
            
            {client}{meta ? " — " + meta : ""}
          </div>
        </div>
        <div
          style={{
            fontFamily: "var(--mono)",
            fontSize: 10,
            letterSpacing: "0.14em",
            opacity: 0.6,
            textTransform: "uppercase"
          }}>
          
          {String(index + 1).padStart(2, "0")}
        </div>
      </div>

      {/* Featured-only chrome: control button group (fullscreen + sound)
          above the scrub bar. Scrub bar is always visible thin line,
          expanded on hover. */}
      {showControls && videoSrc && (
        <React.Fragment>
          <div
            style={{
              position: "absolute",
              bottom: 18,
              right: 14,
              display: "flex",
              gap: 8,
              opacity: ready && !idle ? 1 : 0,
              transition: "opacity .35s ease",
              pointerEvents: ready && !idle ? "auto" : "none",
              zIndex: 3,
            }}>
            {/* Fullscreen */}
            <button
              type="button"
              onClick={(e) => {
                e.stopPropagation();
                // Already in pseudo-fs → exit
                if (pseudoFs) { setPseudoFs(false); return; }
                // Already in real fullscreen → exit
                const fsEl =
                  document.fullscreenElement ||
                  document.webkitFullscreenElement;
                if (fsEl) {
                  (document.exitFullscreen ||
                   document.webkitExitFullscreen ||
                   (() => {})).call(document);
                  return;
                }
                // Always TRY the real API first — only fall back to
                // pseudo-fullscreen if it actually throws or rejects.
                const root = wrapperRef.current;
                const req =
                  root && (root.requestFullscreen || root.webkitRequestFullscreen);
                if (!req) {
                  setPseudoFs(true);
                  return;
                }
                try {
                  const pr = req.call(root);
                  if (pr && pr.catch) pr.catch(() => setPseudoFs(true));
                } catch (err) {
                  setPseudoFs(true);
                }
              }}
              aria-label={fullscreenActive ? "Exit fullscreen" : "Enter fullscreen"}
              className="film-ctl-btn"
              style={ctlBtnStyle}
              onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(10,10,10,0.78)"; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = "rgba(10,10,10,0.55)"; }}
            >
              {fullscreenActive ? (
                <svg width="13" height="13" viewBox="0 0 20 20" fill="none">
                  <path d="M8 2v6H2M12 2v6h6M8 18v-6H2M12 18v-6h6" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
              ) : (
                <svg width="13" height="13" viewBox="0 0 20 20" fill="none">
                  <path d="M3 8V3h5M17 8V3h-5M3 12v5h5M17 12v5h-5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
              )}
            </button>

            {/* Sound */}
            {onSoundToggle && (
              <button
                type="button"
                onClick={(e) => { e.stopPropagation(); onSoundToggle(); }}
                aria-label={muted ? "Unmute" : "Mute"}
                style={ctlBtnStyle}
                onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(10,10,10,0.78)"; }}
                onMouseLeave={(e) => { e.currentTarget.style.background = "rgba(10,10,10,0.55)"; }}
              >
                {muted ? (
                  <svg width="13" height="13" viewBox="0 0 20 20" fill="none">
                    <path d="M3 7v6h3l5 4V3L6 7H3z" fill="currentColor" />
                    <path d="M14 7l5 6M19 7l-5 6" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
                  </svg>
                ) : (
                  <svg width="13" height="13" viewBox="0 0 20 20" fill="none">
                    <path d="M3 7v6h3l5 4V3L6 7H3z" fill="currentColor" />
                    <path d="M14 6c1.3 1.1 2 2.5 2 4s-.7 2.9-2 4M16.5 3.5c2.2 1.7 3.5 4 3.5 6.5s-1.3 4.8-3.5 6.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" fill="none" />
                  </svg>
                )}
              </button>
            )}
          </div>

          {/* Scrub bar — pointer-driven seek. Stops propagation so a click
              on the bar doesn't also toggle play/pause on the panel. */}
          <div
            ref={scrubRef}
            onPointerDown={handleScrubDown}
            onClick={(e) => e.stopPropagation()}
            style={{
              position: "absolute",
              left: 0,
              right: 0,
              bottom: 0,
              height: 18, // generous hit area; visual bar is much thinner
              display: "flex",
              alignItems: "flex-end",
              cursor: "pointer",
              zIndex: 4,
              touchAction: "none",
            }}
          >
            {/* Track */}
            <div
              style={{
                position: "relative",
                width: "100%",
                height: hover || scrubbing ? 5 : 2.5,
                background: "rgba(255,255,255,0.22)",
                transition: "height .25s ease",
              }}
            >
              {/* Fill */}
              <div
                style={{
                  position: "absolute",
                  left: 0,
                  top: 0,
                  bottom: 0,
                  width: `${progress * 100}%`,
                  background: "#ffffff",
                }}
              />
              {/* Knob (visible on hover/scrub) */}
              <div
                style={{
                  position: "absolute",
                  top: "50%",
                  left: `calc(${progress * 100}% - 6px)`,
                  width: 12,
                  height: 12,
                  borderRadius: "50%",
                  background: "#ffffff",
                  boxShadow: "0 1px 3px rgba(0,0,0,0.35)",
                  transform: "translateY(-50%)",
                  opacity: hover || scrubbing ? 1 : 0,
                  transition: "opacity .2s ease",
                }}
              />
            </div>
          </div>
        </React.Fragment>
      )}
    </div>);

}

/* ------------------------------------------------------------------ */
/* Reveal on scroll                                                   */
/* ------------------------------------------------------------------ */
function useReveal() {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            el.classList.add("in");
            io.unobserve(el);
          }
        });
      },
      { threshold: 0.12 }
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return ref;
}

function Reveal({ children, delay = 0, style = {} }) {
  const ref = useReveal();
  return (
    <div ref={ref} className="reveal" style={{ transitionDelay: `${delay}ms`, ...style }}>
      {children}
    </div>);

}

/* ------------------------------------------------------------------ */
/* Wordmark — simple text (logo image used in hero only)              */
/* ------------------------------------------------------------------ */
function Wordmark({ size = 18, animateIn = false }) {
  // Wordmark image is 1100×166. We render it at height = size * 0.95,
  // width auto. The intro video has the exact same aspect (1100×166),
  // so we can overlay it at the same height/width and play once.
  const wordmarkHeight = size * 0.95;
  const wordmarkWidth = wordmarkHeight * (1100 / 166);

  const videoRef = React.useRef(null);
  const prismVideoRef = React.useRef(null);
  // When animateIn is true, we want the video to play once, THEN swap to PNG.
  // When false, we just show the PNG.
  const [videoDone, setVideoDone] = React.useState(!animateIn);
  const [prismVideoDone, setPrismVideoDone] = React.useState(!animateIn);

  React.useEffect(() => {
    if (!animateIn) return;
    const v = videoRef.current;
    if (!v) return;
    // Restart from the beginning in case the video element already cached the end
    // (e.g. it preloaded while the page was hidden behind the intro overlay).
    try { v.currentTime = 0; } catch (e) {}
    setVideoDone(false);
    const onEnded = () => setVideoDone(true);
    v.addEventListener("ended", onEnded, { once: true });
    const p = v.play();
    if (p && p.catch) p.catch(() => setVideoDone(true));
    return () => v.removeEventListener("ended", onEnded);
  }, [animateIn]);

  React.useEffect(() => {
    if (!animateIn) return;
    const v = prismVideoRef.current;
    if (!v) return;
    try { v.currentTime = 0; } catch (e) {}
    setPrismVideoDone(false);
    const onEnded = () => setPrismVideoDone(true);
    v.addEventListener("ended", onEnded, { once: true });
    const p = v.play();
    if (p && p.catch) p.catch(() => setPrismVideoDone(true));
    return () => v.removeEventListener("ended", onEnded);
  }, [animateIn]);

  return (
    <div
      style={{
        display: "inline-flex",
        alignItems: "center",
        gap: size * 0.55
      }}>
      
      <div
        style={{
          position: "relative",
          width: 20,
          height: 20,
          flexShrink: 0,
        }}>
        <img
          src="assets/prism.png"
          alt=""
          draggable="false"
          style={{
            position: "absolute",
            inset: 0,
            width: "100%",
            height: "100%",
            objectFit: "contain",
            userSelect: "none",
            opacity: prismVideoDone ? 1 : 0,
          }} />
        {animateIn && (
          <video
            ref={prismVideoRef}
            muted
            playsInline
            autoPlay
            preload="auto"
            aria-hidden="true"
            style={{
              position: "absolute",
              inset: 0,
              width: "100%",
              height: "100%",
              display: "block",
              objectFit: "contain",
              pointerEvents: "none",
              opacity: prismVideoDone ? 0 : 1,
            }}>
            <source src="assets/prism-intro.webm" type="video/webm" />
            <source src="assets/prism-intro.mp4"  type="video/mp4" />
          </video>
        )}
      </div>
      
      <div
        style={{
          position: "relative",
          height: wordmarkHeight,
          width: wordmarkWidth,
          margin: "0px 0px 0px -10px",
        }}>
        <img
          src="assets/wordmark.png"
          alt="koolorama"
          draggable="false"
          style={{
            position: "absolute",
            inset: 0,
            height: "100%",
            width: "100%",
            display: "block",
            userSelect: "none",
            opacity: videoDone ? 1 : 0,
          }} />
        {animateIn && (
          <video
            ref={videoRef}
            muted
            playsInline
            autoPlay
            preload="auto"
            aria-hidden="true"
            style={{
              position: "absolute",
              inset: 0,
              height: "100%",
              width: "100%",
              display: "block",
              objectFit: "fill",
              pointerEvents: "none",
              opacity: videoDone ? 0 : 1,
            }}>
            <source src="assets/wordmark-intro.webm" type="video/webm" />
            <source src="assets/wordmark-intro.mp4"  type="video/mp4" />
          </video>
        )}
      </div>
      
    </div>);

}

Object.assign(window, {
  IridescentBlob,
  FilmStill,
  Reveal,
  useReveal,
  Wordmark
});