// Desktop-native record view for the TeachCue Studio.
//
// Why this file exists: the original `ScreenRecord` (screen-record.jsx)
// was designed for a 402×874 phone viewport — top bar, absolute-positioned
// controls, vertical control deck pinned to the bottom. That's the right
// UI for the iOS preview but wrong for a desktop SaaS, where users expect
// the chrome to read as an app (sidebar, toolbar, preview pane) regardless
// of the project's video aspect ratio.
//
// This component renders a real desktop layout while reusing the same
// media + speech pipeline by importing helpers exposed on `window` by
// screen-record.jsx (useSpeechFollow, getMediaStream, startCompositor,
// pickRecorderMime, humanizeMediaError).

const { useState: uSR, useEffect: uERR, useRef: uRRR, useCallback: uCBR, useLayoutEffect: uLER } = React;

// Measure an element and return the JS-computed video frame size that fits
// the aspect ratio inside it. CSS-only `aspect-ratio + 100% + auto` fights
// with `place-items: center` and the box collapses to 0×0 — JS measurement
// sidesteps that entirely.
function useFitToBox(refEl, aspectStr) {
  const [size, setSize] = uSR({ w: 0, h: 0 });
  uLER(() => {
    if (!refEl.current) return;
    const [aw, ah] = aspectStr.split('/').map(s => parseFloat(s.trim()));
    const target = aw / ah;
    const fit = () => {
      if (!refEl.current) return;
      const r = refEl.current.getBoundingClientRect();
      if (r.width === 0 || r.height === 0) return;
      const pane = r.width / r.height;
      let w, h;
      if (target > pane) { w = r.width; h = r.width / target; }   // wider than pane → width-limited
      else               { h = r.height; w = r.height * target; } // taller than pane → height-limited
      setSize({ w: Math.round(w), h: Math.round(h) });
    };
    fit();
    const ro = new ResizeObserver(fit);
    ro.observe(refEl.current);
    return () => ro.disconnect();
  }, [aspectStr]);
  return size;
}

// Cheap responsive hook — re-renders when the window crosses a breakpoint.
// Used to collapse the slide rail on narrow viewports so the preview pane
// isn't squeezed to a thumbnail.
function useBreakpoint() {
  const [wide, setWide] = uSR(typeof window !== 'undefined' ? window.innerWidth >= 900 : true);
  uERR(() => {
    const onResize = () => setWide(window.innerWidth >= 900);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);
  return wide;
}

function StudioRecord({ state, set, nav }) {
  // ── Recording state (mirrors screen-record.jsx) ──
  const [recState, setRecState] = uSR('idle');       // idle | recording | paused
  const [time, setTime] = uSR(0);
  const [slideIdx, setSlideIdx] = uSR(0);
  const [frontCam, setFrontCam] = uSR(true);
  const [source, setSource] = uSR('camera');         // camera | screen
  const [streamReady, setStreamReady] = uSR(false);
  const [permError, setPermError] = uSR(null);
  const [speechFollowOn, setSpeechFollowOn] = uSR(false);
  const [burnIn, setBurnIn] = uSR(true);
  // Beauty filter — slider state + open/close for the popover panel.
  // Default preset is "natural" so users get a subtle effect out of the box.
  const [beauty, setBeauty] = uSR({
    ...(window.BEAUTY_PRESETS?.natural || { smoothness: 0, brightness: 50, warmth: 50, glow: 0 }),
    slim: 0,    // face slim — 0..100, driven by MediaPipe face bbox
  });
  const [beautyOn, setBeautyOn] = uSR(false);
  const [showBeauty, setShowBeauty] = uSR(false);
  // Live CSS filter string — applied to the <video> element AND piped
  // into the compositor (drawStateRef.filter) so the recorded video
  // matches. Slim is NOT part of the CSS filter — it needs face detection
  // and is rendered via canvas (see useFaceSlim below).
  const filterStr = beautyOn ? window.composeBeautyFilter(beauty) : 'none';
  const slimAmount = beautyOn ? (beauty.slim || 0) : 0;
  // Face slim takes over the live preview via a canvas when slim > 0.
  // Hook stays mounted (with enabled=false) on slim=0 so the detector
  // doesn't reload every toggle.
  const slimEnabled = slimAmount > 0;
  const faceSlim = window.useFaceSlim?.(videoRef, {
    enabled: slimEnabled,
    slim: slimAmount,
    mirror: source === 'camera' && frontCam,
    filterStr,
  }) || { canvasRef: { current: null }, ready: false, face: null, error: null };

  // ── Refs ──
  const videoRef = uRRR(null);
  const streamRef = uRRR(null);
  const recorderRef = uRRR(null);
  const chunksRef = uRRR([]);
  const recordStartRef = uRRR(0);
  const transcriptLogRef = uRRR([]);
  const compositorRef = uRRR(null);
  const drawStateRef = uRRR({
    composition: 'cameraOnly',
    aspect: '9:16',
    mirror: true,
    slideText: '',
    slideLang: 'auto',
    progress: 0,
    burnInCaption: true,
  });

  const slides = state.slides;
  const slide = slides[slideIdx];
  const fmt = (s) => `${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;
  const wideViewport = useBreakpoint();

  // ── Keep draw-state synced for the compositor's rAF loop ──
  uERR(() => {
    const s = drawStateRef.current;
    s.composition = state.composition;
    s.aspect = state.aspect || '9:16';
    s.mirror = source === 'camera' && frontCam;
    s.slideText = slide?.text || '';
    s.slideLang = slide?.lang || 'auto';
    s.burnInCaption = burnIn;
    s.filter = filterStr;
    // Face slim — burn into export by passing slim + last detected bbox
    // to the compositor. The compositor calls drawFaceSlim() per frame.
    s.slim = slimAmount;
    s.face = faceSlim.face;
  });

  // ── Smart-follow alignment ──
  const advanceSlide = uCBR(() => {
    setSlideIdx(i => Math.min(slides.length - 1, i + 1));
  }, [slides.length]);

  const speech = window.useSpeechFollow({
    enabled: speechFollowOn && recState === 'recording',
    scriptText: slide?.text || '',
    lang: slide?.lang || 'auto',
    onAdvance: advanceSlide,
    onResult: (t) => {
      const rel = recordStartRef.current ? (Date.now() - recordStartRef.current) : 0;
      transcriptLogRef.current.push({ t: rel, text: t.text, progress: t.progress, slide: slideIdx });
      drawStateRef.current.progress = t.progress;
    },
  });

  // ── Media stream acquisition ──
  uERR(() => {
    let cancelled = false;
    async function acquire() {
      if (recorderRef.current && recorderRef.current.state !== 'inactive') return;
      stopStream();
      setStreamReady(false);
      setPermError(null);
      try {
        const stream = await window.getMediaStream(source, frontCam);
        if (cancelled) { stream.getTracks().forEach(t => t.stop()); return; }
        streamRef.current = stream;
        if (videoRef.current) {
          videoRef.current.srcObject = stream;
          videoRef.current.play().catch(() => {});
        }
        stream.getVideoTracks().forEach(t => {
          t.onended = () => { if (source === 'screen') setSource('camera'); };
        });
        setStreamReady(true);
      } catch (err) {
        if (!cancelled) setPermError(window.humanizeMediaError(err, source));
      }
    }
    acquire();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [source, frontCam]);

  // ── Timer ──
  uERR(() => {
    if (recState !== 'recording') return;
    const t = setInterval(() => setTime(s => s + 1), 1000);
    return () => clearInterval(t);
  }, [recState]);

  // ── Cleanup ──
  uERR(() => () => {
    try { const r = recorderRef.current; if (r && r.state !== 'inactive') r.stop(); } catch {}
    stopStream();
  }, []);

  function stopStream() {
    const s = streamRef.current;
    if (s) {
      s.getTracks().forEach(t => { try { t.stop(); } catch {} });
      streamRef.current = null;
    }
    if (videoRef.current) videoRef.current.srcObject = null;
  }

  function startRecording() {
    if (!streamRef.current) { setPermError('No video source.'); return; }
    chunksRef.current = [];
    const mimeType = window.pickRecorderMime();

    let recordedStream = streamRef.current;
    if (burnIn) {
      try {
        compositorRef.current = window.startCompositor({
          sourceVideo: videoRef.current,
          sourceStream: streamRef.current,
          drawStateRef,
        });
        recordedStream = compositorRef.current.stream;
      } catch (e) {
        compositorRef.current = null;
      }
    }

    let recorder;
    try {
      recorder = new MediaRecorder(recordedStream, mimeType ? { mimeType, videoBitsPerSecond: 5_000_000 } : undefined);
    } catch (e) {
      setPermError('Recording not supported. ' + (e?.message || ''));
      try { compositorRef.current?.stop(); } catch {}
      compositorRef.current = null;
      return;
    }
    recorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) chunksRef.current.push(e.data); };
    recorder.onstop = () => {
      const type = recorder.mimeType || mimeType || 'video/webm';
      const blob = new Blob(chunksRef.current, { type });
      chunksRef.current = [];
      const url = URL.createObjectURL(blob);
      try { compositorRef.current?.stop(); } catch {}
      compositorRef.current = null;
      if (state.recording?.url) { try { URL.revokeObjectURL(state.recording.url); } catch {} }
      set({
        recording: {
          url, mimeType: type, durationSec: time, size: blob.size,
          source, createdAt: Date.now(), composited: burnIn,
          transcript: transcriptLogRef.current.slice(),
          slideTexts: slides.map(s => ({ text: s.text, lang: s.lang })),
        },
      });
      transcriptLogRef.current = [];
      setTime(0);
      setRecState('idle');
      setTimeout(() => nav.go('edit'), 250);
    };
    recorder.start(1000);
    recorderRef.current = recorder;
    recordStartRef.current = Date.now();
    transcriptLogRef.current = [];
    setRecState('recording');
    setTime(0);
  }

  function stopRecording() {
    const r = recorderRef.current;
    if (!r) { setRecState('idle'); return; }
    try { if (r.state !== 'inactive') r.stop(); } catch {}
  }
  const startStop = () => recState === 'idle' ? startRecording() : stopRecording();
  const pauseResume = () => {
    const r = recorderRef.current;
    if (!r) return;
    if (r.state === 'recording') { r.pause(); setRecState('paused'); }
    else if (r.state === 'paused') { r.resume(); setRecState('recording'); }
  };

  // ── Aspect → CSS aspect-ratio for the preview pane ──
  const previewAspect = {
    '16:9': '16 / 9',
    '1:1':  '1 / 1',
    '9:16': '9 / 16',
  }[state.aspect || '9:16'];

  const mirror = source === 'camera' && frontCam;

  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: wideViewport ? '1fr 280px' : '1fr',
      gridTemplateRows: '1fr auto',
      height: '100%',
      background: 'var(--bone)',
      minHeight: 0,
    }}>
      {/* ── PREVIEW PANE ─────────────────────────────────────────── */}
      <PreviewPane
        previewAspect={previewAspect}
        videoRef={videoRef}
        streamReady={streamReady}
        permError={permError}
        source={source}
        mirror={mirror}
        recState={recState}
        time={time}
        fmt={fmt}
        slide={slide}
        slideIdx={slideIdx}
        slidesCount={slides.length}
        speech={speech}
        speechFollowOn={speechFollowOn}
        composition={state.composition}
        filterStr={filterStr}
        slimEnabled={slimEnabled}
        faceSlimCanvasRef={faceSlim.canvasRef}
        faceSlimReady={faceSlim.ready}
      >
        {/* placeholder — children unused, all the overlays moved into PreviewPane */}
      </PreviewPane>

      {/* ── SLIDE RAIL (right column, spans both rows) ─────────────
       *   Hidden on narrow viewports where the slide-rail column would
       *   eat the preview pane's width. The toolbar's slide-nav pill
       *   replaces it for narrow viewports. */}
      <div style={{
        gridColumn: '2 / 3', gridRow: '1 / 3',
        borderLeft: '1px solid var(--rule)',
        background: 'var(--bone)',
        display: wideViewport ? 'flex' : 'none',
        flexDirection: 'column',
        minHeight: 0,
      }}>
        <div style={{
          padding: '14px 14px 8px',
          fontFamily: 'ui-monospace, "SF Mono", monospace',
          fontSize: 10, letterSpacing: 1.6, textTransform: 'uppercase',
          color: 'var(--ink-3)',
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        }}>
          <span>Slides · {slides.length}</span>
          <button onClick={() => nav.go('prepare')} style={{
            background: 'transparent', border: 0, color: 'var(--accent)',
            fontSize: 11, fontWeight: 600, cursor: 'pointer',
          }}>Edit</button>
        </div>
        <div style={{flex: 1, overflowY: 'auto', padding: '0 8px 14px', display: 'flex', flexDirection: 'column', gap: 4}}>
          {slides.map((s, i) => {
            const active = i === slideIdx;
            return (
              <button
                key={s.id || i}
                onClick={() => setSlideIdx(i)}
                style={{
                  textAlign: 'left',
                  padding: '10px 12px', borderRadius: 9,
                  background: active ? 'var(--accent-soft)' : 'transparent',
                  border: '1px solid ' + (active ? 'var(--accent)' : 'transparent'),
                  cursor: 'pointer',
                  display: 'flex', alignItems: 'flex-start', gap: 10,
                }}
              >
                <span style={{
                  flexShrink: 0,
                  fontFamily: 'ui-monospace, "SF Mono", monospace', fontSize: 11, fontWeight: 600,
                  color: active ? 'var(--accent-deep)' : 'var(--ink-3)',
                  background: active ? '#fff' : 'var(--bone-2)',
                  padding: '2px 6px', borderRadius: 5,
                  minWidth: 26, textAlign: 'center',
                }}>{String(i+1).padStart(2, '0')}</span>
                <span style={{
                  fontSize: 13, color: active ? 'var(--ink)' : 'var(--ink-2)',
                  lineHeight: 1.35, fontWeight: active ? 500 : 400,
                  overflow: 'hidden', textOverflow: 'ellipsis',
                  display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
                }}>{s.text || `Slide ${i+1}`}</span>
              </button>
            );
          })}
        </div>
      </div>

      {/* ── BOTTOM TOOLBAR ─────────────────────────────────────── */}
      <div style={{
        gridColumn: '1 / 2', gridRow: '2 / 3',
        borderTop: '1px solid var(--rule)',
        background: 'var(--cream)',
        padding: '12px 18px',
        display: 'flex', alignItems: 'center', gap: 12,
      }}>
        {/* Source toggle */}
        <ToolbarBtn
          active={source==='screen'}
          onClick={() => recState === 'idle' && setSource(source==='camera'?'screen':'camera')}
          disabled={recState !== 'idle'}
          title={source==='screen' ? 'Recording screen' : 'Recording camera'}
        >
          <span style={{fontSize: 14}}>{source==='screen' ? '🖥️' : '📷'}</span>
          <span>{source==='screen' ? 'Screen' : 'Camera'}</span>
        </ToolbarBtn>

        {/* Flip cam (only when camera mode) */}
        {source==='camera' && (
          <ToolbarBtn onClick={()=>setFrontCam(!frontCam)} title="Flip camera">
            <span style={{fontSize: 14}}>↺</span>
            <span>{frontCam ? 'Front' : 'Back'}</span>
          </ToolbarBtn>
        )}

        {/* Smart-follow toggle */}
        <ToolbarBtn
          active={speechFollowOn}
          onClick={() => {
            if (!speech.supported && speech.provider !== 'pending') {
              alert('Smart follow needs Chrome, Edge, or Safari (SpeechRecognition API).');
              return;
            }
            setSpeechFollowOn(v => !v);
          }}
          title="Smart follow auto-scrolls the teleprompter from your speech"
        >
          <span style={{fontSize: 14}}>✨</span>
          <span>Smart follow</span>
        </ToolbarBtn>

        {/* Beauty filter — toggle on/off + open the panel */}
        <div style={{position: 'relative'}}>
          <ToolbarBtn
            active={beautyOn}
            onClick={() => {
              if (!beautyOn) { setBeautyOn(true); setShowBeauty(true); }
              else { setShowBeauty(v => !v); }
            }}
            title="Beauty filter — skin smoothing, brightness, warmth, glow"
          >
            <span style={{fontSize: 14}}>🌟</span>
            <span>Beauty</span>
          </ToolbarBtn>
          {beautyOn && showBeauty && (
            <BeautyPanel
              beauty={beauty}
              setBeauty={setBeauty}
              onClose={() => setShowBeauty(false)}
              onTurnOff={() => { setBeautyOn(false); setShowBeauty(false); }}
            />
          )}
        </div>

        {/* Burn-in toggle */}
        <ToolbarBtn
          active={burnIn}
          onClick={() => recState === 'idle' && setBurnIn(v => !v)}
          disabled={recState !== 'idle'}
          title="Bake captions into the exported video"
        >
          <span style={{fontSize: 14}}>T</span>
          <span>Burn-in</span>
        </ToolbarBtn>

        {/* Slide navigator pill — keeps slide-nav reachable when the
            right rail is hidden on narrow viewports. */}
        <div style={{
          display: 'inline-flex', alignItems: 'center', gap: 4,
          padding: '4px 6px', borderRadius: 9,
          background: 'var(--cream)', border: '1px solid var(--rule)',
        }}>
          <button
            onClick={() => setSlideIdx(Math.max(0, slideIdx - 1))}
            disabled={slideIdx === 0}
            style={{
              width: 28, height: 28, borderRadius: 6, border: 0,
              background: 'transparent', color: slideIdx === 0 ? 'var(--ink-3)' : 'var(--ink)',
              cursor: slideIdx === 0 ? 'not-allowed' : 'pointer', fontSize: 14,
            }}
          >‹</button>
          <span style={{
            fontFamily: 'ui-monospace, "SF Mono", monospace',
            fontSize: 11, color: 'var(--ink-2)', minWidth: 38, textAlign: 'center',
            letterSpacing: 0.5,
          }}>{String(slideIdx+1).padStart(2,'0')} / {String(slides.length).padStart(2,'0')}</span>
          <button
            onClick={() => setSlideIdx(Math.min(slides.length - 1, slideIdx + 1))}
            disabled={slideIdx === slides.length - 1}
            style={{
              width: 28, height: 28, borderRadius: 6, border: 0,
              background: 'transparent', color: slideIdx === slides.length - 1 ? 'var(--ink-3)' : 'var(--ink)',
              cursor: slideIdx === slides.length - 1 ? 'not-allowed' : 'pointer', fontSize: 14,
            }}
          >›</button>
        </div>

        <div style={{flex: 1}}/>

        {/* Timer */}
        <span style={{
          fontFamily: 'ui-monospace, "SF Mono", monospace',
          fontSize: 14, fontWeight: 600, color: recState==='recording' ? 'var(--accent-deep)' : 'var(--ink-3)',
          letterSpacing: 1,
          minWidth: 56, textAlign: 'right',
        }}>{fmt(time)}</span>

        {/* Pause/resume (only when recording) */}
        {recState !== 'idle' && (
          <button onClick={pauseResume} style={{
            width: 42, height: 42, borderRadius: 999,
            background: recState==='paused' ? '#F0B042' : 'var(--cream)',
            border: '1px solid var(--rule-2)',
            color: 'var(--ink)',
            cursor: 'pointer', fontSize: 16,
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          }}>{recState==='recording' ? '❚❚' : '▶'}</button>
        )}

        {/* Record button */}
        <button
          onClick={startStop}
          disabled={!streamReady && recState==='idle'}
          style={{
            height: 42, padding: '0 18px', borderRadius: 999,
            background: recState==='recording' ? 'var(--ink)' : 'var(--accent)',
            color: '#fff', border: 0,
            cursor: streamReady ? 'pointer' : 'not-allowed',
            opacity: (!streamReady && recState==='idle') ? 0.5 : 1,
            display: 'inline-flex', alignItems: 'center', gap: 8,
            fontSize: 14, fontWeight: 600, letterSpacing: -0.1,
            boxShadow: '0 8px 20px -8px ' + (recState==='recording' ? 'rgba(30,26,21,0.5)' : 'rgba(232,123,78,0.55)'),
          }}>
          <span style={{
            width: recState==='recording' ? 12 : 14,
            height: recState==='recording' ? 12 : 14,
            borderRadius: recState==='recording' ? 3 : 999,
            background: '#fff',
            transition: 'all .2s',
          }}/>
          <span>{recState==='recording' ? 'Stop' : 'Record'}</span>
        </button>
      </div>
    </div>
  );
}

// Toolbar button — pill, accent when active.
function ToolbarBtn({ children, active, onClick, disabled, title }) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      title={title}
      style={{
        height: 38, padding: '0 13px', borderRadius: 9,
        background: active ? 'var(--ink)' : 'var(--cream)',
        color: active ? 'var(--bone)' : 'var(--ink-2)',
        border: '1px solid ' + (active ? 'var(--ink)' : 'var(--rule)'),
        display: 'inline-flex', alignItems: 'center', gap: 7,
        fontSize: 12.5, fontWeight: 500, letterSpacing: -0.1,
        cursor: disabled ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.5 : 1,
        transition: 'background .15s, color .15s, border-color .15s',
      }}
    >
      {children}
    </button>
  );
}

// Renders the slide text with word-level highlight (matches smart-follow
// progress). Standalone so we don't reach into the iOS-frame's SlideBodyText
// component, which couples furigana rendering to the phone aspect ratio.
function SlideHighlightText({ text, progress, size = 18 }) {
  if (!text) return null;
  const parts = text.split(/(\s+)/);
  const wordCount = parts.filter(p => /\S/.test(p)).length || 1;
  const spoken = Math.round(progress * wordCount);
  let wi = 0;
  return (
    <div style={{color: '#fff', fontSize: size, fontWeight: 600, lineHeight: 1.35, letterSpacing: -0.2}}>
      {parts.map((p, i) => {
        if (!/\S/.test(p)) return <span key={i}>{p}</span>;
        const isSpoken = wi < spoken;
        wi++;
        return (
          <span key={i} style={{
            opacity: progress > 0 ? (isSpoken ? 1 : 0.45) : 1,
            color: progress > 0 && isSpoken ? 'var(--accent)' : '#fff',
            transition: 'opacity .2s, color .2s',
          }}>{p}</span>
        );
      })}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// PreviewPane — the desktop "monitor" area. Hosts the live video
// at its target aspect ratio (sized via JS measurement to avoid the
// `aspect-ratio + auto + place-items: center → 0×0` CSS pitfall),
// plus all overlays: permission state, recording indicator, and the
// teleprompter card. Overlays attach to the PANE (always visible)
// rather than the inner video frame (which can be zero-sized while
// CSS settles), so the user always sees a clear next step.
// ─────────────────────────────────────────────────────────────
function PreviewPane({
  previewAspect, videoRef, streamReady, permError, source, mirror,
  recState, time, fmt, slide, slideIdx, slidesCount,
  speech, speechFollowOn, composition, filterStr,
  slimEnabled, faceSlimCanvasRef, faceSlimReady,
}) {
  const paneRef = uRRR(null);
  const { w, h } = useFitToBox(paneRef, previewAspect);

  return (
    <div
      ref={paneRef}
      style={{
        gridColumn: '1 / 2', gridRow: '1 / 2',
        background: '#0E0D0B',
        position: 'relative', overflow: 'hidden',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: 24,
        minHeight: 0,
      }}
    >
      {/* Subtle floor lights */}
      <div style={{
        position: 'absolute', inset: 0, pointerEvents: 'none',
        background:
          'radial-gradient(ellipse 60% 40% at 50% 110%, rgba(232,123,78,0.10), transparent 70%),' +
          'radial-gradient(ellipse 40% 30% at 10% -10%, rgba(123,168,150,0.08), transparent 70%)',
      }}/>

      {/* Video frame — explicit pixel dimensions from JS measurement.
          Always non-zero whenever the pane is non-zero. */}
      <div style={{
        position: 'relative',
        width: w || 0, height: h || 0,
        background: '#000',
        borderRadius: 10, overflow: 'hidden',
        boxShadow: '0 30px 80px -30px rgba(0,0,0,0.7), inset 0 0 0 1px rgba(255,255,255,0.06)',
        opacity: w > 0 ? 1 : 0,    // hide until first measurement so we don't flash a 0×0 box
        transition: 'opacity .2s',
      }}>
        <video
          ref={videoRef}
          autoPlay playsInline muted
          style={{
            position: 'absolute', inset: 0,
            width: '100%', height: '100%', objectFit: 'cover',
            transform: mirror ? 'scaleX(-1)' : 'none',
            // ALWAYS visible when the stream is ready. The face-slim canvas
            // overlays on top; its transparent regions or its empty initial
            // state reveal this video as a fallback — no black-screen gap
            // while MediaPipe loads. When slim is on we drop the CSS filter
            // here because the canvas applies it pixel-perfect via ctx.filter.
            opacity: streamReady ? 1 : 0,
            transition: 'opacity .25s ease, filter .15s ease',
            background: '#000',
            filter: slimEnabled ? 'none' : (filterStr || 'none'),
          }}
        />

        {/* Face-slim canvas — overlays the video. Stays visible at all times
            once slim is enabled; its transparent areas reveal the video
            underneath, so the user never sees black. */}
        {slimEnabled && (
          <canvas
            ref={faceSlimCanvasRef}
            style={{
              position: 'absolute', inset: 0,
              width: '100%', height: '100%',
              opacity: 1,
              pointerEvents: 'none',
              // No background — transparent canvas pixels show the video below.
            }}
          />
        )}

        {/* Teleprompter card lives over the video so it scales with it. */}
        {streamReady && composition === 'cameraOnly' && slide?.text && (
          <div style={{
            position: 'absolute', left: '4%', right: '4%', top: '6%',
            background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(14px)',
            border: '1px solid rgba(255,255,255,0.1)', borderRadius: 12,
            padding: '14px 16px',
            color: '#fff',
          }}>
            <div style={{display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom: 8}}>
              <span style={{
                fontFamily: 'ui-monospace, "SF Mono", monospace', fontSize: 10, letterSpacing: 1.5,
                background: 'rgba(255,255,255,0.12)', padding: '2px 7px', borderRadius: 4,
              }}>SLIDE {String(slideIdx+1).padStart(2,'0')} / {String(slidesCount).padStart(2,'0')}</span>
              {speechFollowOn && (
                <span style={{
                  fontFamily: 'ui-monospace, "SF Mono", monospace', fontSize: 10, letterSpacing: 1.5,
                  color: 'var(--accent)', background: 'rgba(232,123,78,0.16)', padding: '2px 7px', borderRadius: 4,
                }}>{speech.error ? 'SR ERR' : (speech.listening ? 'LISTENING' : 'SMART')}</span>
              )}
            </div>
            <SlideHighlightText text={slide.text} progress={speechFollowOn ? speech.progress : 0} size={18}/>
            {speechFollowOn && (
              <div style={{height: 3, marginTop: 10, background: 'rgba(255,255,255,0.14)', borderRadius: 2, overflow: 'hidden'}}>
                <div style={{height: '100%', width: `${Math.min(100, Math.max(0, speech.progress*100)).toFixed(1)}%`, background: 'var(--accent)', transition: 'width .25s ease'}}/>
              </div>
            )}
          </div>
        )}

        {/* Recording indicator */}
        {recState === 'recording' && (
          <div style={{
            position: 'absolute', top: 14, right: 14,
            display: 'flex', alignItems: 'center', gap: 6,
            padding: '5px 10px',
            background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(10px)',
            border: '1px solid rgba(255,77,94,0.5)', borderRadius: 999,
          }}>
            <div style={{width: 8, height: 8, borderRadius: '50%', background: '#FF4D5E', animation: 'pulse-rec 1.4s ease-in-out infinite'}}/>
            <span style={{color: '#fff', fontSize: 11, fontFamily: 'ui-monospace, monospace', letterSpacing: 1}}>{fmt(time)}</span>
          </div>
        )}
      </div>

      {/* Permission / waiting overlay — attached to the PANE, not the inner
          video frame, so it always fills the visible area and shows up even
          if the inner frame is still measuring. */}
      {!streamReady && (
        <div style={{
          position: 'absolute', inset: 0,
          display: 'grid', placeItems: 'center',
          padding: 32,
          pointerEvents: 'none',  // overlays don't block the underlying video element
        }}>
          <div style={{textAlign: 'center', maxWidth: 420, color: '#fff', pointerEvents: 'auto'}}>
            {permError ? (
              <>
                <div style={{
                  width: 56, height: 56, borderRadius: 999,
                  background: 'rgba(232,123,78,0.18)', color: 'var(--accent)',
                  display: 'inline-grid', placeItems: 'center',
                  fontSize: 26, marginBottom: 18,
                }}>🎥</div>
                <div style={{fontSize: 22, fontWeight: 600, marginBottom: 8, letterSpacing: -0.3}}>Camera & mic needed</div>
                <div style={{fontSize: 14, color: 'rgba(255,255,255,0.7)', lineHeight: 1.55, marginBottom: 20}}>
                  {permError}
                </div>
                <button
                  onClick={() => location.reload()}
                  style={{
                    padding: '11px 20px', borderRadius: 10,
                    background: 'var(--accent)', color: '#fff', border: 0,
                    fontSize: 14, fontWeight: 600, cursor: 'pointer',
                    boxShadow: '0 10px 22px -10px rgba(232,123,78,0.6)',
                  }}
                >Reload and try again</button>
              </>
            ) : (
              <>
                <div style={{
                  width: 56, height: 56, borderRadius: 999,
                  background: 'rgba(255,255,255,0.08)',
                  display: 'inline-grid', placeItems: 'center',
                  fontSize: 26, marginBottom: 18,
                }}>{source === 'screen' ? '🖥️' : '🎥'}</div>
                <div style={{fontSize: 20, fontWeight: 600, marginBottom: 8, letterSpacing: -0.3}}>
                  {source === 'screen' ? 'Pick a screen to share' : 'Allow camera & microphone'}
                </div>
                <div style={{fontSize: 14, color: 'rgba(255,255,255,0.6)', lineHeight: 1.55}}>
                  {source === 'screen'
                    ? 'Choose the screen, window, or tab you want to record.'
                    : 'Your browser will ask for permission — click Allow. The video appears here once granted.'}
                </div>
              </>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// BeautyPanel — popover from the toolbar button. 4 sliders + preset
// chips + face-slim placeholder ("coming" — needs MediaPipe FaceMesh
// for landmark-driven warp; tracked separately).
// ─────────────────────────────────────────────────────────────
function BeautyPanel({ beauty, setBeauty, onClose, onTurnOff }) {
  const setKey = (k, v) => setBeauty(b => ({ ...b, [k]: v }));
  const presets = ['natural', 'soft', 'bright', 'warm', 'cool', 'cinematic'];
  return (
    <div
      style={{
        position: 'absolute',
        bottom: 'calc(100% + 8px)', left: 0,
        width: 280,
        background: 'var(--cream)',
        border: '1px solid var(--rule)',
        borderRadius: 14,
        boxShadow: '0 20px 50px -20px rgba(56,40,20,0.35), 0 10px 20px -10px rgba(56,40,20,0.15)',
        padding: 14,
        zIndex: 100,
      }}
      // Click outside the panel doesn't auto-close — let the user fiddle
      // freely. The toolbar button toggles visibility.
    >
      <div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12}}>
        <div style={{fontFamily: 'var(--serif)', fontSize: 16, fontWeight: 500, color: 'var(--ink)', letterSpacing: -0.2}}>
          Beauty filter
        </div>
        <button onClick={onClose} style={{
          background: 'transparent', border: 0, color: 'var(--ink-3)',
          fontSize: 16, padding: 4, cursor: 'pointer', lineHeight: 1,
        }}>×</button>
      </div>

      {/* Preset chips */}
      <div style={{display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 14}}>
        {presets.map(p => (
          <button
            key={p}
            onClick={() => setBeauty(window.BEAUTY_PRESETS[p])}
            style={{
              padding: '4px 9px', borderRadius: 6,
              background: 'var(--bone-2)',
              border: '1px solid var(--rule)',
              color: 'var(--ink-2)',
              fontSize: 11, fontWeight: 500, letterSpacing: -0.1,
              cursor: 'pointer', textTransform: 'capitalize',
            }}
          >{p}</button>
        ))}
      </div>

      <BeautySlider label="Smoothness"  value={beauty.smoothness}  onChange={v => setKey('smoothness', v)} />
      <BeautySlider label="Brightness"  value={beauty.brightness}  onChange={v => setKey('brightness', v)} centered />
      <BeautySlider label="Warmth"      value={beauty.warmth}      onChange={v => setKey('warmth', v)} centered />
      <BeautySlider label="Glow"        value={beauty.glow}        onChange={v => setKey('glow', v)} />
      {/* Face slim — runs MediaPipe face detection on the live preview and
          horizontally compresses the face region. Subtle at <30, visible
          band at >50. */}
      <BeautySlider
        label="Face slim"
        value={beauty.slim || 0}
        onChange={v => setKey('slim', v)}
        hint="face-aware"
      />

      <div style={{display: 'flex', gap: 6, marginTop: 12}}>
        <button onClick={onTurnOff} style={{
          flex: 1, padding: '8px 10px', borderRadius: 8,
          background: 'var(--bone-2)', border: '1px solid var(--rule)',
          color: 'var(--ink-2)', fontSize: 12, fontWeight: 500, cursor: 'pointer',
        }}>Turn off</button>
        <button onClick={() => setBeauty(window.BEAUTY_PRESETS.natural)} style={{
          flex: 1, padding: '8px 10px', borderRadius: 8,
          background: 'var(--ink)', border: 0,
          color: 'var(--bone)', fontSize: 12, fontWeight: 500, cursor: 'pointer',
        }}>Reset</button>
      </div>
    </div>
  );
}

function BeautySlider({ label, value, onChange, centered, hint }) {
  // Centered sliders (brightness, warmth) show a tick mark at 50 to
  // signal "neutral" — moving up or down has different effects.
  return (
    <label style={{display: 'block', marginBottom: 10}}>
      <div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4}}>
        <span style={{fontSize: 11.5, color: 'var(--ink-2)', fontWeight: 500}}>
          {label}
          {hint && (
            <span style={{
              marginLeft: 6,
              fontFamily: 'ui-monospace, "SF Mono", monospace',
              fontSize: 9, padding: '1px 5px', borderRadius: 3,
              background: 'var(--accent-soft)', color: 'var(--accent-deep)',
              letterSpacing: 0.3, textTransform: 'uppercase',
              verticalAlign: 1,
            }}>{hint}</span>
          )}
        </span>
        <span style={{
          fontFamily: 'ui-monospace, "SF Mono", monospace',
          fontSize: 10.5, color: 'var(--ink-3)', letterSpacing: 0.3,
        }}>
          {centered ? (value === 50 ? '0' : (value > 50 ? '+' : '') + (value - 50)) : value}
        </span>
      </div>
      <div style={{position: 'relative'}}>
        <input
          type="range" min={0} max={100} value={value}
          onChange={e => onChange(parseInt(e.target.value, 10))}
          style={{width: '100%', accentColor: 'var(--accent)'}}
        />
        {centered && (
          <div style={{
            position: 'absolute', top: 6, left: '50%', width: 1, height: 8,
            background: 'var(--ink-3)', opacity: 0.5, pointerEvents: 'none',
          }}/>
        )}
      </div>
    </label>
  );
}

window.StudioRecord = StudioRecord;
