/* ============================================================
   RootBound — Animated Video · component library
   Built on animations.jsx (Stage/Sprite/Easing/interpolate).
   Coordinate space: the 1920x1080 Stage canvas. Fractional
   helpers map 0..1 -> px against W/H.
   ============================================================ */
const W = 1920, H = 1080;
const fx = (f) => f * W, fy = (f) => f * H;

const TOK = {
  paper: '#F9F5EC', paper2: '#F3ECDD',
  ink: '#003B4F', inkSoft: '#5C6163',
  deep: '#012A38', deep2: '#01202B',
  onDeep: '#F9F5EC', onDeepSoft: '#A8BFC6',
  gold: '#D4AF37', gold2: '#E0C04A', goldEm: '#B0871A', clay: '#8A6D18',
  teal: '#8FD0E0', tealEm: '#0A6E86',
  serif: "'Spectral', Georgia, serif",
  sans: "'Inter', system-ui, sans-serif",
  mono: "'IBM Plex Mono', ui-monospace, monospace",
  brand: "'RootBound Display','Spectral',Georgia,serif",
};
// accent resolves at render via window.__RBV_ACCENT ('gold'|'navy')
const ACC = () => (window.__RBV_ACCENT === 'navy'
  ? { on: TOK.teal, em: TOK.tealEm, glow: '143,208,224' }
  : { on: TOK.gold2, em: TOK.goldEm, glow: '224,192,74' });

/* ---------------- text-to-speech (male voice) ---------------- */
const RBVoice = (function () {
  const synth = window.speechSynthesis || null;
  let voice = null;
  function pick() {
    if (!synth) return;
    const vs = synth.getVoices(); if (!vs.length) return;
    const en = vs.filter(v => /^en/i.test(v.lang)); const pool = en.length ? en : vs;
    const pref = [
      'Microsoft Guy Online (Natural) - English (United States)',
      'Microsoft Christopher Online (Natural) - English (United States)',
      'Microsoft Andrew Online (Natural) - English (United States)',
      'Microsoft Roger Online (Natural) - English (United States)',
      'Google UK English Male', 'Daniel', 'Alex', 'Aaron', 'Rishi',
      'Microsoft David - English (United States)', 'Microsoft Mark - English (United States)',
    ];
    for (const nm of pref) { const v = pool.find(x => x.name === nm) || vs.find(x => x.name === nm); if (v) { voice = v; return; } }
    const male = /(\bmale\b|guy|david|mark|daniel|alex|fred|rishi|aaron|christopher|roger|andrew|george|paul|matthew|liam|ryan)/i;
    voice = pool.find(v => male.test(v.name)) || pool.find(v => /en[-_]US/i.test(v.lang)) || pool[0] || vs[0];
  }
  if (synth) { pick(); try { synth.addEventListener('voiceschanged', pick); } catch (e) { synth.onvoiceschanged = pick; } }
  return {
    speak(text, rate) {
      if (!synth || !text) return;
      try { synth.cancel(); } catch (e) {}
      const u = new SpeechSynthesisUtterance(text);
      if (voice) u.voice = voice;
      u.lang = 'en-US'; u.rate = Math.max(0.6, Math.min(1.3, (rate || 0.96))); u.pitch = 0.9;
      synth.speak(u);
    },
    cancel() { if (synth) { try { synth.cancel(); } catch (e) {} } },
  };
})();

/* ---------------- world background (Ken-Burns camera + cinematic grade) ---------------- */
function WorldBG({ src, from = 1.04, to = 1.12, fxr = 0.5, fyr = 0.45,
  dim = 0, tint = TOK.deep2, ease = Easing.easeInOutSine, vignette = 0.34, blur = 0,
  grade = true, rays = true, warm = 0.55, fade = true, fadeIn = 0.6, fadeOut = 0.55 }) {
  const { progress, localTime, duration } = useSprite();
  const z = from + (to - from) * ease(clamp(progress, 0, 1));
  const op = fade ? clamp(Math.min(localTime / fadeIn, (duration - localTime) / fadeOut), 0, 1) : 1;
  const imgFilter = `${blur ? `blur(${blur}px) ` : ''}${grade ? 'saturate(1.18) contrast(1.05) brightness(1.03)' : ''}`.trim() || 'none';
  return (
    <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', background: TOK.deep2, opacity: op }}>
      <img src={src} alt="" style={{
        position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover',
        transform: `scale(${z})`, transformOrigin: `${fxr * 100}% ${fyr * 100}%`,
        filter: imgFilter, willChange: 'transform',
      }} />
      {/* golden-hour grade: warm key from top, cool fill at base */}
      <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', mixBlendMode: 'soft-light', opacity: warm,
        background: 'linear-gradient(177deg, rgba(255,205,128,.62) 0%, rgba(255,236,200,.14) 28%, rgba(2,28,40,0) 56%, rgba(1,22,32,.55) 100%)' }} />
      {/* soft god-ray key light */}
      {rays && <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', mixBlendMode: 'screen',
        background: 'radial-gradient(58% 48% at 64% 0%, rgba(255,243,212,.30), rgba(255,243,212,0) 62%)' }} />}
      {dim > 0 && <div style={{ position: 'absolute', inset: 0, background: tint, opacity: dim }} />}
      {vignette > 0 && <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none',
        boxShadow: `inset 0 0 300px 110px rgba(1,17,25,${vignette}), inset 0 0 110px 26px rgba(1,17,25,${vignette * 0.55})` }} />}
    </div>
  );
}

/* ---------------- atmosphere: floating golden dust motes ---------------- */
function Particles({ count = 38, color = '255,238,196' }) {
  const items = React.useMemo(() => {
    const r = (s) => { const x = Math.sin(s * 91.7) * 9973; return x - Math.floor(x); };
    return Array.from({ length: count }, (_, i) => ({
      left: r(i + 1) * 100, size: 2 + r(i + 7) * 6.5, dur: 16 + r(i + 3) * 24,
      delay: -r(i + 5) * 40, op: 0.1 + r(i + 9) * 0.36, dx: (r(i + 11) - 0.5) * 90,
      blur: r(i + 13) > 0.55 ? 1.6 : 0.5,
    }));
  }, [count]);
  return (
    <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', overflow: 'hidden', zIndex: 6, mixBlendMode: 'screen' }}>
      <style>{`@keyframes rbvfloat{0%{transform:translateY(40px) translateX(0) scale(.7);opacity:0}12%{opacity:1}88%{opacity:1}100%{transform:translateY(-1140px) translateX(var(--dx)) scale(1);opacity:0}}`}</style>
      {items.map((p, i) => (
        <div key={i} style={{ position: 'absolute', left: p.left + '%', bottom: -12, width: p.size, height: p.size,
          borderRadius: '50%', background: `rgba(${color},${p.op})`, filter: `blur(${p.blur}px)`,
          boxShadow: `0 0 ${p.size * 2.4}px rgba(${color},${p.op * 0.9})`,
          '--dx': p.dx + 'px', animation: `rbvfloat ${p.dur}s linear ${p.delay}s infinite` }} />
      ))}
    </div>
  );
}

/* ---------------- caption bar (subtitle) ---------------- */
function CaptionBar({ captions, enabled = true }) {
  const time = useTime();
  if (!enabled) return null;
  let cur = null;
  for (const c of captions) { if (time >= c.t && time < c.t + (c.d || 6)) cur = c; }
  if (!cur) return null;
  const into = time - cur.t;
  const op = clamp(Math.min(into / 0.35, ((cur.d || 6) - into) / 0.4), 0, 1);
  return (
    <div style={{ position: 'absolute', left: 0, right: 0, bottom: fy(0.085), display: 'flex',
      justifyContent: 'center', padding: '0 12%', pointerEvents: 'none' }}>
      <div style={{
        fontFamily: TOK.sans, fontWeight: 500, fontSize: 34, lineHeight: 1.4, textAlign: 'center',
        color: '#FBF7EE', background: 'rgba(3,16,22,.66)', backdropFilter: 'blur(6px)',
        WebkitBackdropFilter: 'blur(6px)', padding: '14px 28px', borderRadius: 12, maxWidth: '74%',
        opacity: op, boxShadow: '0 8px 30px rgba(0,0,0,.4)', textWrap: 'pretty',
      }}>{cur.x}</div>
    </div>
  );
}

/* ---------------- voiceover controller ---------------- */
function VoiceController({ captions, enabled, rate = 0.96 }) {
  const { time, playing } = useTimeline();
  const lastIdx = React.useRef(-1);
  const lastT = React.useRef(0);
  React.useEffect(() => {
    if (!enabled) { RBVoice.cancel(); lastIdx.current = -1; lastT.current = time; return; }
    const jumped = Math.abs(time - lastT.current) > 0.6; // scrub/seek
    lastT.current = time;
    if (!playing) { RBVoice.cancel(); if (jumped) lastIdx.current = -1; return; }
    let idx = -1;
    for (let i = 0; i < captions.length; i++) { if (time >= captions[i].t) idx = i; else break; }
    if (jumped) { lastIdx.current = idx; return; } // don't re-speak on jump
    if (idx !== lastIdx.current && idx >= 0) {
      lastIdx.current = idx;
      RBVoice.speak(captions[idx].x, rate);
    }
  }, [time, playing, enabled]);
  return null;
}

/* ---------------- chapter bar (interactive) ---------------- */
function ChapterBar({ chapters }) {
  const { time, duration, setTime, setPlaying } = useTimeline();
  return (
    <div style={{ position: 'absolute', left: fx(0.5), bottom: fy(0.028), transform: 'translateX(-50%)',
      display: 'flex', gap: 8, alignItems: 'center', zIndex: 60,
      background: 'rgba(1,23,31,.6)', backdropFilter: 'blur(8px)', WebkitBackdropFilter: 'blur(8px)',
      border: '1px solid rgba(224,192,74,.25)', borderRadius: 100, padding: '8px 12px' }}>
      {chapters.map((c, i) => {
        const next = chapters[i + 1] ? chapters[i + 1].t : duration;
        const active = time >= c.t && time < next;
        return (
          <button key={i} onClick={() => { setTime(c.t + 0.001); setPlaying(true); }}
            title={c.label}
            style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer',
              background: active ? 'rgba(224,192,74,.16)' : 'transparent', border: 0,
              borderRadius: 100, padding: '7px 13px', transition: 'background .2s' }}>
            <span style={{ fontFamily: TOK.mono, fontSize: 13, fontWeight: 600, letterSpacing: '.04em',
              color: active ? ACC().on : 'rgba(168,191,198,.7)' }}>{String(i).padStart(2, '0')}</span>
            <span style={{ fontFamily: TOK.mono, fontSize: 13, letterSpacing: '.02em',
              color: active ? TOK.onDeep : 'rgba(168,191,198,.6)', whiteSpace: 'nowrap' }}>{c.label}</span>
          </button>
        );
      })}
    </div>
  );
}

/* ---------------- eyebrow / title helpers (sprite-aware) ---------------- */
function rise(localTime, dur, inDur = 0.5, outDur = 0.4, dist = 22) {
  const exitStart = Math.max(0, dur - outDur);
  if (localTime < inDur) { const t = Easing.easeOutCubic(clamp(localTime / inDur, 0, 1)); return { opacity: t, ty: (1 - t) * dist }; }
  if (localTime > exitStart) { const t = Easing.easeInCubic(clamp((localTime - exitStart) / outDur, 0, 1)); return { opacity: 1 - t, ty: -t * 10 }; }
  return { opacity: 1, ty: 0 };
}

function Eyebrow({ children, x = fx(0.5), y, align = 'center', color, delay = 0 }) {
  const { localTime, duration } = useSprite();
  const { opacity, ty } = rise(localTime - delay, duration - delay);
  return (
    <div style={{ position: 'absolute', left: x, top: y, transform: `translate(${align === 'center' ? '-50%' : '0'}, ${ty}px)`,
      opacity, fontFamily: TOK.mono, fontSize: 21, fontWeight: 600, letterSpacing: '.22em', textTransform: 'uppercase',
      color: color || ACC().on, display: 'flex', alignItems: 'center', gap: 14, whiteSpace: 'nowrap' }}>
      <span style={{ width: 10, height: 10, background: 'currentColor', transform: 'rotate(45deg)', display: 'inline-block' }} />
      {children}
    </div>
  );
}

function Title({ children, x = fx(0.5), y, align = 'center', size = 96, color = TOK.onDeep, delay = 0, max = 18, font = TOK.brand, shadow = true }) {
  const { localTime, duration } = useSprite();
  const { opacity, ty } = rise(localTime - delay, duration - delay, 0.6, 0.45, 28);
  return (
    <div style={{ position: 'absolute', left: x, top: y, transform: `translate(${align === 'center' ? '-50%' : '0'}, ${ty}px)`,
      opacity, fontFamily: font, fontWeight: 500, fontSize: size, lineHeight: 1.02, letterSpacing: '-0.02em',
      color, textAlign: align, maxWidth: `${max}ch`, ...(align === 'center' ? { width: 'max-content' } : {}),
      textShadow: shadow ? '0 4px 30px rgba(1,28,38,.55)' : 'none' }}>
      {children}
    </div>
  );
}

function Lead({ children, x = fx(0.5), y, align = 'center', size = 30, color = TOK.onDeepSoft, delay = 0.2, max = 60 }) {
  const { localTime, duration } = useSprite();
  const { opacity, ty } = rise(localTime - delay, duration - delay, 0.55, 0.4, 18);
  return (
    <div style={{ position: 'absolute', left: x, top: y, transform: `translate(${align === 'center' ? '-50%' : '0'}, ${ty}px)`,
      opacity, fontFamily: TOK.sans, fontWeight: 400, fontSize: size, lineHeight: 1.5, color,
      textAlign: align, maxWidth: `${max}ch`, textShadow: '0 2px 18px rgba(1,28,38,.5)' }}>
      {children}
    </div>
  );
}

/* ---------------- map overlays: building tag, pulse, beam ---------------- */
function Pulse({ xr, yr, at = 0, color, size = 360 }) {
  const { localTime } = useSprite();
  const c = color || ACC().glow;
  const reps = 3;
  const rings = [];
  for (let i = 0; i < reps; i++) {
    const t = ((localTime - at) - i * 0.9);
    if (t < 0) continue;
    const p = (t % 2.7) / 2.7;
    rings.push(
      <div key={i} style={{ position: 'absolute', left: fx(xr), top: fy(yr), width: size, height: size,
        marginLeft: -size / 2, marginTop: -size / 2, borderRadius: '50%',
        border: `2px solid rgba(${c},${(1 - p) * 0.5})`, transform: `scale(${0.2 + p * 1})`, pointerEvents: 'none' }} />
    );
  }
  return <>{rings}</>;
}

function Beam({ x1r, y1r, x2r, y2r, at = 0, dur = 1.1, color }) {
  const { localTime } = useSprite();
  const c = color || ACC().glow;
  const t = clamp((localTime - at) / dur, 0, 1);
  const e = Easing.easeOutCubic(t);
  const X1 = fx(x1r), Y1 = fy(y1r), X2 = fx(x2r), Y2 = fy(y2r);
  const len = Math.hypot(X2 - X1, Y2 - Y1);
  const ang = Math.atan2(Y2 - Y1, X2 - X1) * 180 / Math.PI;
  const dotX = X1 + (X2 - X1) * e, dotY = Y1 + (Y2 - Y1) * e;
  const lineOp = t > 0 ? clamp(1 - (localTime - at - dur) / 2.5, 0.15, 0.55) : 0;
  return (
    <>
      <div style={{ position: 'absolute', left: X1, top: Y1, width: len * e, height: 3, transformOrigin: '0 50%',
        transform: `rotate(${ang}deg)`, background: `linear-gradient(90deg, rgba(${c},0), rgba(${c},${lineOp}))`,
        borderRadius: 3, pointerEvents: 'none', filter: 'blur(.4px)' }} />
      {t < 1 && t > 0 && (
        <div style={{ position: 'absolute', left: dotX, top: dotY, width: 16, height: 16, marginLeft: -8, marginTop: -8,
          borderRadius: '50%', background: `rgba(${c},1)`, boxShadow: `0 0 18px 6px rgba(${c},.8)`, pointerEvents: 'none' }} />
      )}
    </>
  );
}

// Building tag: appears, hoverable highlight. xr/yr fractional anchor.
function Tag({ xr, yr, label, sub, at = 0, side = 'top' }) {
  const { localTime } = useSprite();
  const [hover, setHover] = React.useState(false);
  const t = clamp((localTime - at) / 0.5, 0, 1);
  const e = Easing.easeOutBack(t);
  const acc = ACC();
  const dy = side === 'top' ? -1 : 1;
  return (
    <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{ position: 'absolute', left: fx(xr), top: fy(yr), transform: `translate(-50%, ${side === 'top' ? '-100%' : '0'})`,
        opacity: t, pointerEvents: 'auto' }}>
      <div style={{ transform: `scale(${e}) translateY(${(1 - e) * 8 * dy}px)`, transformOrigin: 'center bottom',
        display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
        <div style={{ background: hover ? `rgba(${acc.glow},.95)` : 'rgba(1,28,38,.82)',
          color: hover ? TOK.deep : TOK.onDeep, border: `1.5px solid rgba(${acc.glow},${hover ? 1 : .55})`,
          borderRadius: 8, padding: '8px 14px', backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)',
          boxShadow: hover ? `0 8px 26px rgba(${acc.glow},.4)` : '0 6px 20px rgba(0,0,0,.4)', transition: 'all .2s',
          whiteSpace: 'nowrap', textAlign: 'center' }}>
          <div style={{ fontFamily: TOK.mono, fontSize: 17, fontWeight: 600, letterSpacing: '.06em' }}>{label}</div>
          {sub && <div style={{ fontFamily: TOK.sans, fontSize: 13, marginTop: 2,
            color: hover ? 'rgba(1,28,38,.7)' : TOK.onDeepSoft }}>{sub}</div>}
        </div>
        <div style={{ width: 2, height: 18, background: `rgba(${acc.glow},.7)` }} />
        <div style={{ width: 11, height: 11, borderRadius: '50%', background: `rgba(${acc.glow},1)`,
          marginTop: -8, boxShadow: `0 0 14px 4px rgba(${acc.glow},.7)` }} />
      </div>
    </div>
  );
}

/* ---------------- stat counter ---------------- */
function Stat({ xr, yr, to, suffix = '', unit, label, at = 0, big = 72 }) {
  const { localTime } = useSprite();
  const t = clamp((localTime - at) / 1.1, 0, 1);
  const e = Easing.easeOutCubic(t);
  const val = Math.round(to * e);
  const op = clamp((localTime - at) / 0.4, 0, 1);
  return (
    <div style={{ position: 'absolute', left: fx(xr), top: fy(yr), transform: 'translate(-50%,0)', opacity: op, textAlign: 'center' }}>
      <div style={{ fontFamily: TOK.serif, fontWeight: 600, fontSize: big, lineHeight: 1, color: TOK.onDeep, letterSpacing: '-0.02em', textShadow: '0 2px 20px rgba(1,28,38,.78), 0 0 6px rgba(1,28,38,.6)' }}>
        {val}{suffix}{unit && <span style={{ fontSize: big * 0.5, color: ACC().on }}>{unit}</span>}
      </div>
      <div style={{ fontFamily: TOK.sans, fontWeight: 500, fontSize: 18, color: TOK.onDeep, marginTop: 10, maxWidth: 220, marginInline: 'auto', lineHeight: 1.35, textShadow: '0 1px 12px rgba(1,28,38,.9), 0 0 4px rgba(1,28,38,.8)' }}>{label}</div>
    </div>
  );
}

/* ---------------- glass panel ---------------- */
function Panel({ x, y, w, children, at = 0, align = 'left' }) {
  const { localTime } = useSprite();
  const t = clamp((localTime - at) / 0.5, 0, 1);
  const e = Easing.easeOutCubic(t);
  return (
    <div style={{ position: 'absolute', left: x, top: y, width: w, transform: `translate(${align === 'center' ? '-50%' : '0'}, ${(1 - e) * 18}px)`,
      opacity: t, background: 'rgba(1,28,38,.66)', backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)',
      border: '1px solid rgba(224,192,74,.22)', borderRadius: 18, padding: 30, boxShadow: '0 20px 60px rgba(0,0,0,.4)' }}>
      {children}
    </div>
  );
}

/* ---------------- cinematic letterbox bars ---------------- */
function Letterbox({ h = 26 }) {
  return (
    <>
      <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: h, background: '#000', zIndex: 8, pointerEvents: 'none' }} />
      <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: h, background: '#000', zIndex: 8, pointerEvents: 'none' }} />
    </>
  );
}

Object.assign(window, {
  W, H, fx, fy, TOK, ACC, RBVoice,
  WorldBG, Particles, Letterbox, CaptionBar, VoiceController, ChapterBar,
  Eyebrow, Title, Lead, Pulse, Beam, Tag, Stat, Panel, rise,
});
