// GemDish — Map Search story video. Self-contained timeline engine + scenes.
// Registers window.GemDishVideo (a React component). Mount via <x-import>.

// ───────────────────────── engine ─────────────────────────
const Easing = {
  linear: (t) => t,
  easeInQuad: (t) => t * t,
  easeOutQuad: (t) => t * (2 - t),
  easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
  easeOutCubic: (t) => (--t) * t * t + 1,
  easeInCubic: (t) => t * t * t,
  easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1),
  easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
  easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
  easeOutBack: (t) => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); },
  easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
};
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
function interpolate(input, output, ease = Easing.linear) {
  return (t) => {
    if (t <= input[0]) return output[0];
    if (t >= input[input.length - 1]) return output[output.length - 1];
    for (let i = 0; i < input.length - 1; i++) {
      if (t >= input[i] && t <= input[i + 1]) {
        const span = input[i + 1] - input[i];
        const local = span === 0 ? 0 : (t - input[i]) / span;
        const ef = Array.isArray(ease) ? (ease[i] || Easing.linear) : ease;
        return output[i] + (output[i + 1] - output[i]) * ef(local);
      }
    }
    return output[output.length - 1];
  };
}
const lerp = (a, b, t) => a + (b - a) * clamp(t, 0, 1);
// smooth ramp 0->1 across [s,e]
const ramp = (t, s, e, ease = Easing.easeInOutCubic) => ease(clamp((t - s) / (e - s), 0, 1));

const TimelineContext = React.createContext({ time: 0, duration: 10, playing: false });
const useTime = () => React.useContext(TimelineContext).time;
const useTimeline = () => React.useContext(TimelineContext);

function IconButton({ children, onClick, title }) {
  const [h, setH] = React.useState(false);
  return (
    <button onClick={onClick} title={title} onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)}
      style={{ width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
        background: h ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.1)',
        borderRadius: 6, color: '#f6f4ef', cursor: 'pointer', padding: 0 }}>{children}</button>
  );
}
function PlaybackBar({ time, duration, playing, onPlayPause, onReset, onSeek, onHover }) {
  const trackRef = React.useRef(null);
  const [dragging, setDragging] = React.useState(false);
  const timeFromEvent = React.useCallback((e) => {
    const r = trackRef.current.getBoundingClientRect();
    return clamp((e.clientX - r.left) / r.width, 0, 1) * duration;
  }, [duration]);
  React.useEffect(() => {
    if (!dragging) return;
    const up = () => setDragging(false);
    const mv = (e) => { if (trackRef.current) onSeek(timeFromEvent(e)); };
    window.addEventListener('mouseup', up); window.addEventListener('mousemove', mv);
    return () => { window.removeEventListener('mouseup', up); window.removeEventListener('mousemove', mv); };
  }, [dragging, timeFromEvent, onSeek]);
  const pct = duration > 0 ? (time / duration) * 100 : 0;
  const fmt = (t) => { const m = Math.floor(t / 60), s = Math.floor(t % 60), cs = Math.floor((t * 100) % 100);
    return `${m}:${String(s).padStart(2, '0')}.${String(cs).padStart(2, '0')}`; };
  const mono = 'JetBrains Mono, ui-monospace, monospace';
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '8px 16px', background: 'rgba(20,20,20,0.92)',
      borderTop: '1px solid rgba(255,255,255,0.08)', width: '100%', maxWidth: 680, alignSelf: 'center', borderRadius: 8,
      color: '#f6f4ef', fontFamily: 'Inter, system-ui, sans-serif', userSelect: 'none', flexShrink: 0 }}>
      <IconButton onClick={onReset} title="Restart (0)"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3 2v10M12 2L5 7l7 5V2z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" /></svg></IconButton>
      <IconButton onClick={onPlayPause} title="Play/pause (space)">{playing ? (<svg width="14" height="14" viewBox="0 0 14 14"><rect x="3" y="2" width="3" height="10" fill="currentColor" /><rect x="8" y="2" width="3" height="10" fill="currentColor" /></svg>) : (<svg width="14" height="14" viewBox="0 0 14 14"><path d="M3 2l9 5-9 5V2z" fill="currentColor" /></svg>)}</IconButton>
      <div style={{ fontFamily: mono, fontSize: 12, width: 64, textAlign: 'right' }}>{fmt(time)}</div>
      <div ref={trackRef} onMouseMove={(e) => { if (dragging) onSeek(timeFromEvent(e)); else onHover(timeFromEvent(e)); }} onMouseLeave={() => { if (!dragging) onHover(null); }} onMouseDown={(e) => { setDragging(true); onSeek(timeFromEvent(e)); }}
        style={{ flex: 1, height: 22, position: 'relative', cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
        <div style={{ position: 'absolute', left: 0, right: 0, height: 4, background: 'rgba(255,255,255,0.12)', borderRadius: 2 }} />
        <div style={{ position: 'absolute', left: 0, width: `${pct}%`, height: 4, background: '#84CAF5', borderRadius: 2 }} />
        <div style={{ position: 'absolute', left: `${pct}%`, top: '50%', width: 12, height: 12, marginLeft: -6, marginTop: -6, background: '#fff', borderRadius: 6, boxShadow: '0 2px 4px rgba(0,0,0,0.4)' }} />
      </div>
      <div style={{ fontFamily: mono, fontSize: 12, width: 64, color: 'rgba(246,244,239,0.55)' }}>{fmt(duration)}</div>
    </div>
  );
}
function Stage({ width, height, duration, background = '#000', minimal = false, children }) {
  const persistKey = 'gemdishvid';
  const [time, setTime] = React.useState(() => { try { const v = parseFloat(localStorage.getItem(persistKey + ':t') || '0'); return isFinite(v) ? clamp(v, 0, duration) : 0; } catch { return 0; } });
  const [playing, setPlaying] = React.useState(true);
  const [hoverTime, setHoverTime] = React.useState(null);
  const [scale, setScale] = React.useState(1);
  const stageRef = React.useRef(null); const rafRef = React.useRef(null); const lastRef = React.useRef(null);
  React.useEffect(() => { try { localStorage.setItem(persistKey + ':t', String(time)); } catch {} }, [time]);
  React.useEffect(() => {
    if (!stageRef.current) return; const el = stageRef.current;
    const measure = () => { const barH = minimal ? 0 : 52; setScale(Math.max(0.05, Math.min(el.clientWidth / width, (el.clientHeight - barH) / height))); };
    measure(); const ro = new ResizeObserver(measure); ro.observe(el); window.addEventListener('resize', measure);
    return () => { ro.disconnect(); window.removeEventListener('resize', measure); };
  }, [width, height, minimal]);
  React.useEffect(() => {
    if (!playing) { lastRef.current = null; return; }
    const step = (ts) => { if (lastRef.current == null) lastRef.current = ts; const dt = (ts - lastRef.current) / 1000; lastRef.current = ts;
      setTime((t) => { let n = t + dt; if (n >= duration) n = n % duration; return n; }); rafRef.current = requestAnimationFrame(step); };
    rafRef.current = requestAnimationFrame(step);
    return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); lastRef.current = null; };
  }, [playing, duration]);
  React.useEffect(() => {
    if (minimal) return;
    const onKey = (e) => { if (e.code === 'Space') { e.preventDefault(); setPlaying(p => !p); } else if (e.code === 'ArrowLeft') setTime(t => clamp(t - (e.shiftKey ? 1 : 0.1), 0, duration)); else if (e.code === 'ArrowRight') setTime(t => clamp(t + (e.shiftKey ? 1 : 0.1), 0, duration)); else if (e.key === '0') setTime(0); };
    window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey);
  }, [duration, minimal]);
  React.useEffect(() => { window.__gemSeek = (v) => { setTime(clamp(v, 0, duration)); setPlaying(false); }; window.__gemPlay = () => setPlaying(true); }, [duration]);
  const displayTime = hoverTime != null ? hoverTime : time;
  const ctx = React.useMemo(() => ({ time: displayTime, duration, playing }), [displayTime, duration, playing]);
  return (
    <div ref={stageRef} style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', background: '#0a0a0a', fontFamily: 'Inter, system-ui, sans-serif' }}>
      <div style={{ flex: 1, width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', minHeight: 0 }}>
        <div style={{ width, height, background, position: 'relative', transform: `scale(${scale})`, transformOrigin: 'center', flexShrink: 0, boxShadow: '0 20px 60px rgba(0,0,0,0.4)', overflow: 'hidden' }}>
          <TimelineContext.Provider value={ctx}>{children}</TimelineContext.Provider>
        </div>
      </div>
      {!minimal && <PlaybackBar time={displayTime} duration={duration} playing={playing} onPlayPause={() => setPlaying(p => !p)} onReset={() => setTime(0)} onSeek={setTime} onHover={setHoverTime} />}
    </div>
  );
}

// ───────────────────────── data ─────────────────────────
const VW = 1080, VH = 1920;
const WORLD = { w: 1720, h: 2560 };
const PINS = [
  { id: 'viacarota', x: 1180, y: 560, r: '9.5', name: 'Via Carota', spicy: false },
  { id: 'ippudo', x: 620, y: 760, r: '9.2', name: 'Ippudo NY', spicy: false },
  { id: 'sugarfish', x: 1320, y: 1180, r: '9.0', name: 'Sugarfish', spicy: false },
  { id: 'sichuan', x: 560, y: 1360, r: '9.1', name: 'Sichuan Impression', spicy: true },
  { id: 'elvilsito', x: 1020, y: 1560, r: '8.7', name: 'El Vilsito', spicy: true },
  { id: 'bluebottle', x: 360, y: 1820, r: '8.2', name: 'Blue Bottle', spicy: false },
  { id: 'hothoney', x: 840, y: 1980, r: '8.9', name: 'Hot Honey', spicy: true },
];
const TARGET = 'sichuan';

// ───────────────────────── map world ─────────────────────────
function MapWorld({ t, tx, ty, z }) {
  const filtered = t >= 16.05; // highlight after "show on map"
  const reveal = ramp(t, 16.05, 16.6); // 0->1 fade of dim
  return (
    <div style={{ position: 'absolute', left: 0, top: 0, width: WORLD.w, height: WORLD.h, transform: `translate(${tx}px, ${ty}px) scale(${z})`, transformOrigin: '0 0' }}>
      {/* base */}
      <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(135deg,#e3eef5,#d6e8de)' }} />
      {/* parks */}
      <div style={{ position: 'absolute', left: -120, top: 200, width: 760, height: 620, background: '#cfe6d0', borderRadius: '46% 54% 60% 40%', opacity: .85 }} />
      <div style={{ position: 'absolute', right: -160, top: 980, width: 720, height: 700, background: '#cfe6d0', borderRadius: '52% 48% 40% 60%', opacity: .8 }} />
      <div style={{ position: 'absolute', left: 120, top: 1880, width: 640, height: 560, background: '#cfe6d0', borderRadius: '50% 50% 44% 56%', opacity: .8 }} />
      {/* water */}
      <div style={{ position: 'absolute', top: 0, bottom: 0, left: 1180, width: 120, background: '#bcd9ef', transform: 'skewX(-10deg)', opacity: .9 }} />
      {/* street grid */}
      <div style={{ position: 'absolute', inset: 0, background: 'repeating-linear-gradient(0deg,rgba(255,255,255,.6) 0 4px,transparent 4px 150px),repeating-linear-gradient(90deg,rgba(255,255,255,.6) 0 4px,transparent 4px 134px)' }} />
      {/* avenues */}
      <div style={{ position: 'absolute', top: 0, bottom: 0, left: 700, width: 12, background: 'rgba(255,255,255,.85)' }} />
      <div style={{ position: 'absolute', left: 0, right: 0, top: 1280, height: 12, background: 'rgba(255,255,255,.85)' }} />
      <div style={{ position: 'absolute', top: 0, bottom: 0, left: 1080, width: 8, background: 'rgba(255,255,255,.7)' }} />
      {/* pins */}
      {PINS.map((p) => {
        const isTarget = p.id === TARGET;
        const hi = filtered && p.spicy;
        const dim = filtered && !p.spicy ? lerp(1, 0.28, reveal) : 1;
        const selected = isTarget && t >= 16.6;
        const bg = selected ? '#3A8CC4' : hi ? '#F5C842' : '#84CAF5';
        const txt = hi ? '#1A2B3C' : '#fff';
        const pulse = hi && !selected ? (1 + 0.12 * Math.sin((t - 16) * 5)) : 1;
        const popScale = selected ? lerp(1, 1.18, ramp(t, 16.6, 17.2, Easing.easeOutBack)) : 1;
        return (
          <div key={p.id} style={{ position: 'absolute', left: p.x, top: p.y, transform: `translate(-50%,-100%) scale(${pulse * popScale})`, opacity: dim, zIndex: selected ? 9 : hi ? 7 : 3, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
            <div style={{ background: bg, color: txt, fontSize: 26, fontWeight: 800, borderRadius: 11, padding: '5px 13px', boxShadow: '0 5px 14px rgba(0,0,0,.25)', whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' }}>{p.r}</div>
            <div style={{ width: 18, height: 18, borderRadius: '50%', background: bg, border: '4px solid #fff', marginTop: -2, boxShadow: '0 2px 5px rgba(0,0,0,.3)' }} />
            {(hi || selected) && (<div style={{ marginTop: 5, background: 'rgba(255,255,255,.95)', borderRadius: 6, padding: '3px 9px', fontSize: 18, fontWeight: 700, color: '#1A2B3C', whiteSpace: 'nowrap', boxShadow: '0 3px 8px rgba(0,0,0,.15)' }}>{p.name}</div>)}
          </div>
        );
      })}
    </div>
  );
}

// fixed map chrome (search bar, segmented toggle, FAB, tab bar)
function MapChrome({ t }) {
  const fabPulse = 1 + 0.06 * Math.sin(t * 4);
  return (
    <React.Fragment>
      {/* status bar */}
      <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 64, display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', padding: '0 40px 8px', zIndex: 20 }}>
        <span style={{ fontSize: 26, fontWeight: 700, color: '#1A2B3C' }}>9:41</span>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ fontSize: 20, fontWeight: 800, color: '#1A2B3C' }}>5G</span><div style={{ width: 40, height: 19, border: '2.5px solid #1A2B3C', borderRadius: 5, padding: 2 }}><div style={{ height: '100%', width: '72%', background: '#1A2B3C', borderRadius: 2 }} /></div></div>
      </div>
      {/* search + filter row */}
      <div style={{ position: 'absolute', top: 80, left: 28, right: 28, display: 'flex', gap: 14, alignItems: 'center', zIndex: 20 }}>
        <div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 14, background: '#fff', borderRadius: 999, padding: '20px 24px', boxShadow: '0 4px 14px rgba(0,0,0,.14)' }}>
          <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#8AAAC0" strokeWidth="2" strokeLinecap="round"><circle cx="11" cy="11" r="7" /><path d="M21 21l-4-4" /></svg>
          <span style={{ fontSize: 25, color: '#8AAAC0' }}>Search restaurants…</span>
        </div>
        <div style={{ width: 64, height: 64, borderRadius: '50%', background: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 4px 14px rgba(0,0,0,.14)' }}><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#5A7A94" strokeWidth="2" strokeLinecap="round"><path d="M4 6h16M4 12h16M4 18h16" /></svg></div>
      </div>
      {/* segmented */}
      <div style={{ position: 'absolute', top: 168, left: 0, right: 0, display: 'flex', justifyContent: 'center', zIndex: 20 }}>
        <div style={{ display: 'flex', background: 'rgba(255,255,255,.95)', borderRadius: 999, padding: 5, boxShadow: '0 3px 10px rgba(0,0,0,.12)' }}>
          <div style={{ padding: '11px 30px', borderRadius: 999, background: '#84CAF5', fontSize: 24, fontWeight: 700, color: '#5BAEE0' }}>Everyone</div>
          <div style={{ padding: '11px 30px', borderRadius: 999, fontSize: 24, fontWeight: 600, color: '#5A7A94' }}>Friends</div>
        </div>
      </div>
      {/* FAB */}
      <div style={{ position: 'absolute', right: 36, bottom: 188, width: 96, height: 96, borderRadius: '50%', background: '#84CAF5', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 21, boxShadow: '0 8px 22px rgba(132,202,245,.6)', transform: `scale(${fabPulse})` }}>
        <svg width="44" height="44" viewBox="0 0 24 24" fill="#5BAEE0"><path d="M12 2l1.6 4.8L18 8l-4.4 1.5L12 14l-1.6-4.5L6 8l4.4-1.2zM19 13l.9 2.6L22 16l-2.1.7L19 19l-.9-2.3L16 16l2.1-.4zM5 14l.7 2L8 16.6l-2.3.6L5 19l-.7-1.8L2 16.6 4.3 16z" /></svg>
      </div>
      {/* tab bar */}
      <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 108, background: '#F0F8FF', borderTop: '1px solid #C8DFF0', display: 'flex', alignItems: 'center', justifyContent: 'space-around', zIndex: 20 }}>
        <svg width="46" height="46" viewBox="0 0 24 24" fill="none" stroke="#8AAAC0" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 2v7a3 3 0 0 0 6 0V2M6 9v13M21 15V2a5 5 0 0 0-3 5v6c0 1.1.9 2 2 2h1zm0 0v7" /></svg>
        <div style={{ width: 96, height: 96, borderRadius: '50%', background: '#84CAF5', display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: -38, boxShadow: '0 8px 18px rgba(132,202,245,.55)' }}><svg width="50" height="50" viewBox="0 0 24 24" fill="none" stroke="#5BAEE0" strokeWidth="2.6" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg></div>
        <svg width="46" height="46" viewBox="0 0 24 24" fill="none" stroke="#84CAF5" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 3 3 6v15l6-3 6 3 6-3V3l-6 3-6-3zM9 3v15M15 6v15" /></svg>
      </div>
      <div style={{ position: 'absolute', bottom: 12, left: '50%', transform: 'translateX(-50%)', width: 200, height: 8, borderRadius: 4, background: 'rgba(26,43,60,.3)', zIndex: 22 }} />
    </React.Fragment>
  );
}

// ───────────────────────── pin preview card ─────────────────────────
function PinCard({ t }) {
  const inT = ramp(t, 16.9, 17.5, Easing.easeOutCubic);
  const yOff = lerp(560, 0, inT);
  const r = PINS.find((p) => p.id === TARGET);
  const reviews = [
    { i: 'W', u: 'wei.l', r: '9.3', c: 'Mala dry pot is a religious experience.' },
    { i: 'P', u: 'priya.r', r: '8.9', c: 'Authentic málà tingle. Late kitchen saves lives.' },
  ];
  return (
    <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, zIndex: 30, background: '#F0F8FF', borderRadius: '34px 34px 0 0', padding: '14px 30px 130px', boxShadow: '0 -12px 40px rgba(0,0,0,.22)', transform: `translateY(${yOff}px)`, opacity: inT }}>
      <div style={{ width: 72, height: 7, borderRadius: 4, background: '#C8DFF0', margin: '10px auto 18px' }} />
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
        <div>
          <div style={{ fontSize: 38, fontWeight: 800, color: '#1A2B3C', letterSpacing: '-.5px' }}>Sichuan Impression</div>
          <div style={{ marginTop: 10, display: 'inline-block', background: '#E1F0FC', borderRadius: 999, padding: '6px 18px', fontSize: 22, fontWeight: 700, color: '#5BAEE0' }}>Sichuan · 🌶️ Spicy</div>
        </div>
      </div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, margin: '20px 0 6px' }}>
        <span style={{ fontSize: 58, fontWeight: 900, color: '#84CAF5', lineHeight: 1 }}>9.1</span>
        <span style={{ fontSize: 26, color: '#8AAAC0', fontWeight: 600 }}>/10</span>
        <span style={{ fontSize: 23, color: '#8AAAC0' }}>· 212 reviews</span>
        <span style={{ fontSize: 22, fontWeight: 700, color: '#4CAF82', marginLeft: 4 }}>· open til 1am</span>
      </div>
      <div style={{ borderTop: '1px solid #C8DFF0', paddingTop: 18, marginTop: 14, display: 'flex', flexDirection: 'column', gap: 16 }}>
        {reviews.map((rv) => (
          <div key={rv.u} style={{ display: 'flex', alignItems: 'flex-start', gap: 14 }}>
            <div style={{ width: 52, height: 52, borderRadius: '50%', flex: 'none', background: '#84CAF5', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 22, fontWeight: 800, color: '#5BAEE0' }}>{rv.i}</div>
            <div style={{ flex: 1 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                <span style={{ fontSize: 23, fontWeight: 700, color: '#1A2B3C' }}>@{rv.u}</span>
                <span style={{ background: '#84CAF5', borderRadius: 7, padding: '2px 9px', fontSize: 20, fontWeight: 800, color: '#fff' }}>{rv.r}</span>
              </div>
              <div style={{ fontSize: 23, color: '#5A7A94', marginTop: 4 }}>{rv.c}</div>
            </div>
          </div>
        ))}
      </div>
      <div style={{ marginTop: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14, background: '#84CAF5', borderRadius: 999, padding: 22, boxShadow: '0 8px 20px rgba(132,202,245,.4)' }}>
        <span style={{ fontSize: 26, fontWeight: 800, color: '#5BAEE0' }}>View Full Profile</span>
        <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#5BAEE0" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
      </div>
    </div>
  );
}

// ───────────────────────── AI chat overlay ─────────────────────────
function ChatOverlay({ t }) {
  // window 7.6 .. 16.3
  if (t < 7.5 || t > 16.4) return null;
  const slideIn = ramp(t, 7.6, 8.05, Easing.easeOutCubic);
  const slideOut = ramp(t, 16.0, 16.35, Easing.easeInCubic);
  const yOff = lerp(VH, 0, slideIn) + lerp(0, VH, slideOut);

  const tSend = 10.5;
  const showEmpty = t < tSend;
  const emptyOp = (1 - ramp(t, tSend - 0.25, tSend)) * ramp(t, 8.0, 8.4);
  const q = 'Something spicy, open late';
  const qShown = t < 9.0 ? '' : t > 10.3 ? q : q.slice(0, Math.round(((t - 9.0) / 1.3) * q.length));
  const caret = Math.floor(t * 2) % 2 === 0;

  const userIn = ramp(t, tSend, tSend + 0.3, Easing.easeOutBack);
  const thinking = t >= tSend + 0.2 && t < 12.0;
  const aStart = 12.0, aEnd = 13.9;
  const aFull = 'Found 3 spicy spots open late:\n\n🌶️  Sichuan Impression — 9.1 · 1.2mi · til 1am\n🌮  El Vilsito — 8.7 · 0.8mi · til 3am\n🍗  Hot Honey — 8.9 · 1.5mi · til 2am';
  const aShown = t < aStart ? '' : t > aEnd ? aFull : aFull.slice(0, Math.round(((t - aStart) / (aEnd - aStart)) * aFull.length));
  const showA = t >= aStart;
  const btnIn = ramp(t, 14.3, 14.7, Easing.easeOutBack);

  const sendActive = qShown.length > 0;
  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 40, background: '#F0F8FF', display: 'flex', flexDirection: 'column', transform: `translateY(${yOff}px)` }}>
      {/* status */}
      <div style={{ height: 64, flex: 'none', display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', padding: '0 40px 8px' }}>
        <span style={{ fontSize: 26, fontWeight: 700, color: '#1A2B3C' }}>9:41</span>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ fontSize: 20, fontWeight: 800, color: '#1A2B3C' }}>5G</span><div style={{ width: 40, height: 19, border: '2.5px solid #1A2B3C', borderRadius: 5, padding: 2 }}><div style={{ height: '100%', width: '72%', background: '#1A2B3C', borderRadius: 2 }} /></div></div>
      </div>
      {/* header */}
      <div style={{ flex: 'none', display: 'flex', alignItems: 'center', gap: 18, padding: '18px 28px', borderBottom: '1px solid #C8DFF0' }}>
        <div style={{ width: 64, height: 64, borderRadius: '50%', background: '#84CAF5', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><svg width="32" height="32" viewBox="0 0 24 24" fill="#5BAEE0"><path d="M12 2l1.6 4.8L18 8l-4.4 1.5L12 14l-1.6-4.5L6 8l4.4-1.2z" /></svg></div>
        <div style={{ flex: 1 }}><div style={{ fontSize: 30, fontWeight: 800, color: '#1A2B3C' }}>AI Food Scout</div><div style={{ fontSize: 21, color: '#8AAAC0' }}>Ask me anything about food nearby</div></div>
        <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#5BAEE0" strokeWidth="2.2" strokeLinecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
      </div>
      {/* body */}
      <div style={{ flex: 1, padding: 30, display: 'flex', flexDirection: 'column', gap: 18, overflow: 'hidden' }}>
        {showEmpty && (
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16, paddingTop: 50, opacity: emptyOp }}>
            <div style={{ fontSize: 76 }}>🍴</div>
            <div style={{ fontSize: 36, fontWeight: 800, color: '#1A2B3C' }}>What are you craving?</div>
            <div style={{ fontSize: 23, color: '#8AAAC0', marginBottom: 6 }}>Try asking:</div>
            {['"Something spicy, open late"', '"Best ramen near me"', '"Hidden gems with 9+ rating"'].map((s, i) => (
              <div key={i} style={{ background: i === 0 ? '#D6ECFB' : '#E1F0FC', border: '1px solid #C8DFF0', borderRadius: 999, padding: '16px 28px', fontSize: 24, color: '#5A7A94' }}>{s}</div>
            ))}
          </div>
        )}
        {t >= tSend && (
          <div style={{ display: 'flex', justifyContent: 'flex-end', transform: `scale(${userIn})`, transformOrigin: 'right center' }}>
            <div style={{ maxWidth: '82%', background: '#84CAF5', color: '#1A2B3C', fontWeight: 600, borderRadius: 26, borderBottomRightRadius: 6, padding: '20px 26px', fontSize: 27 }}>{q}</div>
          </div>
        )}
        {thinking && (
          <div style={{ alignSelf: 'flex-start', background: '#fff', border: '1px solid #C8DFF0', borderRadius: 26, borderBottomLeftRadius: 6, padding: '22px 28px', display: 'flex', gap: 10 }}>
            {[0, 1, 2].map((i) => { const ph = (t * 3 + i * 0.33) % 1; const yy = Math.sin(ph * Math.PI) * -10; return (<div key={i} style={{ width: 14, height: 14, borderRadius: '50%', background: '#84CAF5', transform: `translateY(${yy}px)`, opacity: 0.5 + 0.5 * Math.sin(ph * Math.PI) }} />); })}
          </div>
        )}
        {showA && (
          <div style={{ alignSelf: 'flex-start', maxWidth: '90%', background: '#fff', border: '1px solid #C8DFF0', color: '#1A2B3C', borderRadius: 26, borderBottomLeftRadius: 6, padding: '22px 26px', fontSize: 25, lineHeight: 1.5, whiteSpace: 'pre-line' }}>{aShown}{t < aEnd ? '▍' : ''}</div>
        )}
        {btnIn > 0 && (
          <div data-showmap style={{ alignSelf: 'flex-start', marginTop: 4, display: 'flex', alignItems: 'center', gap: 14, background: '#84CAF5', borderRadius: 999, padding: '20px 32px', boxShadow: '0 8px 20px rgba(132,202,245,.4)', transform: `scale(${btnIn})`, transformOrigin: 'left center' }}>
            <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="#5BAEE0" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 3 3 6v15l6-3 6 3 6-3V3l-6 3-6-3z" /></svg>
            <span style={{ fontSize: 26, fontWeight: 800, color: '#5BAEE0' }}>Show 3 gems on the map</span>
          </div>
        )}
      </div>
      {/* input */}
      <div style={{ flex: 'none', display: 'flex', alignItems: 'center', gap: 16, padding: '20px 28px 40px', borderTop: '1px solid #C8DFF0' }}>
        <div style={{ flex: 1, background: '#fff', border: '2px solid ' + (sendActive ? '#84CAF5' : '#C8DFF0'), borderRadius: 28, padding: '20px 26px', fontSize: 26, color: qShown ? '#1A2B3C' : '#8AAAC0', minHeight: 30 }}>{qShown || 'Ask about food near you…'}{showEmpty && qShown && caret ? '|' : ''}</div>
        <div style={{ width: 76, height: 76, borderRadius: '50%', background: sendActive ? '#84CAF5' : '#C8DFF0', display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke={sendActive ? '#5BAEE0' : '#9FBBD4'} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5M5 12l7-7 7 7" /></svg></div>
      </div>
      <div style={{ position: 'absolute', bottom: 12, left: '50%', transform: 'translateX(-50%)', width: 200, height: 8, borderRadius: 4, background: 'rgba(26,43,60,.3)' }} />
    </div>
  );
}

// ───────────────────────── touch indicator ─────────────────────────
function computeTouch(t) {
  // returns {x,y,press,opacity} in screen space, or opacity 0
  const swipes = [
    [0.7, 1.6, 640, 1360, 560, 760],
    [2.4, 3.3, 700, 1380, 600, 820],
    [4.1, 5.0, 620, 1360, 560, 840],
    [5.7, 6.5, 700, 1380, 620, 860],
  ];
  for (const [s, e, x0, y0, x1, y1] of swipes) {
    if (t >= s && t <= e) {
      const p = (t - s) / (e - s); const ep = Easing.easeInOutCubic(p);
      return { x: lerp(x0, x1, ep), y: lerp(y0, y1, ep), press: 1, opacity: ramp(t, s, s + 0.12) * (1 - ramp(t, e - 0.15, e)) };
    }
  }
  // FAB tap
  if (t >= 6.8 && t <= 7.6) { const press = t > 7.15 && t < 7.4 ? 0.82 : 1; return { x: 992, y: 1664, press, opacity: ramp(t, 6.8, 7.0) * (1 - ramp(t, 7.45, 7.6)) }; }
  // send tap
  if (t >= 10.2 && t <= 10.75) { const press = t > 10.4 && t < 10.6 ? 0.82 : 1; return { x: 1000, y: 1748, press, opacity: ramp(t, 10.2, 10.32) * (1 - ramp(t, 10.62, 10.75)) }; }
  // show-on-map tap
  if (t >= 14.9 && t <= 15.7) { const press = t > 15.25 && t < 15.5 ? 0.82 : 1; return { x: 360, y: 1230, press, opacity: ramp(t, 14.9, 15.05) * (1 - ramp(t, 15.5, 15.7)) }; }
  return { x: 0, y: 0, press: 1, opacity: 0 };
}
function TouchDot({ t }) {
  const { x, y, press, opacity } = computeTouch(t);
  if (opacity <= 0.01) return null;
  return (
    <div style={{ position: 'absolute', left: x, top: y, zIndex: 60, transform: `translate(-50%,-50%) scale(${press})`, opacity, pointerEvents: 'none' }}>
      <div style={{ position: 'absolute', left: -8, top: -8, width: 16, height: 16 }} />
      {press < 1 && <div style={{ position: 'absolute', left: '50%', top: '50%', width: 150, height: 150, marginLeft: -75, marginTop: -75, borderRadius: '50%', border: '4px solid rgba(132,202,245,.7)', opacity: 0.6 }} />}
      <div style={{ width: 84, height: 84, borderRadius: '50%', background: 'rgba(91,174,224,.32)', border: '4px solid rgba(58,140,196,.85)', boxShadow: '0 4px 16px rgba(0,0,0,.25)' }} />
    </div>
  );
}

// ───────────────────────── caption ─────────────────────────
function Caption({ t }) {
  const caps = [
    [0.5, 3.4, 'Friday night. Where do we eat?'],
    [3.8, 6.7, 'Endless pins… nothing’s grabbing me.'],
    [16.6, 20.5, 'Spicy. Open late. Found it.'],
  ];
  const c = caps.find(([s, e]) => t >= s && t <= e);
  if (!c) return null;
  const [s, e, text] = c;
  const op = ramp(t, s, s + 0.4) * (1 - ramp(t, e - 0.4, e));
  const ty = lerp(14, 0, ramp(t, s, s + 0.4, Easing.easeOutCubic));
  return (
    <div style={{ position: 'absolute', top: 232, left: 0, right: 0, display: 'flex', justifyContent: 'center', zIndex: 55, opacity: op, transform: `translateY(${ty}px)`, pointerEvents: 'none' }}>
      <div style={{ background: 'rgba(26,43,60,.9)', color: '#fff', borderRadius: 999, padding: '16px 32px', fontSize: 28, fontWeight: 700, boxShadow: '0 8px 24px rgba(0,0,0,.3)', letterSpacing: '-.2px' }}>{text}</div>
    </div>
  );
}

// ───────────────────────── camera + scene ─────────────────────────
function MapScene() {
  const t = useTime();
  const fx = interpolate([0, 2, 4, 6, 7.4, 16, 18, 21], [820, 1080, 640, 820, 820, 820, 560, 552], Easing.easeInOutSine)(t);
  const fy = interpolate([0, 2, 4, 6, 7.4, 16, 18, 21], [760, 1000, 1260, 1740, 1740, 1480, 1360, 1345], Easing.easeInOutSine)(t);
  const z = interpolate([0, 16, 18, 21], [1, 1, 1.5, 1.5], Easing.easeInOutCubic)(t);
  const tx = VW / 2 - fx * z;
  const ty = VH / 2 - fy * z;
  return (
    <div data-screen-label={`t=${Math.floor(t)}s`} style={{ position: 'absolute', inset: 0, overflow: 'hidden', background: '#dce9f2' }}>
      <MapWorld t={t} tx={tx} ty={ty} z={z} />
      <MapChrome t={t} />
      {t >= 16.6 && <PinCard t={t} />}
      <Caption t={t} />
      <ChatOverlay t={t} />
      <TouchDot t={t} />
      {/* subtle screen vignette */}
      <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', boxShadow: 'inset 0 0 120px rgba(0,0,0,.12)', zIndex: 50 }} />
    </div>
  );
}

function GemDishVideo(props) {
  const minimal = !!(props && (props.minimal === true || props.minimal === 'true' || props.minimal === '1'));
  return (
    <Stage width={VW} height={VH} duration={21} background="#000" minimal={minimal}>
      <MapScene />
    </Stage>
  );
}
window.GemDishVideo = GemDishVideo;
