// =====================================================================
// React overlay app — captions, scrubber, controls, take-control panel,
// cold-open text, CTA.
// =====================================================================
const { useState, useEffect, useMemo } = React;

function useFilm() {
  const [, force] = useState(0);
  useEffect(() => {
    // R38 — FilmAPI is built inside scene.js's async IIFE, so React can mount
    // first. The old code force-rendered once when FilmAPI appeared but never
    // SUBSCRIBED in that branch — the whole overlay froze at its first render
    // (no time updates, Play gate stuck). Always attach the subscription once
    // FilmAPI exists, whichever side wins the race.
    let unsub = null;
    let id = null;
    const attach = () => {
      unsub = window.FilmAPI.subscribe(() => force((x) => x + 1));
      force((x) => x + 1);
    };
    if (window.FilmAPI) {
      attach();
    } else {
      id = setInterval(() => {
        if (window.FilmAPI) { clearInterval(id); id = null; attach(); }
      }, 50);
    }
    return () => { if (id) clearInterval(id); if (unsub) unsub(); };
  }, []);
  return window.FilmAPI;
}

// ---- Caption ----------------------------------------------------------
function Caption({ time, on }) {
  const line = useMemo(() => {
    if (!window.VO_SCRIPT) return null;
    return window.VO_SCRIPT.find((v) => time >= v.t_in && time <= v.t_out);
  }, [time]);

  if (!on || !line) return null;

  // Letter-reveal: progress through 200ms ramp-in window per line.
  const since = time - line.t_in;
  const len = line.text.length;
  const charsPerSec = Math.max(20, len / Math.max(0.6, line.t_out - line.t_in - 1.0));
  const shown = Math.min(len, Math.floor(since * charsPerSec));
  // exit dissolve in last 0.5s
  const remaining = line.t_out - time;
  const exitOpacity = remaining < 0.5 ? Math.max(0, remaining / 0.5) : 1;

  // Round 33 — clean, consistent typography (no contrast halo / outline).
  // White text on dark backdrops, near-black on light, driven by the published
  // bgLerp (0 = dark, 1 = light). Legibility comes from a single SOFT drop
  // shadow (a gentle readability glow), not a hard multi-direction outline —
  // so the lettering stays crisp and matches the reference look.
  const bg = (typeof window !== 'undefined' && window.__V5_BG_LERP__ != null)
    ? window.__V5_BG_LERP__ : 1;
  const dark = bg < 0.5;
  const color = dark ? 'rgba(255,255,255,0.98)' : '#15130f';
  const textShadow = dark
    ? '0 2px 16px rgba(2,6,16,0.62)'
    : '0 1px 12px rgba(255,255,255,0.55)';

  return (
    <div
      className="absolute left-0 right-0 px-6 text-center"
      style={{
        // R43 — captions moved to the very bottom (the scrubber is gone). They
        // sit just above the persistent right-aligned legal footer.
        bottom: 'clamp(32px, 2.8vw, 48px)',
        maxWidth: 'min(1340px, 92vw)',
        marginLeft: 'auto',
        marginRight: 'auto',
        opacity: exitOpacity,
        transition: 'opacity 120ms linear',
      }}
    >
      <div
        style={{
          color,
          textShadow,
          fontSize: 'clamp(22px, 2.3vw, 33px)',
          lineHeight: 1.28,
          fontWeight: 600,
          letterSpacing: '0.012em',
          transition: 'color 400ms linear, text-shadow 400ms linear',
          fontFamily: 'Inter, system-ui, sans-serif',
        }}
      >
        {line.text.slice(0, shown)}
        <span style={{ opacity: 0.0 }}>{line.text.slice(shown)}</span>
      </div>
    </div>
  );
}

// ---- Scrubber ---------------------------------------------------------
function Scrubber({ api, time, duration }) {
  const [hoverT, setHoverT] = useState(null);
  const markers = window.CHAPTER_MARKERS || [];

  function pointerT(e) {
    const r = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX - r.left) / r.width;
    return Math.max(0, Math.min(1, x)) * duration;
  }

  const hoverMarker = useMemo(() => {
    if (hoverT == null) return null;
    return [...markers].reverse().find((m) => m.t <= hoverT) || markers[0];
  }, [hoverT, markers]);

  return (
    <div className="absolute left-0 right-0 bottom-0 px-8 pb-6">
      {hoverMarker && (
        <div
          className="text-[11px] tracking-[0.16em] uppercase font-medium pb-2"
          style={{ color: '#1a1814', opacity: 0.7, fontFamily: 'IBM Plex Mono, monospace' }}
        >
          {hoverMarker.short}
        </div>
      )}
      <div
        className="relative h-6 cursor-pointer group"
        onPointerMove={(e) => setHoverT(pointerT(e))}
        onPointerLeave={() => setHoverT(null)}
        onClick={(e) => { api.seek(pointerT(e)); }}
      >
        {/* track */}
        <div className="absolute left-0 right-0 top-1/2 -translate-y-1/2 h-[2px]"
             style={{ background: 'rgba(26,24,20,0.18)' }} />
        {/* filled */}
        <div className="absolute left-0 top-1/2 -translate-y-1/2 h-[2px]"
             style={{ background: '#4D8BFF', width: `${(time / duration) * 100}%` }} />
        {/* markers */}
        {markers.map((m, i) => (
          <div key={i}
               className="absolute top-1/2 -translate-y-1/2"
               style={{ left: `${(m.t / duration) * 100}%` }}>
            <div className="w-[1px] h-3 -translate-x-[0.5px]"
                 style={{ background: time >= m.t ? '#4D8BFF' : 'rgba(26,24,20,0.35)' }} />
          </div>
        ))}
        {/* playhead */}
        <div className="absolute top-1/2 -translate-y-1/2 -translate-x-1/2"
             style={{ left: `${(time / duration) * 100}%` }}>
          <div className="w-2 h-2 rounded-full" style={{ background: '#4D8BFF', boxShadow: '0 0 12px #4D8BFF' }} />
        </div>
      </div>
    </div>
  );
}

// ---- Top bar / controls ----------------------------------------------
// Round 19: buttons + duration counter readable across BOTH the dark
// cold-open veil and the light frosted-glass dashboard background. We
// give the chrome its own subtle dark pill (rgba(11,16,28,0.55) +
// backdrop-blur) so the text contrast is independent of what's behind.
function TopBar({ api, time, duration, captionsOn, muted, takeControl }) {
  function fmt(t) {
    const m = Math.floor(t / 60).toString().padStart(1, '0');
    const s = Math.floor(t % 60).toString().padStart(2, '0');
    return `${m}:${s}`;
  }
  const playing = api?.isPlaying?.() ?? false;
  const chipBg = {
    background:           'rgba(11,16,28,0.62)',
    backdropFilter:       'blur(8px) saturate(140%)',
    WebkitBackdropFilter: 'blur(8px) saturate(140%)',
  };
  return (
    <div className="absolute top-0 left-0 right-0 px-8 pt-6 flex items-center justify-between pointer-events-none">
      <div className="pointer-events-auto flex items-center gap-3 px-3 py-1.5 rounded-md"
           style={chipBg}>
        <button
          onClick={() => api.toggle()}
          className="w-7 h-7 flex items-center justify-center"
          style={{ color: 'rgba(255,255,255,0.92)' }}
          aria-label={playing ? 'Pause' : 'Play'}
        >
          {playing
            ? <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
                <rect x="2" y="1" width="3" height="12" fill="currentColor" /><rect x="9" y="1" width="3" height="12" fill="currentColor" />
              </svg>
            : <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 1 L12 7 L2 13 Z" fill="currentColor" /></svg>}
        </button>
        <div className="text-[13px] tracking-[0.16em] uppercase font-medium"
             style={{ color: 'rgba(255,255,255,0.92)', fontFamily: 'IBM Plex Mono, monospace' }}>
          3DREAMS@SG · CAPABILITIES FILM · {fmt(time)} / {fmt(duration)}
        </div>
      </div>
      <div className="pointer-events-auto flex items-center gap-1 text-[13px] tracking-[0.14em] uppercase font-medium rounded-md p-1"
           style={{ ...chipBg, fontFamily: 'IBM Plex Mono, monospace' }}>
        <KbdToggle label="C captions" on={captionsOn} onClick={() => api.setCaptions(!captionsOn)} />
        <KbdToggle label="M sound"   on={!muted}     onClick={() => api.setMuted(!muted)} />
        <KbdToggle label="T control" on={takeControl} onClick={() => api.setTakeControl(!takeControl)} />
        <KbdToggle label="F fullscreen" on={false} onClick={() => api.toggleFullscreen()} />
        <button
          onClick={() => {
            // Round 36 — RESTART the film in place (back to the Play gate),
            // rather than navigating to the product root. This is a standalone
            // capabilities film, so there's no "home" to exit to; restarting
            // lets a presenter re-run it from the top.
            try {
              if (api && api.pause) api.pause();
              if (api && api.seek)  api.seek(0);
            } catch (_) {}
          }}
          className="px-3 py-1.5 border transition-colors rounded-sm"
          style={{
            borderColor: 'rgba(239,68,68,0.55)',
            color: 'rgba(255,210,210,0.95)',
            background: 'rgba(239,68,68,0.10)',
          }}
        >
          ↺ RESTART
        </button>
      </div>
    </div>
  );
}

function KbdToggle({ label, on, onClick }) {
  return (
    <button onClick={onClick}
            className="px-2.5 py-1.5 border transition-colors rounded-sm"
            style={{
              borderColor: on ? 'rgba(77,139,255,0.85)' : 'rgba(255,255,255,0.18)',
              color: on ? 'rgba(255,255,255,0.96)' : 'rgba(255,255,255,0.72)',
              background: on ? 'rgba(77,139,255,0.18)' : 'transparent',
            }}>
      {label}
    </button>
  );
}

// ---- Cold open --------------------------------------------------------
// Dark-room hero: full-bleed veil with the animated 3DREAMS@SG wordmark
// centred, then fades out by t≈3.2 s as the hologram column reveals.
// Cool-blue accent matches the rest of the Round-6+ palette (no phosphor).
function ColdOpen({ time }) {
  // Round 21 — the cold-open wordmark is now a 3D holographic plane the
  // camera flies through (title-hologram.js), and the scene opens in dark
  // space on its own (no veil needed). The old 2D overlay is retired to
  // avoid a double title.
  return null;
  // eslint-disable-next-line no-unreachable
  if (time > 3.6) return null;

  // Wordmark visible from t=0 — the user wants the dark room with the
  // logo already there, not a flash-to-black. We use a quick 0..200 ms
  // ramp instead of the previous 0..400 ms (which left t=0 blank).
  const titleOpacity =
    time < 0.20 ? time / 0.20 :
    time > 3.20 ? Math.max(0, (3.6 - time) / 0.4) : 1;

  // Sub-label fades in slightly later
  const subOpacity =
    time < 0.80 ? 0 :
    time < 1.30 ? (time - 0.80) / 0.50 :
    time > 3.20 ? Math.max(0, (3.6 - time) / 0.4) : 1;

  // Subtle "ignite" — a soft pulse on the wordmark every 1.6 s.
  const pulse = 1.0 + 0.018 * Math.sin(time * Math.PI * 1.25);

  // Round 20: bright WHITE wash (not a dark navy veil) so the opening is a
  // clean white-room ambient with the jet particle cloud showing through as
  // the wash eases off. 0.94 at t=0 → 0 by t≈3.2.
  const veil =
    time < 2.6 ? 0.94 :
    time < 3.2 ? 0.94 * (3.2 - time) / 0.6 : 0;

  return (
    <div className="absolute inset-0 pointer-events-none flex flex-col items-center justify-center"
         style={{
           opacity: veil,
           transition: 'opacity 80ms linear',
           // Bright frosted-white room with a faint cool-blue radial cast.
           background: 'radial-gradient(circle at 50% 52%, #ffffff 0%, #f3f7fd 58%, #e6edf8 100%)',
         }}>
      {/* corner brackets — frame the wordmark as a HUD card */}
      {[
        { top: 24, left: 24, borderTop: '2px solid rgba(77,139,255,0.70)', borderLeft: '2px solid rgba(77,139,255,0.70)' },
        { top: 24, right: 24, borderTop: '2px solid rgba(77,139,255,0.70)', borderRight: '2px solid rgba(77,139,255,0.70)' },
        { bottom: 24, left: 24, borderBottom: '2px solid rgba(77,139,255,0.70)', borderLeft: '2px solid rgba(77,139,255,0.70)' },
        { bottom: 24, right: 24, borderBottom: '2px solid rgba(77,139,255,0.70)', borderRight: '2px solid rgba(77,139,255,0.70)' },
      ].map((s, i) => (
        <span key={i} aria-hidden="true" style={{ position: 'absolute', width: 22, height: 22, ...s, opacity: titleOpacity }} />
      ))}

      {/* eyebrow */}
      <div style={{
             fontFamily: 'Share Tech Mono, JetBrains Mono, monospace',
             fontSize: '11px',
             letterSpacing: '0.30em',
             textTransform: 'uppercase',
             color: 'rgba(58,107,204,0.95)',   // cool-blue ink (readable on white)
             marginBottom: '24px',
             opacity: titleOpacity,
           }}>
        Atmospheric Intelligence · Singapore
      </div>

      {/* wordmark — dark ink on white + cool-blue underline glow */}
      <div style={{
             position: 'relative',
             fontFamily: 'Inter, sans-serif',
             fontWeight: 800,
             fontSize: 'clamp(56px, 10vw, 128px)',
             letterSpacing: '-0.035em',
             lineHeight: 1,
             color: 'rgba(15,23,42,0.94)',
             opacity: titleOpacity,
             transform: `scale(${pulse})`,
             transition: 'transform 120ms linear',
             textShadow: '0 0 28px rgba(77,139,255,0.22)',
             display: 'flex',
             alignItems: 'baseline',
             gap: '0.04em',
           }}>
        3DREAMS<span style={{
                       fontFamily: 'Share Tech Mono, JetBrains Mono, monospace',
                       fontSize: '0.40em',
                       fontWeight: 500,
                       letterSpacing: '0.10em',
                       color: 'rgba(77,139,255,1)',
                       transform: 'translateY(-0.04em)',
                       display: 'inline-block',
                       textShadow: '0 0 18px rgba(77,139,255,0.45)',
                     }}>@SG</span>
        {/* underline sweep — animates left → right then holds */}
        <span aria-hidden="true" style={{
          position: 'absolute',
          left: 0,
          bottom: -10,
          height: 1.5,
          width: `${Math.min(100, Math.max(0, (time - 0.2) / 0.9 * 100))}%`,
          background: 'linear-gradient(90deg, transparent, rgba(122,166,255,0.85), transparent)',
          opacity: titleOpacity,
        }} />
      </div>

      {/* sub-label below the wordmark */}
      <div style={{
             fontFamily: 'Share Tech Mono, JetBrains Mono, monospace',
             fontSize: '11px',
             letterSpacing: '0.18em',
             textTransform: 'uppercase',
             color: 'rgba(15,23,42,0.55)',
             marginTop: '34px',
             opacity: subOpacity,
           }}>
        NTU · Centre for Climate Change &amp; Environmental Health
      </div>
    </div>
  );
}

// ---- Location callout (PR 23) ----------------------------------------
// "SINGAPORE · 1°20'45" N · 103°49'12" E · 12.5 KM ATMOSPHERIC COLUMN"
// Fades in as the cold-open hero finishes (t≈3.5 s), holds through the
// atmosphere reveal beat (3.5–11.5 s), fades out before the orbit-rise
// spiral starts at t≈14 s. Names the place + binds the basemap to the
// hologram column narratively. Typography matches the top bar:
// IBM Plex Mono 500 11 px 0.18em uppercase, deep charcoal.
function LocationCallout({ time }) {
  // Round 24 — TWO phases: the hologram era (3.5–12 s) AND the Singapore
  // basemap reveal, so "SINGAPORE + coordinates" re-appears as the map resolves.
  // Round 33 — phase 2 now ALSO introduces the three Doppler-LiDAR stations
  // (NTU, Raffles Girls' School, Woodlands) in the SAME monospace HUD
  // typography as SINGAPORE, timed to the network-reveal voiceover (13–32 s)
  // while the basemap + station markers resolve.
  const inP1 = time >= 3.5 && time <= 12.0;
  const inP2 = time >= 14.0 && time <= 33.0;
  if (!inP1 && !inP2) return null;
  const opacity = inP1
    ? (time < 4.0  ? (time - 3.5) / 0.5 :
       time > 11.5 ? (12.0 - time) / 0.5 : 1.0)
    : (time < 15.0 ? (time - 14.0) / 1.0 :
       time > 32.5 ? (33.0 - time) / 0.5 : 1.0);
  // Round 23 — adaptive (white on the dark intro backdrop).
  const bg = (typeof window !== 'undefined' && window.__V5_BG_LERP__ != null)
    ? window.__V5_BG_LERP__ : 0;
  const dark = bg < 0.5;
  const c1 = dark ? 'rgba(255,255,255,0.96)' : 'rgba(26, 24, 20, 0.95)';
  const c2 = dark ? 'rgba(255,255,255,0.72)' : 'rgba(26, 24, 20, 0.72)';
  const c3 = dark ? 'rgba(255,255,255,0.50)' : 'rgba(26, 24, 20, 0.48)';
  const rule = dark ? 'rgba(255,255,255,0.18)' : 'rgba(26,24,20,0.16)';

  // The three Doppler-LiDAR stations — name + coordinates, introduced the
  // same way SINGAPORE is. Appear only in phase 2 (the network reveal).
  const STATIONS = [
    { name: 'NTU',                   coord: "1°20'53\" N · 103°41'02\" E" },
    { name: "Raffles Girls' School", coord: "1°20'28\" N · 103°50'24\" E" },
    { name: 'Woodlands',             coord: "1°26'13\" N · 103°47'10\" E" },
  ];

  return (
    <div style={{
      position: 'fixed',
      bottom: 'clamp(72px, 13vh, 140px)',
      left: 'clamp(24px, 4vw, 56px)',
      fontFamily: 'IBM Plex Mono, monospace',
      pointerEvents: 'none',
      zIndex: 30,
      opacity,
      transition: 'opacity 120ms linear',
    }}>
      <div style={{
        fontSize: '22px',
        fontWeight: 600,
        letterSpacing: '0.04em',
        textTransform: 'uppercase',
        color: c1,
        lineHeight: 1.1,
      }}>
        Singapore
      </div>
      <div style={{
        marginTop: '8px',
        fontSize: '11px',
        fontWeight: 500,
        letterSpacing: '0.18em',
        textTransform: 'uppercase',
        color: c2,
        lineHeight: 1.35,
      }}>
        1°20&prime;45&Prime; N · 103°49&prime;12&Prime; E
      </div>
      <div style={{
        marginTop: '4px',
        fontSize: '11px',
        fontWeight: 500,
        letterSpacing: '0.18em',
        textTransform: 'uppercase',
        color: c3,
        lineHeight: 1.35,
      }}>
        12.5 km atmospheric column
      </div>

      {/* Round 33 — the three Doppler-LiDAR stations, same HUD typography. */}
      {inP2 && (
        <div style={{
          marginTop: '16px',
          paddingTop: '13px',
          borderTop: `1px solid ${rule}`,
          maxWidth: '420px',
        }}>
          <div style={{
            fontSize: '12px',
            fontWeight: 700,
            letterSpacing: '0.2em',
            textTransform: 'uppercase',
            color: c2,
            lineHeight: 1.1,
            marginBottom: '12px',
          }}>
            Doppler LiDAR network · 3 stations
          </div>
          {STATIONS.map((s, i) => (
            <div key={s.name} style={{ marginTop: i === 0 ? 0 : '12px' }}>
              <div style={{
                fontSize: '16px',
                fontWeight: 700,
                letterSpacing: '0.05em',
                textTransform: 'uppercase',
                color: c1,
                lineHeight: 1.15,
              }}>
                {s.name}
              </div>
              <div style={{
                marginTop: '3px',
                fontSize: '12.5px',
                fontWeight: 600,
                letterSpacing: '0.1em',
                textTransform: 'uppercase',
                color: c2,
                lineHeight: 1.3,
              }}>
                {s.coord}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ---- Closing credit ---------------------------------------------------
// Round 36 — the outro re-materializes the 3D 3DREAMS@SG wordmark; beneath it,
// a single low-contrast credit line fades in. Tightly scoped to the outro
// window (207–220) so it can never bleed into earlier chapters (the bug that
// retired the old CTA in Round 26).
function CloseCTA({ time }) {
  // R42 — REMOVED. This centered outro credit duplicated the always-on
  // lower-right ".v5-watermark" brand/restriction mark. The single (now bigger)
  // corner watermark is the only persistent credit. Kept as a no-op so the
  // render tree / call site stays intact.
  return null;
  // eslint-disable-next-line no-unreachable
  if (time < 207 || time > 220) return null;
  const fadeIn  = time < 209   ? (time - 207) / 2.0   : 1;
  const fadeOut = time > 217   ? (220 - time) / 3.0   : 1;
  const opacity = Math.max(0, Math.min(1, fadeIn * fadeOut));
  return (
    <div
      className="absolute left-0 right-0 text-center"
      style={{
        bottom: 'clamp(40px, 6vh, 80px)',
        opacity,
        transition: 'opacity 120ms linear',
        fontFamily: 'JetBrains Mono, IBM Plex Mono, monospace',
        fontSize: 'clamp(10.5px, 1.05vw, 13px)',
        letterSpacing: '0.14em',
        color: 'rgba(255,255,255,0.62)',
        textShadow: '0 1px 12px rgba(2,6,16,0.6)',
        pointerEvents: 'none',
      }}
    >
      All rights reserved. Centre for Climate Change &amp; Environmental Health. 2026.
    </div>
  );
}

// ---- Take-control panel ----------------------------------------------
function TakeControlPanel({ api, takeControl }) {
  if (!takeControl) return null;
  return (
    <div className="absolute top-1/2 right-8 -translate-y-1/2 pointer-events-auto p-5 border"
         style={{
           borderColor: 'rgba(232,178,92,0.55)',
           background: 'rgba(255,252,242,0.85)',
           backdropFilter: 'blur(10px)',
           color: '#1a1814',
           fontFamily: 'IBM Plex Mono, monospace',
           minWidth: '240px',
         }}>
      <div className="text-[11px] tracking-[0.16em] uppercase mb-3" style={{ color: '#4D8BFF' }}>
        Take control · drag to orbit
      </div>
      <button onClick={() => api.setTakeControl(false)}
              className="block w-full text-left py-2 border-t border-b mb-3 text-[13px]"
              style={{ borderColor: 'rgba(26,24,20,0.12)' }}>
        Resume film →
      </button>
      <div className="text-[10px] tracking-[0.16em] uppercase mb-2"
           style={{ color: 'rgba(26,24,20,0.55)' }}>Jump to chapter</div>
      <div className="flex flex-col gap-1">
        {window.CHAPTER_MARKERS.map((m) => (
          <button key={m.name}
                  onClick={() => { api.setTakeControl(false); api.seek(m.t); api.play(); }}
                  className="text-left py-1 text-[12px] hover:text-amber-700">
            {m.name.padEnd(8)} · {m.short}
          </button>
        ))}
      </div>
    </div>
  );
}

// ---- Start gate -------------------------------------------------------
// Round 32 — one unmistakable, reliable way to START the film. The timeline
// is paused on load (no autoplay — browser audio policy needs a gesture), so
// this big centred Play target IS that gesture. It covers the whole stage
// until the film begins, so the cold-open animating at t=0 can never be
// mistaken for "already playing", and there is no small button to miss.
function StartOverlay({ api, time, playing }) {
  const started = playing || time > 0.05;
  if (!api || started) return null;
  const start = () => { try { api.play && api.play(); } catch (_) {} };
  return (
    <div
      className="absolute inset-0 flex items-center justify-center"
      style={{
        zIndex: 50,
        pointerEvents: 'auto',
        cursor: 'pointer',
        background: 'radial-gradient(circle at 50% 44%, rgba(7,11,24,0.30), rgba(7,11,24,0.58))',
      }}
      onClick={start}
      role="button"
      aria-label="Play the film"
    >
      <div className="flex flex-col items-center" style={{ pointerEvents: 'none', gap: '22px' }}>
        <div style={{
          width: '104px', height: '104px', borderRadius: '50%',
          background: 'rgba(77,139,255,0.18)',
          border: '2px solid rgba(255,255,255,0.88)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          boxShadow: '0 0 46px rgba(77,139,255,0.55), inset 0 0 24px rgba(77,139,255,0.25)',
        }}>
          <svg width="38" height="38" viewBox="0 0 24 24" style={{ marginLeft: '5px' }}>
            <path d="M5 3 L21 12 L5 21 Z" fill="rgba(255,255,255,0.96)" />
          </svg>
        </div>
        <div style={{
          color: 'rgba(255,255,255,0.94)', fontFamily: 'IBM Plex Mono, monospace',
          fontSize: '14px', letterSpacing: '0.26em', textTransform: 'uppercase',
          textShadow: '0 2px 12px rgba(0,0,0,0.7)',
        }}>
          Play the film
        </div>
      </div>
    </div>
  );
}

// ---- Persistent legal footer (R43) -----------------------------------
// Replaces the old corner ".v5-watermark". A single right-aligned line pinned
// to the very bottom-right; the colour adapts to the published backdrop
// (white on the dark intro/outro, near-black on the light dashboard frames) —
// driven by the same bgLerp signal the captions use.
function Footer() {
  const bg = (typeof window !== 'undefined' && window.__V5_BG_LERP__ != null)
    ? window.__V5_BG_LERP__ : 1;
  const dark = bg < 0.5;
  const color = dark ? 'rgba(255,255,255,0.82)' : 'rgba(21,19,15,0.80)';
  const textShadow = dark
    ? '0 1px 10px rgba(2,6,16,0.50)'
    : '0 1px 8px rgba(255,255,255,0.55)';
  return (
    <div
      className="absolute text-right"
      style={{
        bottom: 'clamp(9px, 1.0vw, 15px)',
        right: 'clamp(20px, 2.4vw, 40px)',
        maxWidth: '74vw',
        color,
        textShadow,
        fontFamily: 'IBM Plex Mono, monospace',
        fontSize: 'clamp(11px, 1.0vw, 14px)',
        letterSpacing: '0.07em',
        fontWeight: 500,
        transition: 'color 400ms linear, text-shadow 400ms linear',
        pointerEvents: 'none',
      }}
    >
      All rights reserved · Centre for Climate Change &amp; Environmental Health · 2026 · Not for redistribution
    </div>
  );
}

// ---- App --------------------------------------------------------------
function FilmOverlay() {
  const api = useFilm();
  const time = api?.time?.() ?? 0;
  const playing = api?.isPlaying?.() ?? false;
  // Round 19: prefer the canonical FILM_DURATION global (set in vo.js)
  // so the duration counter reads "3:00" even before FilmAPI mounts.
  const duration = api?.duration ?? (typeof window !== 'undefined' ? window.FILM_DURATION : null) ?? 180;
  const captionsOn = api?.captionsOn?.() ?? true;
  const muted = api?.isMuted?.() ?? false;
  const takeControl = api?.isTakeControl?.() ?? false;

  if (!api) {
    return (
      <div className="absolute inset-0 flex items-center justify-center pointer-events-none"
           style={{ background: '#0c0a08', color: '#4D8BFF', fontFamily: 'IBM Plex Mono, monospace' }}>
        <div className="text-[11px] tracking-[0.18em] uppercase">Preparing the room</div>
      </div>
    );
  }

  // R38 — export mode (?export=1): no player chrome in the baked master.
  // The Play gate would flash on frames 0–1 (started = time > 0.05), and a
  // burned-in scrubber/timecode doesn't belong in a video file. Captions,
  // the stations callout, and the outro credit are content — they stay.
  const exporting = (typeof window !== 'undefined') && !!window.__V5_EXPORT_MODE__;

  return (
    <div className="absolute inset-0 pointer-events-none">
      <ColdOpen time={time} />
      <LocationCallout time={time} />
      <Caption time={time} on={captionsOn} />
      {/* R43 — persistent right-aligned legal footer (replaces the corner watermark). */}
      <Footer />
      {!exporting && (
        <TopBar api={api} time={time} duration={duration}
                captionsOn={captionsOn} muted={muted} takeControl={takeControl} />
      )}
      {/* R43 — the bottom scrubber/slider was removed (cleaner bottom band; the
          captions now occupy it). Playback is driven by the TopBar play/pause. */}
      <TakeControlPanel api={api} takeControl={takeControl} />
      <CloseCTA time={time} />
      {!exporting && <StartOverlay api={api} time={time} playing={playing} />}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('overlay-root')).render(<FilmOverlay />);
