const WS_URL = "wss://api.usegreenroom.app/ws/interview";

function useMobileView(bp = 768) {
  const [mobile, setMobile] = React.useState(() => window.innerWidth < bp);
  React.useEffect(() => {
    const fn = () => setMobile(window.innerWidth < bp);
    window.addEventListener("resize", fn, { passive: true });
    return () => window.removeEventListener("resize", fn);
  }, [bp]);
  return mobile;
}

const FREE_TIER_LIMIT = 600; // 10 minutes

function MockRoom({ onEnd, onHome, jobContext = {}, token = "", onUpgrade, subscriptionPlan = "free" }) {
  const isFree = subscriptionPlan === "free";
  const [seconds, setSeconds] = React.useState(() => isFree ? FREE_TIER_LIMIT : 0);
  const [questions, setQuestions] = React.useState([]);
  const [qLoading, setQLoading] = React.useState(true);
  const [hasStream, setHasStream] = React.useState(false);
  const wsStartedRef = React.useRef(false);
  const [muted, setMuted] = React.useState(false);
  const [agentSpeaking, setAgentSpeaking] = React.useState(false);
  const [hasCamera, setHasCamera] = React.useState(false);
  const [camError, setCamError] = React.useState(null);
  const [videoOff, setVideoOff] = React.useState(false);
  const [started, setStarted] = React.useState(false);
  const [ending, setEnding] = React.useState(false);
  const [githubClaimed, setGithubClaimed] = React.useState(false);
  const [capHit, setCapHit] = React.useState(null); // { used, limit }
  const [status, setStatus] = React.useState("connecting");
  const [transcript, setTranscript] = React.useState([]);
  const [typingText, setTypingText] = React.useState("");
  const [voice, setVoice] = React.useState("female");
  const [language, setLanguage] = React.useState("python");
  const [showRating, setShowRating]       = React.useState(false);
  const [ratingStep, setRatingStep]       = React.useState(1); // 1 = realism, 2 = accuracy
  const [sessionRating, setSessionRating] = React.useState(0);
  const [accuracyRating, setAccuracyRating] = React.useState(0);
  const [mobileTab, setMobileTab] = React.useState("transcript");
  const isMobile = useMobileView(768);
  const editorRef = React.useRef(null);
  const monacoInstanceRef = React.useRef(null);

  const videoRef = React.useRef(null);
  const streamRef = React.useRef(null);       // audio-only, used for WS mic capture
  const recorderRef       = React.useRef(null);
  const recordedChunksRef = React.useRef([]);
  const videoStreamRef = React.useRef(null);  // video-only, managed separately
  const videoOffMounted = React.useRef(false); // skip effect on first render
  const wsRef = React.useRef(null);
  const audioCtxRef = React.useRef(null);
  const processorRef = React.useRef(null);
  const sourceRef = React.useRef(null);
  const transcriptEndRef = React.useRef(null);
  const feedbackRef = React.useRef(null);
  const pendingFeedbackRef = React.useRef(null);
  const pingRef = React.useRef(null);
  const interviewStartRef = React.useRef(null);
  const isSpeakingRef = React.useRef(false);    // true while TTS is playing — pause mic send
  const currentTTSRef = React.useRef(null);     // current AudioBufferSourceNode — stop before new one
  const outputCtxRef = React.useRef(null);      // dedicated playback AudioContext (separate from mic)
  const nextPlayTimeRef = React.useRef(0);      // scheduled end of last queued chunk (Web Audio clock)
  const pendingChunksRef = React.useRef(0);    // count of chunks scheduled but not yet ended
  const readyToListenRef = React.useRef(false); // don't send mic audio until backend says "listening"
  const playQueueRef = React.useRef(Promise.resolve()); // serialise playWav to prevent nextPlayTimeRef race

  // ── Output AudioContext helpers (defined early so useEffects can call them) ──
  function getOutputCtx() {
    if (!outputCtxRef.current) {
      outputCtxRef.current = new AudioContext();
    }
    return outputCtxRef.current;
  }

  async function unlockOutputCtx() {
    const ctx = getOutputCtx();
    if (ctx.state === "suspended") {
      try { await ctx.resume(); } catch (e) { console.warn("AudioContext resume:", e); }
    }
    return ctx;
  }

  // ── Timer ────────────────────────────────────────────────
  const endingRef = React.useRef(false);
  React.useEffect(() => {
    const id = setInterval(() => {
      setSeconds(s => {
        if (isFree) {
          if (s <= 1) {
            clearInterval(id);
            if (!endingRef.current) { endingRef.current = true; handleEnd(); }
            return 0;
          }
          return s - 1;
        }
        return s + 1;
      });
    }, 1000);
    return () => clearInterval(id);
  }, [isFree]);

  const mm = String(Math.floor(seconds / 60)).padStart(2, "0");
  const ss_str = String(seconds % 60).padStart(2, "0");

  // ── Fetch personalised questions from scraper ────────────
  React.useEffect(() => {
    if (!token) return;
    setQLoading(true);
    fetch("https://api.usegreenroom.app/api/questions/generate", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
      body: JSON.stringify({
        role: jobContext.role || "Software Engineer",
        company: jobContext.company || "",
        seniority: jobContext.seniority || "mid",
        job_description: jobContext.jobDescription || "",
        github_languages: jobContext.githubLanguages || [],
        github_top_repos: (jobContext.githubTopRepos || []).map(r => ({
          name: r.name, language: r.language, description: r.description || "",
        })),
      }),
    })
      .then(r => r.json())
      .then(data => {
        const valid = (data.questions || []).filter(q => q && (q.prompt || q.text || "").trim());
        if (valid.length > 0) setQuestions(valid);
      })
      .catch(() => { })
      .finally(() => setQLoading(false));
  }, [token]);

  // ── Camera + mic — just get the stream, WS connects separately ──
  React.useEffect(() => {
    const audioConstraints = {
      echoCancellation: true,   // removes Ari's voice from mic capture
      noiseSuppression: true,
      autoGainControl: true,
    };
    navigator.mediaDevices.getUserMedia({ video: true, audio: audioConstraints })
      .then(stream => {
        const audioStream = new MediaStream(stream.getAudioTracks());
        const videoStream = new MediaStream(stream.getVideoTracks());
        streamRef.current = audioStream;
        videoStreamRef.current = videoStream;
        if (videoRef.current) videoRef.current.srcObject = videoStream;
        setHasCamera(true);
        setHasStream(true);
      })
      .catch(err => {
        setCamError(err.name === "NotAllowedError" ? "Permission denied" : "Camera unavailable");
        navigator.mediaDevices.getUserMedia({ audio: audioConstraints })
          .then(stream => {
            streamRef.current = stream;
            setHasStream(true);
          })
          .catch(() => setStatus("error"));
      });
    return () => cleanup();
  }, []);

  // ── Connect WS only after user clicks Start (needed to unlock AudioContext) ──
  React.useEffect(() => {
    if (!hasStream || !started || wsStartedRef.current) return;
    wsStartedRef.current = true;
    connectWS(streamRef.current);
  }, [hasStream, started]);

  // ── Assign stream to <video> once it mounts (after Start is clicked) ─────────
  React.useEffect(() => {
    if (!started) return;
    if (videoStreamRef.current && videoRef.current) {
      videoRef.current.srcObject = videoStreamRef.current;
    }
  }, [started]);

  // ── Push questions to WS the moment they finish loading ──────────────────
  React.useEffect(() => {
    if (qLoading || !questions.length) return;
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({ type: "questions_update", questions }));
    }
  }, [qLoading, questions]);

  React.useEffect(() => {
    streamRef.current?.getAudioTracks().forEach(t => { t.enabled = !muted; });
  }, [muted]);

  React.useEffect(() => {
    if (!videoOffMounted.current) { videoOffMounted.current = true; return; }
    if (videoOff) {
      videoStreamRef.current?.getTracks().forEach(t => t.stop());
      if (videoRef.current) videoRef.current.srcObject = null;
    } else {
      navigator.mediaDevices.getUserMedia({ video: true })
        .then(vs => {
          videoStreamRef.current = vs;
          if (videoRef.current) videoRef.current.srcObject = vs;
        })
        .catch(() => setCamError("Camera unavailable"));
    }
  }, [videoOff]);

  // ── Monaco Editor ────────────────────────────────────────
  // Depends on `started` because editorRef.current is null until the interview
  // screen renders (the div is inside the !started early-return branch).
  React.useEffect(() => {
    if (!started || !editorRef.current) return;
    const MONACO_CDN = "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs";

    function initEditor() {
      if (monacoInstanceRef.current) return;
      window.require.config({ paths: { vs: MONACO_CDN } });
      window.require(["vs/editor/editor.main"], () => {
        if (!editorRef.current || monacoInstanceRef.current) return;
        monacoInstanceRef.current = monaco.editor.create(editorRef.current, {
          value: "# Write your solution here\n",
          language: "python",
          theme: "vs-dark",
          fontSize: 14,
          minimap: { enabled: false },
          scrollBeyondLastLine: false,
          lineNumbers: "on",
          automaticLayout: true,
          padding: { top: 12 },
        });
      });
    }

    if (window.require) {
      initEditor();
    } else {
      const script = document.createElement("script");
      script.src = `${MONACO_CDN}/loader.min.js`;
      script.onload = initEditor;
      document.head.appendChild(script);
    }

    return () => { monacoInstanceRef.current?.dispose(); monacoInstanceRef.current = null; };
  }, [started]);

  React.useEffect(() => {
    if (!monacoInstanceRef.current) return;
    const model = monacoInstanceRef.current.getModel();
    if (model) monaco.editor.setModelLanguage(model, language);
  }, [language]);

  // ── Auto-scroll transcript ───────────────────────────────
  React.useEffect(() => {
    transcriptEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [transcript]);

  // ── WebSocket ────────────────────────────────────────────
  async function submitRating(realism, accuracy) {
    const publicId = pendingFeedbackRef.current?.public_id;
    if (publicId && (realism > 0 || accuracy > 0)) {
      try {
        await fetch("https://api.usegreenroom.app/api/session/rating", {
          method: "POST",
          headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
          body: JSON.stringify({ public_id: publicId, rating: realism, accuracy_rating: accuracy }),
        });
      } catch {}
    }
    setShowRating(false);
    onEnd(pendingFeedbackRef.current);
  }

  function connectWS(stream) {
    if (!token) { setStatus("error"); return; }
    const ws = new WebSocket(`${WS_URL}?token=${encodeURIComponent(token)}`);
    ws.binaryType = "arraybuffer";
    wsRef.current = ws;

    ws.onopen = () => {
      try {
        ws.send(JSON.stringify({
          type: "start",
          role: jobContext.role || "Software Engineer",
          company: jobContext.company || "",
          questions: questions.length > 0 ? questions : [],
          voice,
          github: jobContext.github || "",
          github_bio: jobContext.githubBio || "",
          github_company: jobContext.githubCompany || "",
          github_location: jobContext.githubLocation || "",
          github_top_repos: jobContext.githubTopRepos || [],
          github_languages: jobContext.githubLanguages || [],
          linkedin_name: jobContext.linkedinName || "",
          linkedin_first_name: jobContext.linkedinFirstName || "",
          linkedin_last_name: jobContext.linkedinLastName || "",
          linkedin_email: jobContext.linkedinEmail || "",
          linkedin_headline: jobContext.linkedinHeadline || "",
          linkedin_bio: jobContext.linkedinBio || "",
          linkedin_experiences: jobContext.linkedinExperiences || [],
          linkedin_skills: jobContext.linkedinSkills || [],
          linkedin_posts: jobContext.linkedinPosts || [],
          job_description: jobContext.jobDescription || "",
        }));
        startAudioCapture(stream);
        interviewStartRef.current = Date.now();
        window.posthog?.capture('interview_started', {
          role: jobContext.role || "Software Engineer",
          company: jobContext.company || "",
          voice,
          has_github: !!(jobContext.github),
          has_linkedin: !!(jobContext.linkedinName),
          has_job_desc: !!(jobContext.jobDescription),
        });
        pingRef.current = setInterval(() => {
          if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" }));
        }, 20000);
        // Start audio recording for replay (separate stream from WS pipeline)
        try {
          const recStream = new MediaStream(stream.getAudioTracks());
          const mime = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus"
                    : MediaRecorder.isTypeSupported("audio/webm")               ? "audio/webm"
                    : "";
          const rec = mime ? new MediaRecorder(recStream, { mimeType: mime }) : new MediaRecorder(recStream);
          rec.ondataavailable = (e) => { if (e.data.size > 0) recordedChunksRef.current.push(e.data); };
          rec.start(1000);
          recorderRef.current = rec;
        } catch (err) { console.warn("MediaRecorder unavailable:", err); }
      } catch (e) { console.error("WS onopen error:", e); setStatus("error"); }
    };

    ws.onmessage = async (ev) => {
      if (typeof ev.data === "string") {
        const msg = JSON.parse(ev.data);
        switch (msg.type) {
          case "status":
            setStatus(msg.value);
            if (msg.value === "starting") setAgentSpeaking(true);
            if (msg.value === "listening") {
              readyToListenRef.current = true;
              setAgentSpeaking(false);
              // Safety net: backend says mic should be open — always honour it
              pendingChunksRef.current = 0;
              isSpeakingRef.current = false;
              // Sync latest editor code to backend
              const code = monacoInstanceRef.current?.getValue() || "";
              if (wsRef.current?.readyState === WebSocket.OPEN) {
                wsRef.current.send(JSON.stringify({ type: "code", content: code }));
              }
            }
            break;
          case "transcript":
            if (msg.speaker === "interviewer") {
              // Reveal words one-by-one so the wait feels intentional
              const words = msg.text.split(" ");
              setTypingText("");
              setTranscript(t => [...t, { speaker: "interviewer", text: "" }]);
              words.forEach((word, i) => {
                setTimeout(() => {
                  setTypingText(words.slice(0, i + 1).join(" "));
                  if (i === words.length - 1) {
                    setTranscript(t => {
                      const next = [...t];
                      next[next.length - 1] = { speaker: "interviewer", text: msg.text };
                      return next;
                    });
                    setTypingText("");
                  }
                }, i * 60);
              });
            } else {
              setTranscript(t => [...t, { speaker: msg.speaker, text: msg.text }]);
            }
            setAgentSpeaking(msg.speaker === "interviewer");
            break;
          case "feedback":
            feedbackRef.current = msg.data;
            // Upload the audio recording (fire and forget — don't block the UI)
            if (msg.data?.public_id && recorderRef.current) {
              const rec = recorderRef.current;
              rec.onstop = async () => {
                try {
                  const blob = new Blob(recordedChunksRef.current, { type: rec.mimeType || "audio/webm" });
                  if (recordedChunksRef.current.length > 0 && blob.size > 5000) {
                    const fd = new FormData();
                    fd.append("file", blob, "mock.webm");
                    fetch(`https://api.usegreenroom.app/api/recording/interview/${msg.data.public_id}`, { method: "POST", body: fd }).catch(() => {});
                  }
                } catch {}
              };
              try { rec.stop(); } catch {}
            }
            break;
          case "ended":
            setStatus("ended");
            cleanup();
            window.posthog?.capture('interview_completed', {
              role: jobContext.role || "Software Engineer",
              company: jobContext.company || "",
              questions_covered: feedbackRef.current?.questions_covered || 0,
              duration_seconds: interviewStartRef.current ? Math.round((Date.now() - interviewStartRef.current) / 1000) : 0,
            });
            pendingFeedbackRef.current = feedbackRef.current;
            setShowRating(true);
            break;
          case "gate":
            cleanup();
            if (msg.reason === "free_used") onUpgrade?.("pro");
            else if (msg.reason === "monthly_cap") setCapHit({ used: msg.used, limit: msg.limit });
            else if (msg.reason === "github_claimed") setGithubClaimed(true);
            else window.location.reload(); // no_github — reload to show the connect gate
            break;
          case "error":
            console.error("Agent error:", msg.message);
            break;
        }
      } else {
        // Binary: WAV audio from piper TTS — block mic immediately before decoding
        isSpeakingRef.current = true;
        await playWav(ev.data);
      }
    };

    ws.onerror = (e) => console.error("WS error", e);
    ws.onclose = () => { clearInterval(pingRef.current); stopAudioCapture(); };
  }

  // ── Mic → 16kHz PCM → WebSocket ──────────────────────────
  async function startAudioCapture(stream) {
    // Prefer the AudioContext pre-created inside handleStart (still inside
    // the user-gesture stack). Fall back to creating one here only if needed.
    const ctx = audioCtxRef.current || new AudioContext({ sampleRate: 16000 });
    audioCtxRef.current = ctx;
    if (ctx.state === "suspended") {
      try { await ctx.resume(); } catch (e) { console.warn("input ctx resume (late):", e); }
    }
    const source = ctx.createMediaStreamSource(stream);
    sourceRef.current = source;
    const processor = ctx.createScriptProcessor(4096, 1, 1);
    processorRef.current = processor;
    processor.onaudioprocess = (e) => {
      if (wsRef.current?.readyState !== WebSocket.OPEN) return;
      if (!readyToListenRef.current) return; // don't send until backend is ready for input
      if (isSpeakingRef.current) return;     // don't send while TTS is playing
      const f32 = e.inputBuffer.getChannelData(0);
      const i16 = new Int16Array(f32.length);
      for (let i = 0; i < f32.length; i++) {
        i16[i] = Math.max(-32768, Math.min(32767, f32[i] * 32768));
      }
      wsRef.current.send(i16.buffer);
    };
    source.connect(processor);
    processor.connect(ctx.destination);
  }

  function stopAudioCapture() {
    try { processorRef.current?.disconnect(); } catch { }
    try { sourceRef.current?.disconnect(); } catch { }
    try { audioCtxRef.current?.close(); } catch { }
  }

  // ── WAV bytes → speaker (chunk-scheduled for streaming TTS) ─
  // playWav serialises through playQueueRef so that concurrent onmessage
  // invocations never race on nextPlayTimeRef. Each chunk awaits the previous
  // decode+schedule before reading/writing the shared time cursor.
  function playWav(arrayBuffer) {
    playQueueRef.current = playQueueRef.current
      .catch(() => {})
      .then(() => _doPlayWav(arrayBuffer));
    return playQueueRef.current;
  }

  async function _doPlayWav(arrayBuffer) {
    if (!arrayBuffer || arrayBuffer.byteLength < 44) {
      isSpeakingRef.current = false;
      return;
    }
    try {
      const ctx = await unlockOutputCtx();
      const decoded = await ctx.decodeAudioData(arrayBuffer.slice(0));
      const src = ctx.createBufferSource();
      src.buffer = decoded;
      src.connect(ctx.destination);
      isSpeakingRef.current = true;
      pendingChunksRef.current += 1;

      // Schedule chunk right after the previous one (or immediately if none queued).
      // Because _doPlayWav runs serially, this read-modify-write is race-free.
      const now = ctx.currentTime;
      const startAt = Math.max(now, nextPlayTimeRef.current);
      nextPlayTimeRef.current = startAt + decoded.duration;

      src.onended = () => {
        pendingChunksRef.current = Math.max(0, pendingChunksRef.current - 1);
        // Only close out when the last queued chunk has finished
        if (pendingChunksRef.current === 0) {
          currentTTSRef.current = null;
          setAgentSpeaking(false);
          // Brief cooldown so speaker echo drains before mic reopens
          setTimeout(() => { isSpeakingRef.current = false; }, 400);
        }
      };
      currentTTSRef.current = src;
      src.start(startAt);
    } catch (e) {
      isSpeakingRef.current = false;
      console.error("Playback error:", e);
    }
  }

  // ── Cleanup ───────────────────────────────────────────────
  function cleanup() {
    clearInterval(pingRef.current);
    stopAudioCapture();
    try { currentTTSRef.current?.stop(); } catch { }
    currentTTSRef.current = null;
    nextPlayTimeRef.current = 0;
    pendingChunksRef.current = 0;
    playQueueRef.current = Promise.resolve();
    try { outputCtxRef.current?.close(); } catch { }
    outputCtxRef.current = null;
    isSpeakingRef.current = false;
    streamRef.current?.getTracks().forEach(t => t.stop());
    videoStreamRef.current?.getTracks().forEach(t => t.stop());
  }

  // ── End session ───────────────────────────────────────────
  async function handleStart() {
    // This click IS a page-level user gesture — unlock both the output context
    // (so Ari can speak) AND pre-create the input context (so the mic can be
    // captured). Doing the input ctx later, after async hops, leaves it
    // suspended in Chrome and the ScriptProcessor never fires.
    await unlockOutputCtx();
    if (!audioCtxRef.current) {
      const ctx = new AudioContext({ sampleRate: 16000 });
      audioCtxRef.current = ctx;
      if (ctx.state === "suspended") {
        try { await ctx.resume(); } catch (e) { console.warn("input ctx resume:", e); }
      }
    }
    setStarted(true);
  }

  function handleEnd() {
    if (ending) return;
    setEnding(true);
    // Stop mic + camera immediately so the candidate's hardware indicator
    // turns off the moment they click End — don't wait for the server.
    try { stopAudioCapture(); } catch {}
    try { streamRef.current?.getTracks().forEach(t => t.stop()); } catch {}
    try { videoStreamRef.current?.getTracks().forEach(t => t.stop()); } catch {}
    try {
      if (recorderRef.current && recorderRef.current.state !== "inactive") {
        recorderRef.current.stop();
      }
    } catch {}
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({ type: "end" }));
    } else {
      onEnd(feedbackRef.current);
    }
  }

  // ── Code block renderer ───────────────────────────────────
  function renderText(text) {
    const parts = text.split(/(```[\s\S]*?```)/g);
    return parts.map((part, i) => {
      if (part.startsWith("```")) {
        const match = part.match(/```(\w*)\n?([\s\S]*?)```/);
        const lang = match?.[1] || "";
        const code = match?.[2]?.trim() || part.replace(/```/g, "").trim();
        return (
          <div key={i} style={{ marginTop: 6, marginBottom: 2 }}>
            <div style={{
              display: "flex", alignItems: "center", justifyContent: "space-between",
              background: "#1e1e2e", borderRadius: "8px 8px 0 0",
              padding: "6px 12px",
            }}>
              <span style={{ font: "500 11px var(--font-mono)", color: "#7c7c9c", textTransform: "lowercase" }}>{lang || "code"}</span>
              <button onClick={() => navigator.clipboard.writeText(code)} style={{
                background: "none", border: "none", cursor: "pointer",
                color: "#7c7c9c", font: "500 11px var(--font-sans)",
                display: "flex", alignItems: "center", gap: 4, padding: 0,
              }}>
                <Icon name="copy" size={12} color="#7c7c9c" /> Copy
              </button>
            </div>
            <pre style={{
              margin: 0, padding: "12px 14px",
              background: "#13131f", borderRadius: "0 0 8px 8px",
              font: "13px/1.6 var(--font-mono)", color: "#cdd6f4",
              overflowX: "auto", whiteSpace: "pre",
            }}>{code}</pre>
          </div>
        );
      }
      return <span key={i}>{part}</span>;
    });
  }

  const statusLabel = {
    connecting: "Connecting…",
    loading: "Loading AI models…",
    starting: "Starting interview…",
    listening: "Listening…",
    processing: "Processing…",
    ended: "Interview ended",
    error: "Connection failed",
  }[status] || status;

  const statusDot = {
    listening: "var(--gr-green-500)",
    processing: "var(--gr-warning)",
    starting: "var(--gr-green-300)",
    loading: "var(--gr-green-300)",
  }[status] || "var(--fg-4)";

  if (showRating) {
    const realismLabels   = ["", "Not at all", "Somewhat", "Mostly", "Very much", "Exactly like one"];
    const accuracyLabels  = ["", "Way off", "Partially right", "Mostly right", "Pretty accurate", "Spot on"];

    function StarRow({ value, onChange }) {
      return (
        <div style={{ display: "flex", gap: 10 }}>
          {[1, 2, 3, 4, 5].map(n => (
            <button key={n} onClick={() => onChange(n)} style={{
              width: 52, height: 52, borderRadius: 999,
              background: value >= n ? "var(--gr-green-500)" : "var(--bg-subtle)",
              border: `1.5px solid ${value >= n ? "var(--gr-green-500)" : "var(--border-1)"}`,
              color: value >= n ? "#fff" : "var(--fg-3)",
              font: "600 18px var(--font-sans)", cursor: "pointer",
              transition: "all 120ms ease",
            }}>{n}</button>
          ))}
        </div>
      );
    }

    // Step indicator
    const StepDots = () => (
      <div style={{ display: "flex", gap: 6 }}>
        {[1, 2].map(s => (
          <div key={s} style={{
            width: s === ratingStep ? 20 : 8, height: 8, borderRadius: 999,
            background: s === ratingStep ? "var(--gr-green-500)" : "var(--border-1)",
            transition: "all 200ms ease",
          }} />
        ))}
      </div>
    );

    return (
      <div style={{ height: "100vh", background: "var(--bg-app)", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 32 }}>
        <img src="../../assets/logo.svg" width="32" height="32" alt="Greenroom" />
        <StepDots />

        {ratingStep === 1 && (
          <>
            <div style={{ textAlign: "center", maxWidth: 400 }}>
              <div style={{ font: "400 28px/1 var(--font-display)", letterSpacing: "-0.02em", color: "var(--fg-1)", marginBottom: 8 }}>Session complete</div>
              <div style={{ font: "var(--text-body)", color: "var(--fg-3)" }}>Did that feel like a real interview?</div>
            </div>
            <StarRow value={sessionRating} onChange={setSessionRating} />
            {sessionRating > 0 && (
              <div style={{ font: "var(--text-body-sm)", color: "var(--fg-3)", marginTop: -16 }}>
                {realismLabels[sessionRating]}
              </div>
            )}
            <div style={{ display: "flex", gap: 12 }}>
              <Button variant="primary" onClick={() => setRatingStep(2)} disabled={sessionRating === 0}>
                Next →
              </Button>
              <Button variant="secondary" onClick={() => submitRating(0, 0)}>
                Skip both
              </Button>
            </div>
          </>
        )}

        {ratingStep === 2 && (
          <>
            <div style={{ textAlign: "center", maxWidth: 440 }}>
              <div style={{ font: "400 28px/1 var(--font-display)", letterSpacing: "-0.02em", color: "var(--fg-1)", marginBottom: 8 }}>One more question</div>
              <div style={{ font: "var(--text-body)", color: "var(--fg-3)", lineHeight: 1.6 }}>
                How accurately did the feedback reflect what you actually know?<br />
                <span style={{ font: "var(--text-body-sm)", color: "var(--fg-4)" }}>Be honest — this improves the AI for everyone.</span>
              </div>
            </div>
            <StarRow value={accuracyRating} onChange={setAccuracyRating} />
            {accuracyRating > 0 && (
              <div style={{ font: "var(--text-body-sm)", color: "var(--fg-3)", marginTop: -16 }}>
                {accuracyLabels[accuracyRating]}
              </div>
            )}
            <div style={{ display: "flex", gap: 12 }}>
              <Button variant="primary" onClick={() => submitRating(sessionRating, accuracyRating)} disabled={accuracyRating === 0}>
                Submit and see feedback
              </Button>
              <Button variant="secondary" onClick={() => submitRating(sessionRating, 0)}>
                Skip this one
              </Button>
            </div>
          </>
        )}
      </div>
    );
  }

  // Free-tier time warning banner — shown at ≤2 min, urgent at ≤60s
  const FreeBanner = isFree && seconds <= 120 && seconds > 0 && started ? (
    <div style={{
      position: "fixed", top: 64, left: 0, right: 0, zIndex: 200,
      background: seconds <= 60 ? "rgba(220,38,38,0.94)" : "rgba(217,119,6,0.92)",
      backdropFilter: "blur(6px)",
      padding: "10px 20px",
      display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12,
      font: "500 13px var(--font-sans)", color: "#fff",
    }}>
      <span>
        {seconds <= 60
          ? `⚡ Less than 1 minute left on your free session — wrapping up now.`
          : `⏱ 2 minutes remaining on your free 10-min session.`}
      </span>
      {onUpgrade && (
        <button onClick={() => onUpgrade("pro")} style={{
          padding: "5px 14px", borderRadius: 8, border: "1.5px solid rgba(255,255,255,0.5)",
          background: "rgba(255,255,255,0.15)", color: "#fff",
          font: "600 12px var(--font-sans)", cursor: "pointer", flexShrink: 0,
          transition: "background 150ms",
        }}>
          Upgrade for unlimited →
        </button>
      )}
    </div>
  ) : null;

  // GitHub account already used on a different Greenroom account
  if (capHit) {
    return (
      <div style={{ height: "100vh", background: "var(--bg-app)", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 24 }}>
        <img src="../../assets/logo.svg" width="36" height="36" alt="Greenroom" onClick={onHome} style={{ cursor: onHome ? "pointer" : "default" }} />
        <div style={{ textAlign: "center", maxWidth: 380 }}>
          <div style={{ font: "400 28px/1 var(--font-display)", letterSpacing: "-0.02em", color: "var(--fg-1)", marginBottom: 8 }}>Monthly limit reached</div>
          <div style={{ font: "var(--text-body)", color: "var(--fg-3)", lineHeight: 1.6 }}>
            You've used {capHit.used} of {capHit.limit} sessions this month. Your sessions reset on the 1st. Come back then, or contact support if you need more.
          </div>
        </div>
        <Button variant="secondary" onClick={onHome}>Go back</Button>
      </div>
    );
  }

  if (githubClaimed) {
    return (
      <div style={{ height: "100vh", background: "var(--bg-app)", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 24 }}>
        <img src="../../assets/logo.svg" width="36" height="36" alt="Greenroom" onClick={onHome} style={{ cursor: onHome ? "pointer" : "default" }} />
        <div style={{ textAlign: "center", maxWidth: 380 }}>
          <div style={{ font: "400 28px/1 var(--font-display)", letterSpacing: "-0.02em", color: "var(--fg-1)", marginBottom: 8 }}>GitHub already in use</div>
          <div style={{ font: "var(--text-body)", color: "var(--fg-3)" }}>
            This GitHub account has already been used for a free mock on another Greenroom account. Each GitHub account is limited to one free session. Upgrade to Pro for unlimited access.
          </div>
        </div>
        <div style={{ display: "flex", gap: 12 }}>
          <Button variant="primary" onClick={() => onUpgrade?.("pro")}>Upgrade to Pro — $15/mo</Button>
          <Button variant="secondary" onClick={onHome}>Go back</Button>
        </div>
      </div>
    );
  }

  if (!started) {
    return (
      <div style={{ height: "100vh", background: "var(--bg-app)", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 24 }}>
        <img src="../../assets/logo.svg" width="36" height="36" alt="Greenroom" onClick={onHome} style={{ cursor: onHome ? "pointer" : "default" }} />
        <div style={{ textAlign: "center" }}>
          <div style={{ font: "400 28px/1 var(--font-display)", letterSpacing: "-0.02em", color: "var(--fg-1)", marginBottom: 8 }}>Ready to start?</div>
          <div style={{ font: "var(--text-body)", color: "var(--fg-3)", maxWidth: 320 }}>
            {hasStream ? "Your mic and camera are ready." : "Waiting for mic permission…"}
          </div>
        </div>
        <Button variant="primary" onClick={handleStart} disabled={!hasStream}>
          Start Interview
        </Button>
      </div>
    );
  }

  // ── Shared sub-panels (used in both mobile and desktop) ─────────────────

  const transcriptPanel = (
    <Card padding={0} style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden", minHeight: 0, borderRadius: isMobile ? 0 : undefined, border: isMobile ? "none" : undefined }}>
      {!isMobile && (
        <div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border-1)", flexShrink: 0 }}>
          <Eyebrow>Live transcript</Eyebrow>
        </div>
      )}
      <div style={{
        flex: 1, minHeight: 0, overflowY: "auto", padding: "12px 16px",
        display: "flex", flexDirection: "column", gap: 10,
        WebkitOverflowScrolling: "touch",
      }}>
        {transcript.length === 0 && status !== "processing" && (
          <div style={{ font: "var(--text-body-sm)", color: "var(--fg-4)", textAlign: "center", padding: "32px 0" }}>
            Transcript will appear here…
          </div>
        )}
        {transcript.map((entry, i) => {
          const isLast = i === transcript.length - 1;
          const displayText = isLast && entry.speaker === "interviewer" && typingText ? typingText : entry.text;
          if (!displayText && !typingText) return null;
          return (
            <div key={i} style={{
              display: "flex", flexDirection: "column", gap: 2,
              alignItems: entry.speaker === "candidate" ? "flex-end" : "flex-start",
            }}>
              <span style={{ font: "var(--text-caption)", color: "var(--fg-4)", textTransform: "uppercase", letterSpacing: "0.06em" }}>
                {entry.speaker === "candidate" ? "You" : "Coach Ari"}
              </span>
              <div style={{
                maxWidth: "90%", padding: "8px 12px", borderRadius: 10,
                font: "var(--text-body-sm)", lineHeight: 1.55,
                background: entry.speaker === "candidate" ? "var(--bg-brand-soft)" : "var(--bg-subtle)",
                color: entry.speaker === "candidate" ? "var(--gr-green-700)" : "var(--fg-1)",
                border: `1px solid ${entry.speaker === "candidate" ? "var(--gr-green-100)" : "var(--border-1)"}`,
              }}>
                {renderText(displayText)}
                {isLast && entry.speaker === "interviewer" && typingText && (
                  <span style={{ display: "inline-block", width: 2, height: "1em", background: "var(--gr-green-500)", marginLeft: 2, verticalAlign: "text-bottom", animation: "blink 700ms step-end infinite" }} />
                )}
              </div>
            </div>
          );
        })}
        {status === "processing" && (
          <div style={{ display: "flex", flexDirection: "column", gap: 2, alignItems: "flex-start" }}>
            <span style={{ font: "var(--text-caption)", color: "var(--fg-4)", textTransform: "uppercase", letterSpacing: "0.06em" }}>Coach Ari</span>
            <div style={{
              padding: "10px 14px", borderRadius: 10, background: "var(--bg-subtle)",
              border: "1px solid var(--border-1)", display: "flex", alignItems: "center", gap: 6,
            }}>
              {[0, 1, 2].map(i => (
                <span key={i} style={{
                  width: 7, height: 7, borderRadius: 999,
                  background: "var(--fg-3)", display: "inline-block",
                  animation: `thinking 1.2s ease-in-out ${i * 0.2}s infinite`,
                }} />
              ))}
            </div>
          </div>
        )}
        <div ref={transcriptEndRef} />
      </div>
    </Card>
  );

  const editorPanel = (
    <section style={{
      display: "flex", flexDirection: "column",
      minWidth: 0, minHeight: 0, flex: 1,
      borderRadius: isMobile ? 0 : 12,
      overflow: "hidden",
      border: isMobile ? "none" : "1px solid var(--border-1)",
    }}>
      <div style={{ background: "#1e1e2e", padding: "8px 12px", flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "space-between", borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
        <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
          {["python", "javascript", "java", "cpp"].map(lang => (
            <button key={lang} onClick={() => setLanguage(lang)} style={{
              padding: "3px 10px", borderRadius: 4, border: "none", cursor: "pointer",
              font: "500 12px var(--font-mono)",
              background: language === lang ? "rgba(255,255,255,0.12)" : "transparent",
              color: language === lang ? "#cdd6f4" : "#7c7c9c",
              transition: "all 100ms",
            }}>{lang}</button>
          ))}
        </div>
        <button onClick={() => {
          const code = monacoInstanceRef.current?.getValue() || "";
          navigator.clipboard.writeText(code);
        }} style={{ background: "none", border: "none", cursor: "pointer", color: "#7c7c9c", display: "flex", alignItems: "center", gap: 4, font: "12px var(--font-sans)", flexShrink: 0 }}>
          <Icon name="copy" size={12} color="#7c7c9c" /> Copy
        </button>
      </div>
      {/* touch-action: none lets Monaco capture touch scroll events on mobile */}
      <div ref={editorRef} style={{ flex: 1, minHeight: 0, touchAction: "none" }} />
    </section>
  );

  // ── Mobile layout — split view ───────────────────────────────────────────
  if (isMobile) {
    return (
      <div data-screen-label="Mock interview room" style={{ height: "100dvh", background: "var(--bg-app)", display: "flex", flexDirection: "column", overflow: "hidden" }}>
        {FreeBanner}

        {/* Slim header: logo | status | timer | end */}
        <header style={{
          padding: "8px 14px", borderBottom: "1px solid var(--border-1)", background: "var(--bg-surface)",
          display: "flex", alignItems: "center", gap: 10, flexShrink: 0, height: 44,
        }}>
          <img src="../../assets/logo.svg" width="18" height="18" alt="Greenroom" onClick={onHome} style={{ cursor: onHome ? "pointer" : "default", flexShrink: 0 }} />
          <div style={{ display: "flex", alignItems: "center", gap: 5, flex: 1, minWidth: 0 }}>
            <div style={{ width: 6, height: 6, borderRadius: 999, background: statusDot, flexShrink: 0 }} />
            <span style={{ font: "500 12px var(--font-sans)", color: "var(--fg-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
              {qLoading ? "Loading…" : statusLabel}
            </span>
          </div>
          <span style={{ font: "500 15px var(--font-mono)", color: isFree && seconds <= 60 ? "var(--gr-red-500, #ef4444)" : "var(--fg-1)", fontVariantNumeric: "tabular-nums", flexShrink: 0 }}>{mm}:{ss_str}</span>
          <Button variant="danger" onClick={handleEnd} disabled={ending} style={{ padding: "5px 10px", font: "500 12px var(--font-sans)", flexShrink: 0 }}>
            {ending ? "…" : "End session"}
          </Button>
        </header>

        {/* ── Top half: transcript with floating video thumbnail ── */}
        <div style={{ flex: 1, minHeight: 0, position: "relative", background: "var(--bg-surface)", borderBottom: "1px solid var(--border-1)", display: "flex", flexDirection: "column" }}>

          {/* Video thumbnail — top-left floating */}
          <div style={{
            position: "absolute", top: 10, left: 10, zIndex: 20,
            width: 88, height: 66, borderRadius: 10, overflow: "hidden",
            border: "1.5px solid var(--border-1)",
            background: "#111113",
            boxShadow: "0 2px 12px rgba(0,0,0,0.25)",
          }}>
            <video ref={videoRef} autoPlay playsInline muted style={{
              width: "100%", height: "100%", objectFit: "cover",
              display: hasCamera && !videoOff ? "block" : "none",
            }} />
            {(!hasCamera || videoOff) && (
              <div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }}>
                <Avatar initials="JL" size={28} brand />
              </div>
            )}
            {/* Ari waveform badge */}
            {agentSpeaking && (
              <div style={{
                position: "absolute", bottom: 4, left: 0, right: 0,
                display: "flex", justifyContent: "center",
              }}>
                <div style={{ background: "rgba(0,0,0,0.55)", borderRadius: 999, padding: "2px 6px", display: "flex", alignItems: "center", gap: 3 }}>
                  <Avatar initials="AI" size={12} brand />
                  <Waveform active />
                </div>
              </div>
            )}
          </div>

          {/* Transcript scroll area — left-padded so first messages clear the video */}
          <div style={{
            flex: 1, minHeight: 0, overflowY: "auto", WebkitOverflowScrolling: "touch",
            padding: "10px 12px 10px 12px",
            display: "flex", flexDirection: "column", gap: 8,
          }}>
            {/* Spacer so the first message doesn't go under the video */}
            <div style={{ height: 72, flexShrink: 0 }} />

            {transcript.length === 0 && status !== "processing" && (
              <div style={{ font: "var(--text-body-sm)", color: "var(--fg-4)", textAlign: "center", padding: "16px 0" }}>
                Transcript will appear here…
              </div>
            )}
            {transcript.map((entry, i) => {
              const isLast = i === transcript.length - 1;
              const displayText = isLast && entry.speaker === "interviewer" && typingText ? typingText : entry.text;
              if (!displayText && !typingText) return null;
              return (
                <div key={i} style={{
                  display: "flex", flexDirection: "column", gap: 2,
                  alignItems: entry.speaker === "candidate" ? "flex-end" : "flex-start",
                }}>
                  <span style={{ font: "var(--text-caption)", color: "var(--fg-4)", textTransform: "uppercase", letterSpacing: "0.06em" }}>
                    {entry.speaker === "candidate" ? "You" : "Ari"}
                  </span>
                  <div style={{
                    maxWidth: "88%", padding: "7px 11px", borderRadius: 10,
                    font: "var(--text-body-sm)", lineHeight: 1.5,
                    background: entry.speaker === "candidate" ? "var(--bg-brand-soft)" : "var(--bg-subtle)",
                    color: entry.speaker === "candidate" ? "var(--gr-green-700)" : "var(--fg-1)",
                    border: `1px solid ${entry.speaker === "candidate" ? "var(--gr-green-100)" : "var(--border-1)"}`,
                  }}>
                    {renderText(displayText)}
                    {isLast && entry.speaker === "interviewer" && typingText && (
                      <span style={{ display: "inline-block", width: 2, height: "1em", background: "var(--gr-green-500)", marginLeft: 2, verticalAlign: "text-bottom", animation: "blink 700ms step-end infinite" }} />
                    )}
                  </div>
                </div>
              );
            })}
            {status === "processing" && (
              <div style={{ display: "flex", flexDirection: "column", gap: 2, alignItems: "flex-start" }}>
                <span style={{ font: "var(--text-caption)", color: "var(--fg-4)", textTransform: "uppercase", letterSpacing: "0.06em" }}>Ari</span>
                <div style={{ padding: "8px 12px", borderRadius: 10, background: "var(--bg-subtle)", border: "1px solid var(--border-1)", display: "flex", alignItems: "center", gap: 6 }}>
                  {[0, 1, 2].map(i => (
                    <span key={i} style={{ width: 6, height: 6, borderRadius: 999, background: "var(--fg-3)", display: "inline-block", animation: `thinking 1.2s ease-in-out ${i * 0.2}s infinite` }} />
                  ))}
                </div>
              </div>
            )}
            <div ref={transcriptEndRef} />
          </div>
        </div>

        {/* ── Bottom half: Monaco editor ── */}
        <div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", background: "#1e1e2e" }}>

          {/* Editor toolbar */}
          <div style={{ padding: "6px 10px", flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "space-between", borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
            <div style={{ display: "flex", gap: 3 }}>
              {["python", "javascript", "java", "cpp"].map(lang => (
                <button key={lang} onClick={() => setLanguage(lang)} style={{
                  padding: "3px 8px", borderRadius: 4, border: "none", cursor: "pointer",
                  font: "500 11px var(--font-mono)",
                  background: language === lang ? "rgba(255,255,255,0.12)" : "transparent",
                  color: language === lang ? "#cdd6f4" : "#7c7c9c",
                }}>{lang}</button>
              ))}
            </div>
            <button onClick={() => { const code = monacoInstanceRef.current?.getValue() || ""; navigator.clipboard.writeText(code); }} style={{ background: "none", border: "none", cursor: "pointer", color: "#7c7c9c", display: "flex", alignItems: "center", gap: 4, font: "11px var(--font-sans)", flexShrink: 0 }}>
              <Icon name="copy" size={11} color="#7c7c9c" /> Copy
            </button>
          </div>

          {/* Monaco mount */}
          <div ref={editorRef} style={{ flex: 1, minHeight: 0, touchAction: "none" }} />

          {/* Mute button — bottom centre */}
          <div style={{ padding: "10px 16px", flexShrink: 0, display: "flex", justifyContent: "center", background: "#16161f", borderTop: "1px solid rgba(255,255,255,0.06)" }}>
            <button onClick={() => setMuted(m => !m)} style={{
              width: 52, height: 52, borderRadius: 999,
              background: muted ? "var(--gr-danger)" : "rgba(255,255,255,0.10)",
              border: muted ? "none" : "1.5px solid rgba(255,255,255,0.15)",
              cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center",
              transition: "background 150ms ease",
            }}>
              <Icon name={muted ? "mic-off" : "mic"} size={22} color="#fff" />
            </button>
          </div>
        </div>

      </div>
    );
  }

  // ── Desktop layout ───────────────────────────────────────────────────────
  return (
    <div data-screen-label="Mock interview room" style={{ height: "100vh", background: "var(--bg-app)", display: "flex", flexDirection: "column", overflow: "hidden" }}>
      {FreeBanner}
      <header style={{
        padding: "12px 24px", borderBottom: "1px solid var(--border-1)", background: "var(--bg-surface)",
        display: "flex", alignItems: "center", gap: 16, flexShrink: 0,
      }}>
        <img src="../../assets/logo.svg" width="22" height="22" alt="Greenroom" onClick={onHome} style={{ cursor: onHome ? "pointer" : "default", flexShrink: 0 }} />
        <div>
          <div style={{ font: "500 14px var(--font-sans)" }}>Mock interview · AI Voice</div>
          <div style={{ font: "var(--text-body-sm)", color: "var(--fg-3)", display: "flex", alignItems: "center", gap: 6 }}>
            {qLoading && <div style={{ width: 10, height: 10, borderRadius: 999, border: "1.5px solid var(--gr-green-200)", borderTopColor: "var(--gr-green-500)", animation: "spin 700ms linear infinite", flexShrink: 0 }} />}
            {qLoading ? "Generating personalised questions…" : statusLabel}
          </div>
        </div>
        <div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 18 }}>
          {status === "connecting" && (
            <div style={{ display: "flex", alignItems: "center", gap: 4, background: "var(--bg-subtle)", borderRadius: 8, padding: 4, border: "1px solid var(--border-1)" }}>
              {["female", "male"].map(v => (
                <button key={v} onClick={() => setVoice(v)} style={{
                  padding: "5px 12px", borderRadius: 6, border: "none", cursor: "pointer",
                  font: "500 13px var(--font-sans)",
                  background: voice === v ? "#fff" : "transparent",
                  color: voice === v ? "var(--fg-1)" : "var(--fg-3)",
                  boxShadow: voice === v ? "0 1px 3px rgba(0,0,0,0.08)" : "none",
                  display: "flex", alignItems: "center", gap: 6,
                  transition: "all 120ms ease",
                }}>
                  <Icon name={v === "female" ? "user-round" : "user"} size={13} />
                  {v.charAt(0).toUpperCase() + v.slice(1)}
                </button>
              ))}
            </div>
          )}
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <Icon name="clock" size={16} color="var(--fg-3)" />
            <span style={{ font: "500 22px var(--font-mono)", color: isFree && seconds <= 60 ? "var(--gr-red-500, #ef4444)" : "var(--fg-1)", fontVariantNumeric: "tabular-nums" }}>{mm}:{ss_str}</span>
          </div>
          <Button variant="danger" onClick={handleEnd} disabled={ending}>
            {ending ? "Ending…" : "End session"}
          </Button>
        </div>
      </header>

      <main style={{ flex: 1, minHeight: 0, padding: 24, display: "grid", gridTemplateColumns: "260px 1fr 320px", gap: 16, overflow: "hidden" }}>
        {/* Video + status */}
        <section style={{ display: "flex", flexDirection: "column", gap: 16, minWidth: 0, minHeight: 0 }}>
          <div style={{
            position: "relative", flex: 1, minHeight: 200,
            background: "linear-gradient(180deg, #1a1a1c 0%, #0a0a0b 100%)",
            borderRadius: 14, overflow: "hidden", border: "1px solid var(--gr-stone-800)",
          }}>
            <video ref={videoRef} autoPlay playsInline muted style={{
              position: "absolute", inset: 0, width: "100%", height: "100%",
              objectFit: "cover", display: hasCamera && !videoOff ? "block" : "none",
            }} />
            {!hasCamera && (
              <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center" }}>
                <div style={{ textAlign: "center", color: "var(--gr-stone-500)" }}>
                  <Icon name={camError ? "video-off" : "loader"} size={32} color="var(--gr-stone-500)" />
                  <div style={{ marginTop: 10, font: "var(--text-body-sm)" }}>{camError || "Starting camera…"}</div>
                </div>
              </div>
            )}
            {hasCamera && videoOff && (
              <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", background: "#111113" }}>
                <Avatar initials="JL" size={72} brand />
              </div>
            )}
            <div style={{
              position: "absolute", bottom: 16, left: 16,
              padding: "8px 12px", borderRadius: 999,
              background: "rgba(255,255,255,0.92)", backdropFilter: "blur(6px)",
              display: "flex", alignItems: "center", gap: 10,
              border: "1px solid rgba(0,0,0,0.04)",
            }}>
              <Avatar initials="AI" size={24} brand />
              <span style={{ font: "500 13px var(--font-sans)" }}>Coach Ari</span>
              <Waveform active={agentSpeaking} />
            </div>
            <div style={{
              position: "absolute", bottom: 16, left: "50%", transform: "translateX(-50%)",
              display: "flex", gap: 8,
              background: "rgba(0,0,0,0.55)", backdropFilter: "blur(8px)",
              padding: "6px 10px", borderRadius: 999,
              border: "1px solid rgba(255,255,255,0.08)",
            }}>
              <button onClick={() => setVideoOff(v => !v)} style={{
                width: 40, height: 40, borderRadius: 999,
                background: videoOff ? "var(--gr-danger)" : "rgba(255,255,255,0.14)",
                border: "none", cursor: "pointer",
                display: "flex", alignItems: "center", justifyContent: "center",
                transition: "background 120ms ease",
              }}><Icon name={videoOff ? "video-off" : "video"} size={18} color="#fff" /></button>
              <button onClick={() => setMuted(m => !m)} style={{
                width: 40, height: 40, borderRadius: 999,
                background: muted ? "var(--gr-danger)" : "rgba(255,255,255,0.14)",
                border: "none", cursor: "pointer",
                display: "flex", alignItems: "center", justifyContent: "center",
                transition: "background 120ms ease",
              }}><Icon name={muted ? "mic-off" : "mic"} size={18} color="#fff" /></button>
            </div>
          </div>

          <Card padding={20}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
              <div style={{ width: 8, height: 8, borderRadius: 999, background: statusDot }} />
              <Eyebrow>{statusLabel}</Eyebrow>
            </div>
            {status === "loading" ? (
              <div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 4 }}>
                <div style={{
                  width: 18, height: 18, borderRadius: 999, flexShrink: 0,
                  border: "2px solid var(--gr-green-200)",
                  borderTopColor: "var(--gr-green-500)",
                  animation: "spin 700ms linear infinite",
                }} />
                <span style={{ font: "var(--text-body)", color: "var(--fg-2)" }}>
                  Loading Whisper speech model — this takes a few seconds on first connect…
                </span>
              </div>
            ) : (
              <p style={{ font: "var(--text-body)", color: "var(--fg-2)", margin: 0 }}>
                {status === "listening" && "Speak naturally — the interviewer is listening."}
                {status === "processing" && "Processing your answer…"}
                {status === "starting" && "The interviewer is introducing the session."}
                {status === "connecting" && "Connecting to voice agent…"}
                {status === "error" && "Could not connect. Make sure the voice agent server is running on port 8000."}
              </p>
            )}
          </Card>
        </section>

        {/* Code Editor — fixed Monaco scroll */}
        {editorPanel}

        {/* Transcript */}
        <aside style={{ display: "flex", flexDirection: "column", minWidth: 0, minHeight: 0 }}>
          {transcriptPanel}
        </aside>
      </main>
    </div>
  );
}

if (!document.getElementById("gr-wave-kf")) {
  const _s = document.createElement("style");
  _s.id = "gr-wave-kf";
  _s.textContent = `
    @keyframes wave { from { transform: scaleY(0.5); } to { transform: scaleY(1.2); } }
    @keyframes thinking { 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
    @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
  `;
  document.head.appendChild(_s);
}

function Waveform({ active }) {
  return (
    <div style={{ display: "inline-flex", alignItems: "center", gap: 2, height: 14 }}>
      {[6, 12, 8, 14, 6, 10, 4].map((h, i) => (
        <span key={i} style={{
          width: 3, borderRadius: 2,
          background: "var(--gr-green-500)",
          height: active ? h : 3,
          transition: "height 200ms var(--ease-out)",
          animation: active ? `wave ${800 + i * 70}ms ease-in-out infinite alternate` : "none",
        }}></span>
      ))}
    </div>
  );
}

window.MockRoom = MockRoom;
